Compare commits

...

1686 Commits

Author SHA1 Message Date
bab3681ac2 Merge pull request 'Merge XMPP reply feature into upstream repo' (#5) from uwaru/matterbridge:master into updatexmppp
Some checks failed
Development / test-build-upload (1.22.x, ubuntu-latest) (push) Waiting to run
Development / golangci-lint (push) Has been cancelled
Reviewed-on: #5
2025-02-23 09:52:49 -08:00
b74b884793
xmpp: Add support for replies
Some checks failed
Development / golangci-lint (pull_request) Has been cancelled
Development / test-build-upload (1.22.x, ubuntu-latest) (pull_request) Has been cancelled
2024-11-04 04:52:16 +01:00
Wim
c4157a4d5b
Update dependencies (#2180)
Some checks failed
Development / golangci-lint (push) Has been cancelled
Development / test-build-upload (1.22.x, ubuntu-latest) (push) Has been cancelled
* Update dependencies

* Fix whatsmeow API changes
2024-08-27 19:04:05 +02:00
Wim
d16645c952
Update mattermost library (#2152)
* Update mattermost library

* Fix linting
2024-05-24 23:08:09 +02:00
Wim
65d78e38af
Update go-xmpp dependency (#2151) 2024-05-24 01:28:32 +02:00
Wim
996e4a7fcf
Update go-xmpp dependency 2024-05-24 01:16:34 +02:00
Wim
a89267943c
Fix linting 2024-05-24 00:31:18 +02:00
Wim
815d8b804f
Fix linting 2024-05-24 00:23:50 +02:00
Brian S. Stephan
6edd5de3b7
Trim nick whitespace when looking up the guild members (discord) (#2072)
this fixes the matching to attempt to find avatars

fixes #2059
2024-05-24 00:19:31 +02:00
Quantum0
2e9db32a83
Fix typos in matterbridge.toml.sample (#2077) 2024-05-24 00:18:27 +02:00
Ben Wiederhake
4bf1c0450c
Split messages if necessary (discord) (#2124)
* Implement and test byte-splitting helper function

* Implement discord botuser message splitting

* Implement discord webhooks message splitting
2024-05-24 00:14:45 +02:00
patrickxia
0bb521512a
Do not bridge CTCP commands other than ACTION (irc) (#2090)
CTCP commands other than ACTION are designed for client-to-client interaction on
IRC networks. Drop such messages when we receive them.

Also get rid of a "CTCP_ACTION" handler in the handler registration.

This

1) can't do anything (if anything, we wanted the string constant
   girc.CTCP_ACTION, which is "ACTION")
2) doesn't do anything in this context, because CTCP handlers are
   registered separately:
   f47717952b/ctcp.go (L205)

The PRIVMSG handler already listens to all CTCPs.

Thanks to @lexande for the bug report (found on a live instance).
2024-05-24 00:05:55 +02:00
Wim
13fd5c5d5b
Fix linting 2024-05-24 00:03:29 +02:00
Ben Wiederhake
d055b4530e
Correctly split three-or-more byte sequences of UTF-8 (#2123) 2024-05-24 00:02:09 +02:00
Neil Hanlon
6b528ffa4f
Update post types and include system removals in skip logic (mattermost) (#2125) 2024-05-23 23:58:58 +02:00
Wim
fa147c076f
Fix linting 2024-05-23 23:57:30 +02:00
Bryan Davis
b2df32bc81
Clear existing IRC event handlers before connecting to new ones (irc) (#2138)
Clear IRC event handlers that we will be registering for the new
connection before registering new handlers. This prevents duplicate
event handlers in the case where we are connecting via a BNC and are
seeing a reconnect. Attempting to clear handlers when none have been set
is a no-op.

Fixes 42wim#1564
Co-authored-by: Andreas Vögele <andreas@andreasvoegele.com>
2024-05-23 23:55:31 +02:00
guangwu
733f4c71b8
Close res body (rocketchat) (#2135) 2024-05-23 23:53:27 +02:00
erentar
70e8c6e9d3
Move deprecated/inactive 3rd party bridges to their own sections (#2143) 2024-05-23 23:50:32 +02:00
Wim
2f33fe86f5
Update dependencies and build to go1.22 (#2113)
* Update dependencies and build to go1.22

* Fix api changes wrt to dependencies

* Update golangci config
2024-05-23 23:44:31 +02:00
Wim
56e7bd01ca
Update dependencies and remove old matterclient lib (#2067) 2023-08-05 20:43:19 +02:00
Juhani Karppinen
9459495484
Fix typo in logging (#2056) 2023-08-05 20:39:46 +02:00
Wim
89b0d362d2
Remove gitter bridge (#2035)
See https://blog.gitter.im/2023/02/13/gitter-has-fully-migrated-to-matrix/
2023-04-05 23:39:15 +02:00
Joseph Mansy
574f25337d
Add nil check for group update (telegram) (#2036) 2023-04-05 23:08:03 +02:00
Joseph Mansy
5bbe422161
Support telegram chat Join/Leave updates (#2019)
Co-authored-by: Wim <wim@42.be>
2023-04-03 23:27:44 +02:00
Faye Duxovni
6500714a93
Don't treat nil as an unknown type of error (zulip) (#2024)
Fixes #1869
2023-04-03 23:27:00 +02:00
Thom Dickson
5feafcddba
Fix broken reply (telegram) (#2026)
Fixes #2021 

* Fix broken reply

* Fix reply/quoting logic with topics

* Update handlers.go

---------

Co-authored-by: Wim <wim@42.be>
2023-04-03 23:20:53 +02:00
mvoolt
3e20a3d180
Create mumble-autolink.tengo (#2029) 2023-04-03 23:00:54 +02:00
dependabot[bot]
60639b6e50
Bump google.golang.org/protobuf from 1.29.0 to 1.29.1 (#2018)
Bumps [google.golang.org/protobuf](https://github.com/protocolbuffers/protobuf-go) from 1.29.0 to 1.29.1.
- [Release notes](https://github.com/protocolbuffers/protobuf-go/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf-go/blob/master/release.bash)
- [Commits](https://github.com/protocolbuffers/protobuf-go/compare/v1.29.0...v1.29.1)

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-15 22:22:38 +01:00
Joseph Mansy
839f384e45
Return a message ID when sending with a webhook (discord) (#1976)
Resolves #1975
2023-03-14 23:16:22 +01:00
Joseph Mansy
d42277979a
Support topic changes and Join/Leave messages (whatsapp) (#2017) 2023-03-14 23:12:45 +01:00
Wim
24cf007a74
Update matterbridge.toml.sample 2023-03-14 23:05:48 +01:00
Thom Dickson
601f48a50e
Add support for Telegram topics (telegram) (#1942)
Topics are surfaced by appending /<topic-id> to the channel setting for the gateway. 
An example for the topic with ID of 16 would be:

```
[[gateway.inout]]
account="telegram.mytelegram"
channel="-100xxxxxxxxxx/16"
```
2023-03-14 23:03:04 +01:00
Wim
c2b8e298d8
Add extra error checking for fileinfo (api) (#2015) 2023-03-11 23:10:41 +01:00
Asiel Díaz Benítez
0917e17383
Allow to send files in Extra via /api/message (api) (#1993) 2023-03-11 22:34:27 +01:00
Wim
8587fa8585
Fix possible panic in mattermost. (mattermost) Fixes #1947 (#2014) 2023-03-11 18:55:29 +01:00
Wim
f345eeae55
Set correct loglevel (whatsapp). Fixes #1980 (#2013) 2023-03-11 18:18:24 +01:00
Wim
89e2dbac15
Check client disconnect to exit for loop (api). Fixes #1983 (#2012)
Also update to latest melody upstream
2023-03-11 18:14:49 +01:00
Wim
356ada872c
Add handlers at the end. Fixes #1988 (discord) (#2011) 2023-03-11 17:55:11 +01:00
mvoolt
a3deb48726
Add support for Mumble servers with no message length limit (#2008)
* Rename .{jfif,jpe} to .jpg (mumble)

* Fix messages not sending in properly if no limit is set (mumble)

Co-authored-by: yellows111 <ice_ice@email.com>

* Formatting fix (mumble)

---------

Co-authored-by: yellows111 <ice_ice@email.com>
2023-03-10 22:57:35 +01:00
ilmaisin
544cd5cd9e
Fix Android media download problem (whatsapp) (#2010) 2023-03-10 22:30:48 +01:00
Wim
53b63adc71
Update to actions v3 (#2009) 2023-03-10 22:29:44 +01:00
Joseph Mansy
2c349c50c7
Support handling delete message from whatsapp. (#1986) 2023-03-10 22:28:32 +01:00
Wim
08779c2909
Update dependencies (#2007)
* Update dependencies
2023-03-09 22:48:00 +01:00
Wim
d5f9cdf912
Update builds to go 1.20 (#2006) 2023-03-09 22:29:58 +01:00
Joseph Mansy
3a4bdd7c56
Fix rate-overlimit issue when there are more than 5 whatsapp channels in a single account (whatsapp) (#1998)
Resolves #1844
2023-03-09 22:10:09 +01:00
Asiel Díaz Benítez
2b236f3ff7
add deltachat to 3rd party API bridges (#1992) 2023-03-09 22:08:29 +01:00
Joseph Mansy
768fb791c5
Use AuthorSignature as a fallback username (telegram) (#1979)
This comes in handy for annoucement type channels where neither the SenderChat or From structs contain name information.

Also Tweak username logic as when using a full name the username field can be " " instead of "".
2023-03-09 22:02:31 +01:00
Joseph Mansy
d00dcf3f58
Handle Whatsapp threading/replies (whatsapp) (#1974)
* Handle Whatsapp threading/replies.
In this change we are updating whatsapp message IDs to include sender JID as this is necessary to reply to a message
https://github.com/tulir/whatsmeow/issues/88#issuecomment-1093195237
https://github.com/tulir/whatsmeow/discussions/148#discussioncomment-3094325

Based on commit 6afa93e537c53b371db37f35a3546ff0fb669416 from #1934
    Author: Iiro Laiho <iiro.laiho@iki.fi>

* Fix replies.
Sender JID can have a `:` inside of it, using `/` as a delimiter now.
Added messageID parser + struct.
messages sent with an attachment do not show replies
But at least common `sendMessage` will make repies from whatsapp to an attachement bridge across.

The new message ID format broke message deleting, so we change the messageID into the real id at the beginning of send.
We really do need the extra info for when we reply to a message though.

* Refactored message replies.
file/Image/audio/replies all work now.
2023-03-09 21:57:19 +01:00
Ben Wiederhake
d5980303e5
Provide a multi-room multi-bridge example (#1972) 2023-03-09 21:51:11 +01:00
dependabot[bot]
f436ae7b31
Bump golang.org/x/image from 0.3.0 to 0.5.0 (#1973)
Bumps [golang.org/x/image](https://github.com/golang/image) from 0.3.0 to 0.5.0.
- [Release notes](https://github.com/golang/image/releases)
- [Commits](https://github.com/golang/image/compare/v0.3.0...v0.5.0)

---
updated-dependencies:
- dependency-name: golang.org/x/image
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-09 21:50:26 +01:00
Joseph Mansy
a9ccc18d73
Prefer mp4 extension for video attachments (whatsapp) (#1971)
resolves #1967
2023-03-09 21:49:38 +01:00
Joseph Mansy
ac681687f8
Handle messages with link preview not being relayed (whatsapp) (#1970)
Resolves #1840
2023-03-09 21:46:57 +01:00
dependabot[bot]
57ce19150f
Bump golang.org/x/net from 0.5.0 to 0.7.0 (#2003)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.5.0 to 0.7.0.
- [Release notes](https://github.com/golang/net/releases)
- [Commits](https://github.com/golang/net/compare/v0.5.0...v0.7.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-03-09 21:39:06 +01:00
Wim
24f6747516
Bump version 2023-01-29 18:57:41 +01:00
Wim
6dafebc7cc
Release v1.26.0 (#1957) 2023-01-29 18:26:52 +01:00
Wim
d23f2560d5
Make the cgo lottie a build tag (-tag cgolottie) (#1955)
This should fix #1906 as we don't have any cgo dependencies anymore by
default.
2023-01-29 00:34:27 +01:00
Wim
880586bac4
Update dependencies (#1951) 2023-01-28 22:57:53 +01:00
Wim
eac2a8c8dc
Remove mattermost 5 support (#1936) 2023-01-28 21:57:06 +01:00
ilmaisin
4cf313c4c6
Fix the "Someone" nickname problem (whatsapp) (#1931)
* Apply patch to work around the "Someone" nickname problem

* Code cleanup (whatsapp)

* Cleanup

* Code cleanup (whatsapp)

* Re-examine if sender exists

* Cleanup (whatsapp)
2022-12-14 23:47:30 +01:00
ilmaisin
91353d0a4d
Handle incoming document captions from whatsapp (#1935) 2022-12-14 23:20:00 +01:00
ilmaisin
0a6d64ab48
Improve attachment handling (whatsapp) (#1928)
* Allow captions for document attachments, too

* Handle audio messages

* Improve attachment handling
2022-11-27 00:55:37 +01:00
Wim
4fd0a76727
Update dependencies (#1929) 2022-11-27 00:42:16 +01:00
Sebastian P
6da9d567dc
Implement sending of EventJoinLeave both to and from Mumble (#1915)
* mumble: Implement sending of EventJoinLeave both to and from Mumble (Closes #1435)

* mumble: Break handleUserChange into two functions
2022-11-27 00:17:11 +01:00
Kufat
6d5a3dff22
Allow substitution of bot's nick in RunCommands (irc) (#1890)
* Allow substitution of bot's nick in RunCommands

* Tweak description of "{BOTNICK}"

Made the description of "{BOTNICK}" consistent with that of other keywords
2022-11-27 00:01:10 +01:00
Jair Sanchez
3ad5deaff1
Fix empty messages on IRC (#1897) 2022-11-26 23:53:48 +01:00
Alexander
9bbdf70e69
Fix telegram attachment comment formatting and escaping (#1920) 2022-11-26 23:50:46 +01:00
Lucki
0c83946983
Add Matrix username spoofing (#1875)
* Matrix username spoofing

* Add config sample
2022-09-06 00:46:52 +02:00
Wim
fda05f2262
Update dependencies and fix whatsmeow API changes (#1887)
* Update dependencies

* Fix whatsmau API changes
2022-09-05 21:00:54 +02:00
Buckaroo Banzai
7abf1a5884
Add the beerchat project (a minetest mod) (#1877) 2022-09-05 19:32:30 +02:00
Alexander
365acc36ea
Fix error messages in telegram and slack bridges (#1862)
* Fix message html entities escaping when sending to Telegram

* Fix error messages in telegram and slack bridges

Co-authored-by: Wim <wim@42.be>
2022-09-05 19:31:45 +02:00
Glandos
0482cd191d
Rename freenode to libera in simple config (#1870)
* freenode to libera

The freenode network doesn't really work anymore

* Missing replacement for Libera
2022-08-13 18:51:21 +02:00
Wim
6a3fc71397
Update dependencies and go1.18 (#1873)
* Update dependencies and go1.18

* Exclude unnecessary linters and update build to go1.18
2022-08-13 16:14:26 +02:00
Alexander
3c4192ebf6
Fix message html entities escaping when sending to Telegram (#1855) 2022-07-07 00:47:50 +02:00
Wim
e450e1c447
Bump version 2022-06-25 01:11:49 +02:00
Wim
20f841c513
Release v1.25.2 (#1853) 2022-06-25 01:00:40 +02:00
Wim
d07a3e09c9
Support mattermost v7 (#1852)
Mattermost api (almost) didn't change between v6.7.x and v7.0
Everything should just work
2022-06-25 00:57:17 +02:00
Wim
4649876956
Update dependencies (#1851) 2022-06-25 00:36:16 +02:00
Sam W
5604d140e3
Ignore events from other guilds, add nosendjoinpart support (discord) (#1846)
* discord: add nosendjoinpart support

This allows the discord bridge to be configured with `nosendjoinpart`,
preventing discord-originating join/part messages from being send to
other bridged platforms.

* discord: Ignore incoming events for other guilds

Ignore all incoming discord events originating from Guild IDs other than
the one we have configured.
This is necessary because discord bots receive events for *all* discord
guilds that they are present in.

Fixes #1612
2022-06-24 23:50:48 +02:00
Wim
8751fb4bb1
Update dependencies (#1841) 2022-06-11 23:07:42 +02:00
Wim
3819062574
Bump version 2022-06-04 15:09:13 +02:00
Wim
051e6e76e9
Release v1.25.1 (#1832) 2022-05-09 23:28:02 +02:00
Wim
1e55dd47f2
Update dependencies (#1831) 2022-05-09 23:00:23 +02:00
Andy
700b95546b
Improve Slack attachments formatting (slack) (#1807)
* Improve Slack attachments formatting (slack)

* Add TitleLink
* Add Footer

* Fix linter issues
2022-05-09 22:56:19 +02:00
Wim
2fa96ec0ed
Add KeepQuotedReply option for matrix to fix regression (#1823)
Matrix quotes replies and as of matterbridge 1.24.0 we strip those as this causes
issues with bridges support threading and have PreserveThreading enabled.

Introduced via 9a8ce9b17e

But if you for example use mattermost or discord with webhooks you'll need to enable
this if you want something that looks like a reply from matrix.
See issues:
- https://github.com/42wim/matterbridge/issues/1819
- https://github.com/42wim/matterbridge/issues/1780
2022-05-06 23:32:25 +02:00
Wim
81e6f75aa4
Update dependencies (#1822) 2022-05-02 00:10:54 +02:00
Wim
888c8b9a84
Add space between filename and URL (mattermost). Fixes #1820 2022-05-01 23:28:51 +02:00
Wim
e775a8a22e
Revert "Clear existing IRC event handlers before connecting new ones (#1795)"
This reverts commit f044b948e257814e8e1f70d4b66821bfd9c2ff06.

Fixes #1815
2022-05-01 22:28:42 +02:00
Alexander
99fbd9cae6
Fix telegram message deletion request (#1818) 2022-05-01 22:00:50 +02:00
Wim
67adad3e08
Update dependencies (#1813) 2022-04-25 23:50:10 +02:00
Wim
2fca3c7563
Add CGO_ENABLED info also to whatsappmulti build 2022-04-24 16:52:25 +02:00
Wim
c3573f1a46
Add CGO_ENABLED info to README 2022-04-24 16:51:16 +02:00
Daniil Suvorov
ee932a9f8e
Fix UploadMessagesPhoto for vk community chat (vk) (#1812) 2022-04-22 23:37:09 +02:00
ValdikSS
ce18c948e6
Do not apply any markup to URL entities (telegram) (#1808)
handleEntities code uses simple modification offset which does not
allow to detect whether the offset is placed before or after
the element in already modified string.
This works fine is most cases as Telegram server always sort the
elements by offset, in ascending order.
However, this is not the case when the modification, for example bold
text, is applied to the URL. In this case, the offset of URL and
bold entity is equal, which raises the issue.

This commit introduces additional hack for this case, stripping
any entities which intersect with URL.
2022-04-22 01:00:57 +02:00
Wim
7bc93c5506
Do not modify .webm files (telegram). Fixes #17** (#1802) 2022-04-17 13:35:11 +02:00
Wim
d7cad3b404
Update matterbridge/gomatrix. Fixes #1772 (#1803) 2022-04-12 00:59:30 +02:00
Wim
7740a362c9
Fix build command for latest stable 2022-04-12 00:39:06 +02:00
Wim
281ef53e7d
Update dependencies (#1800) 2022-04-12 00:30:21 +02:00
Bryan Davis
f044b948e2
Clear existing IRC event handlers before connecting new ones (#1795)
Clear all existing IRC event handler registrations before registering
new handlers in case we are connecting via a BNC and are seeing
a reconnect.

Fixes #1564
2022-04-07 23:00:17 +02:00
Wim
32474a5f4d
Bump version 2022-04-07 22:51:22 +02:00
Wim
26596acf80
Release v1.25.0 (#1793) 2022-04-04 00:44:43 +02:00
Wim
e63870a631
Add whatsapp deprecation warning (#1792) 2022-04-04 00:31:18 +02:00
Wim
ce782ff6fb
Change discord non-native threading behaviour (discord) (#1791)
Sorta regression introduced by 9a8ce9b17e560433731eb5efa3cee7ced0b93605
which changes the way we get replies of matrix.

This causes issues like https://github.com/42wim/matterbridge/issues/1780
We "fix" this by mimicking the old behaviour when "PreserveThreading" is
disabled.
2022-04-04 00:19:31 +02:00
Wim
c6716e030c
Update dependencies (#1784) 2022-04-01 00:23:19 +02:00
Alexander
4ab72acec6
Ignore sending file with comment, if comment contains IgnoreMessages value (#1783)
* Ignore sending file with comment, if comment contains message to ignore

* Fix linter issue
2022-03-31 23:50:38 +02:00
Alexander
30aae8e257
Multiple media in one message (telegram) (#1779)
* Send multiple images/video/documents as media group

* Fix media caption quotting

* Fix errors handling

* Refactor parent id detection

* Try to reduce cognitive complexity of code

* Remove unused conditional
2022-03-30 22:23:52 +02:00
Alexander
d7b7ff7bb4
Preserve threading for messages with files (slack) (#1781)
* Preserve threading for slack messages with files

* Update bridge/slack/slack.go

Co-authored-by: Wim <wim@42.be>
2022-03-30 22:22:37 +02:00
Alexander
6fe0cff342
Use slack real name as user name (slack) (#1775)
* Use slack real name as user name

* Change slack option UseRealName to UseFullName
2022-03-26 20:52:24 +01:00
Wim
5f75f9886d
Update lrstanley/girc dep (#1773) 2022-03-25 22:01:02 +01:00
Alexander
5d9604cd15
Preserve threading from telegram replies (telegram) (#1776)
* Preserve threading from telegram replies

* Add fallback for unthreaded telegram message

* Fix linter issue
2022-03-25 21:58:52 +01:00
Alexander
cc36ebf1c9
Add UseFullName option (telegram) (#1777) 2022-03-25 21:42:28 +01:00
Tobias Niepel
e6adecfd81
Add Dockerfile_whatsappmulti for building with WhatsApp Multi-Device support (Whatsmeow) (#1774)
Co-authored-by: Tobias Niepel <tobias.niepel@obi.de>
2022-03-22 10:05:32 +01:00
Wim
5c8f224e3b
Update README 2022-03-20 17:04:33 +01:00
Wim
952221d3b9 Fix linting (whatsapp) 2022-03-20 14:57:48 +01:00
Wim
496d5b4ec7 Add whatsappmulti buildflag for whatsapp with multidevice support (whatsapp) 2022-03-20 14:57:48 +01:00
Wim
2623a412c4 Update vendor (whatsapp) 2022-03-20 14:57:48 +01:00
Wim
d64eed49bc Fix linting (whatsapp) 2022-03-20 14:57:48 +01:00
Wim
fffa29c2f3 Fix channel in video/audio/image/document handling (whatsapp) 2022-03-20 14:57:48 +01:00
Wim
4da1444ffc Check for Conversation on receiving messages (whatsapp) 2022-03-20 14:57:48 +01:00
Wim
21c4e56d16 Use Conversation instead of ExtendedTextMessage (whatsapp) 2022-03-20 14:57:48 +01:00
Wim
5356b3856a Update vendor (whatsapp) 2022-03-20 14:57:48 +01:00
Wim
320c996a21 Refactor login logic (whatsapp) 2022-03-20 14:57:48 +01:00
Wim
69c74be7bb Add busy_timeout which fixes SQLITE_BUSY errors (whatsapp) 2022-03-20 14:57:48 +01:00
Wim
aefa70891c Update vendor (whatsapp) 2022-03-20 14:57:48 +01:00
Wim
1b9877fda4 Fetch avatars synchronous (whatsapp) 2022-03-20 14:57:48 +01:00
Wim
0205a67309 Refactor JoinChannel (whatsapp) 2022-03-20 14:57:48 +01:00
Wim
e3cafeaf92 Add dependencies/vendor (whatsapp) 2022-03-20 14:57:48 +01:00
Wim
e7b193788a Rewrite whatsapp bridge to use whatsmeow 2022-03-20 14:57:48 +01:00
Wim
17da95b094
Remove go replace by fork (matrix) (#1771) 2022-03-20 01:43:26 +01:00
Wim
c5e49eec96
Bump version 2022-03-19 23:47:50 +01:00
Wim
24bc0f127b
Release v1.24.1 (#1768) 2022-03-19 23:28:15 +01:00
Wim
f0f801402d
Refactor utf-8 conversion (irc) (#1767) 2022-03-19 23:14:56 +01:00
Sebastian P
663850a2b8
Implement a workaround to signal Opus support (mumble) (#1764)
* Mumble: Implement a workaround to signal Opus support without pulling in the CGO gopus dependency.

* mumble: lowercase error messages

* mumble: Add link to #1750 in bridge/mumble/codec.go
2022-03-19 21:32:00 +01:00
ValdikSS
c51753cab1
Fix for complex-formatted Telegram text (#1765)
* Telegram: handle entities before everything

* Telegram: use runes for text entities

* Telegram: use proper offset and runes for links

* Telegram: put newline after backticks for pre

* Telegram: use utf16 for entity processing
2022-03-19 11:34:46 +01:00
Wim
b3be2e208c
Update dependencies and vendor (#1761) 2022-03-12 19:41:07 +01:00
Wim
c30e90ff3f
Fix panic in irc. Closes #1751 (#1760) 2022-03-12 17:33:39 +01:00
Wim
e4c0ca0f48
Switch to discordgo upstream again (#1759)
* Switch to upstream discordgo again

* Fix discord api changes
2022-03-12 17:06:39 +01:00
ValdikSS
9c203327c0
Fix Telegram channel title in forwards (#1753)
Forward from channels requires different handling than forward from the regular users.
This patch fixes the issue: it prints channel title instead of "forwarded from unknown".
2022-03-12 00:20:39 +01:00
Jan Martin Reckel
ccb5b1d075
Fix Telegram Problem (unforwarded formatting and skipping of linebreaks) (#1749)
* Change bridge/telegram/handlers.go

Comment out the removing of empty lines
add support for bold, italic and striked telegram messages

* Implement Telegram MessageEntities correctly

* Apply gofmt

Co-authored-by: Jan Martin Reckel <jan-martin.reckel@s2017.tu-chemnitz.de>
Co-authored-by: Wim <wim@42.be>
2022-03-12 00:19:02 +01:00
jan Anja
0dbbd0414c
Create inmessage-logger.tengo (#1688) (#1747) 2022-03-11 23:31:45 +01:00
jan Anja
e7b3ebf98a
Add OpenRC service file (#1746) 2022-02-20 22:24:42 +01:00
Wim
5bc18fb780
Remove dependabot to fix fork spamming
See https://github.com/dependabot/dependabot-core/issues/2198
2022-02-08 00:13:09 +01:00
Wim
df30366072
Bump version 2022-02-07 23:48:38 +01:00
Wim
65c7ac80b5
Release v1.24.0 (#1732) 2022-02-07 23:10:56 +01:00
dependabot[bot]
dd3fb32ec7
Bump github.com/SevereCloud/vksdk/v2 from 2.13.0 to 2.13.1 (#1730)
Bumps [github.com/SevereCloud/vksdk/v2](https://github.com/SevereCloud/vksdk) from 2.13.0 to 2.13.1.
- [Release notes](https://github.com/SevereCloud/vksdk/releases)
- [Commits](https://github.com/SevereCloud/vksdk/compare/v2.13.0...v2.13.1)

---
updated-dependencies:
- dependency-name: github.com/SevereCloud/vksdk/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-02-07 20:30:09 +01:00
Wim
2a3f475ff5
Make EditSuffix option actually work (whatsapp). Fixes #1510 (#1728)
To keep it backwards compatible we keep the "(edited)" message when no
editsuffix is configured.
2022-02-06 23:56:54 +01:00
Wim
7288f71201
Make HTMLDisable work correct (matrix) (#1716) 2022-02-06 20:58:13 +01:00
Wim
9c43eff753
Add support for using ID in channel config (mattermost) (#1715) 2022-02-06 18:26:30 +01:00
Wim
c8d7fdeedc
Add UseUsername option (mattermost). Fixes #1665 (#1714) 2022-02-06 17:33:41 +01:00
Wim
c211152e23
Add more debug options for discord (#1712)
debuglevel=1 dumps every received discord event
debuglevel=2 dumps every discord event we are sending to discord (also
logs sensitive information)
2022-02-06 16:58:35 +01:00
Wim
ab75d5097e
Use own gomatrix fork again. Fixes #1382 (#1713) 2022-02-06 00:59:34 +01:00
Wim
c3644c8d3b
Add support for client certificate (irc) (#1710)
Supports https://libera.chat/guides/certfp.html
2022-02-05 21:12:03 +01:00
Wim
6438a3dba3
Add support for deleting files from slack to discord. Fixes #1705 (#1709)
We create a new event EventFileDelete which will be used to delete
specific uploaded files using the Extra["file"] in the config.Message.

We also add a new NativeID key to the FileInfo struct which will contain
the native file ID of the sending bridge.

When a new file is added to the config.Message.Extra["file"] map, now
the bridge native file ID should be added here.

When the receiving bridge receives such a message, it should keep an
internal mapping of NativeID <> bridge fileid/message id. In the case of
discord we map it to the resulted discord message ID after uploading it.

Now when a bridge deletes a file, it should send a EventFileDelete and
setting the ID to the native file ID of the bridge.

When the receiving bridge will get this event it'll look into the
NativeID <> bridge id mapping to find their internal ID and use it to
delete the specific file on their side.

For now this is implemented for slack to discord but this will be add to
other bridges where useful.
2022-02-05 14:45:54 +01:00
Wim
4b226a6a63
Add support for sender_chat (telegram) (#1677)
* Add support for sender_chat (telegram)

Fixes #1654
https://core.telegram.org/bots/api#december-7-2021

* Add debuglevel option

Add `debuglevel=1` in telegram config to increase debug
2022-02-04 16:15:19 +01:00
Ivan Zuev
4801850013
Add Telegram Bot Command /chatId (telegram) (#1703)
* feat(telegram): command to get chat id

* Gofumpt

Co-authored-by: Ivan Zuev <i-zuev@yandex-team.ru>
Co-authored-by: Wim <wim@42.be>
2022-02-03 00:20:25 +01:00
Ivan Zuev
6a7412bf2b
Increase batch size for conversation.list api method (slack) (#1700)
Co-authored-by: Ivan Zuev <i-zuev@yandex-team.ru>
2022-01-29 00:13:15 +01:00
dependabot[bot]
5a1fd7dadd
Bump github.com/SevereCloud/vksdk/v2 from 2.11.0 to 2.13.0 (#1698)
Bumps [github.com/SevereCloud/vksdk/v2](https://github.com/SevereCloud/vksdk) from 2.11.0 to 2.13.0.
- [Release notes](https://github.com/SevereCloud/vksdk/releases)
- [Commits](https://github.com/SevereCloud/vksdk/compare/v2.11.0...v2.13.0)

---
updated-dependencies:
- dependency-name: github.com/SevereCloud/vksdk/v2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-28 23:48:40 +01:00
dependabot[bot]
ac06a26809
Bump github.com/go-telegram-bot-api/telegram-bot-api/v5 (#1693)
Bumps [github.com/go-telegram-bot-api/telegram-bot-api/v5](https://github.com/go-telegram-bot-api/telegram-bot-api) from 5.5.0 to 5.5.1.
- [Release notes](https://github.com/go-telegram-bot-api/telegram-bot-api/releases)
- [Changelog](https://github.com/go-telegram-bot-api/telegram-bot-api/blob/master/docs/changelog.md)
- [Commits](https://github.com/go-telegram-bot-api/telegram-bot-api/compare/v5.5.0...v5.5.1)

---
updated-dependencies:
- dependency-name: github.com/go-telegram-bot-api/telegram-bot-api/v5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-26 00:04:58 +01:00
dependabot[bot]
61d56f26f8
Bump github.com/labstack/echo/v4 from 4.6.1 to 4.6.3 (#1685)
Bumps [github.com/labstack/echo/v4](https://github.com/labstack/echo) from 4.6.1 to 4.6.3.
- [Release notes](https://github.com/labstack/echo/releases)
- [Changelog](https://github.com/labstack/echo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/labstack/echo/compare/v4.6.1...v4.6.3)

---
updated-dependencies:
- dependency-name: github.com/labstack/echo/v4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-18 20:42:25 +01:00
dependabot[bot]
6aa05b3981
Bump github.com/spf13/viper from 1.9.0 to 1.10.1 (#1684)
Bumps [github.com/spf13/viper](https://github.com/spf13/viper) from 1.9.0 to 1.10.1.
- [Release notes](https://github.com/spf13/viper/releases)
- [Commits](https://github.com/spf13/viper/compare/v1.9.0...v1.10.1)

---
updated-dependencies:
- dependency-name: github.com/spf13/viper
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-18 20:34:42 +01:00
dependabot[bot]
aad60c882e
Bump github.com/mattermost/mattermost-server/v6 from 6.1.0 to 6.3.0 (#1686)
Bumps [github.com/mattermost/mattermost-server/v6](https://github.com/mattermost/mattermost-server) from 6.1.0 to 6.3.0.
- [Release notes](https://github.com/mattermost/mattermost-server/releases)
- [Changelog](https://github.com/mattermost/mattermost-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mattermost/mattermost-server/compare/v6.1.0...v6.3.0)

---
updated-dependencies:
- dependency-name: github.com/mattermost/mattermost-server/v6
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-18 20:24:14 +01:00
dependabot[bot]
fecca57507
Bump github.com/mattermost/mattermost-server/v5 from 5.39.0 to 5.39.3 (#1682)
Bumps [github.com/mattermost/mattermost-server/v5](https://github.com/mattermost/mattermost-server) from 5.39.0 to 5.39.3.
- [Release notes](https://github.com/mattermost/mattermost-server/releases)
- [Changelog](https://github.com/mattermost/mattermost-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mattermost/mattermost-server/compare/v5.39.0...v5.39.3)

---
updated-dependencies:
- dependency-name: github.com/mattermost/mattermost-server/v5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-01-18 20:19:12 +01:00
Wim
2bcad846c0
Add more ignore debug messages (mattermost) (#1678) 2022-01-10 22:58:53 +01:00
Wim
15ad0165fc
Log eventtype in debug (mattermost) (#1676) 2022-01-10 00:50:03 +01:00
Wim
2e8ab11978
Use current parentID if rootId is not set (mattermost) (#1675) 2022-01-10 00:37:09 +01:00
vpzomtrrfrt
9a8ce9b17e
Reply support for Matrix (#1664)
* Post replies to matrix

* Handle replies from matrix

* Include protocol in canonical ID return

* fmt
2022-01-09 23:46:59 +01:00
Daniil Suvorov
16ab4c6fed
Remove GroupID (vk) (#1668) 2022-01-09 22:50:07 +01:00
Felix
e3ee0df7ba
Add Dependabot.yml config (#1663)
* Added: Dependabot.yml config

* Updated: schedule interval

* Updated: Interval to weekly
2021-12-19 21:53:09 +01:00
Wim
8f7ab280e2
Fix codeql warnings 2021-12-19 14:39:24 +01:00
Janet Blackquill
dbedc99421
Add support for Harmony (#1656)
Harmony is a relatively new (1,5yo) chat protocol with a small community.
This introduces support for Harmony into Matterbridge, using the functionality
specifically designed for bridge bots. The implementation is a modest 200 lines
of code.
2021-12-18 22:43:29 +01:00
Wim
6cb359cb80
Fix vendored xmpp (#1661) 2021-12-12 14:11:11 +01:00
Soloam
ae2ad824a9
Add comments to messages (telegram) (#1652)
* Add's comments to message in telegram messages

This is a change to handle comments in telegram messages!

Some messages in telegram have comments added to the message! This normally is the description in images or links. This changes appends the comment to the message if available.

This should fix the issue in #1649

* [fix] discord: send comments in extras

Co-authored-by: Wim <wim@42.be>
2021-12-12 01:40:31 +01:00
Wim
02e3d7852b
Update telegram-bot-api to v5 (#1660) 2021-12-12 00:35:32 +01:00
Wim
3893a035be
Update dependencies/vendor (#1659) 2021-12-12 00:05:15 +01:00
Wim
658bdd9faa
Fix telegram/handlers.go linting (#1658) 2021-12-10 22:13:54 +01:00
Wim
e1eebcd4e0
Disable some more linters 2021-12-10 21:54:09 +01:00
Yash Rathore
062b831e88
Fix Zulip example in matterbridge.toml.sample (#1657)
Commit 11fc4c286fbcd6c1519362d2b0123ebd4ab19067 changed the example for
Zulip, in a way that was not accurate to what zulip.go expects, hence
this commit fixes the example.
2021-12-10 21:47:47 +01:00
Dan Walmsley
b275efaeff
Add support for code blocks in telegram (#1650)
* handle code blocks in telegram.

* support multi-line code blocks.

* remove import.

* handle code blocks in middle of normal text.

* support multiple code blocks in same message.
2021-12-07 21:26:28 +01:00
PeGaSuS
80d3033456
Update matterbridge.toml.sample (#1644)
Missing `{NOPINGNICK}` example on the general re-loadable settings
2021-12-02 00:49:16 +01:00
Sandro
bd0516f09a
Use Alpine stable again in Dockerfile (#1643)
* Use alpine stable again

* fix build for tgs.Dockerfile
2021-11-29 01:19:10 +01:00
Santtu Lakkala
df4d76e466
Allow binding to IP on IRC (#1640)
Add configuration option "Bind" that is passed on to girc, allowing
to choose which IP address to use on systems that have multiple ones.
2021-11-29 01:15:51 +01:00
Wim
dcbd7f8cad
Bump version 2021-11-02 23:34:42 +01:00
Wim
73ec02ab9d
Release v1.23.2 (#1631) 2021-11-02 23:22:21 +01:00
snikpic
d1f8347071
Update go-whatsapp version (#1630) 2021-11-02 21:11:42 +01:00
Wim
8601eedada
Bump version 2021-11-01 23:56:24 +01:00
Wim
9afd33cdfc
Release v1.23.1 (#1629) 2021-10-30 18:47:35 +02:00
Polynomdivision
5e1be8e558
Do not fail on no avatar data (xmpp) #1529 (#1627)
* Detect errors when working with AvatarData

* Remove not neccessary line

Co-authored-by: Wim <wim@42.be>
2021-10-30 17:50:37 +02:00
Wim
835dd2635a
Update dependencies (#1628) 2021-10-30 15:17:50 +02:00
Wim
f65b18c2f6
Remove wrapcheck linter 2021-10-30 15:12:31 +02:00
Minecraftchest1
b0e7b84f40
Add article. (#1625)
Add article at https://minecraftchest1.wordpress.com/2021/06/05/how-to-install-and-setup-matterbridge/
2021-10-25 19:05:13 +02:00
Wim
1635db93c7
Do not check cache on deleted messages (mattermost). Fixes #1555 (#1624) 2021-10-25 00:08:08 +02:00
Wim
c4fe462d11
Use a new msgID when replacing messages (xmpp). Fixes #1584 (#1623) 2021-10-24 23:15:46 +02:00
Wim
b1f403165d
Fix panic in msteams. Fixes #1588 (#1622) 2021-10-24 22:17:46 +02:00
Wim
46e4317b77
Keep the logger on a disabled bridge. Fixes #1616 (#1621) 2021-10-24 19:00:15 +02:00
Alex Vandiver
e3ffbcadd8
Add better error handling on Zulip (#1589)
* zulip: Treat unknown errors with a 10-second backoff.

An unknown error (including an unauthorized error) would fall through
with no calls to time.Sleep, resulting in hammering the server as
quickly as possible.

Add a 10-second sleep in the default error case.  The heartbeat is
left with no explicit sleep, but all other codepaths now contain one.

* version: Move version information into a separate package.

This will allow it to be accessed by other sections of the code.

* zulip: Use the matterbridge version in the user-agent.

Co-authored-by: Wim <wim@42.be>
2021-10-23 23:46:27 +02:00
Wim
b7d73077e5
Remove forbidigo linter 2021-10-23 23:25:15 +02:00
Wim
77f61ee20a
Fix gozulipbot vendor 2021-10-23 23:20:34 +02:00
Wim
8967f02fc9
Update gozulipbot dependency (#1618) 2021-10-23 23:13:07 +02:00
Wim
831ff6d0a9
Update matterclient dep. Fixes #1617 2021-10-21 15:57:34 +02:00
Wim
2199174def
Bump version 2021-10-18 23:40:20 +02:00
Wim
55f41ddaab
Release v1.23.0 (#1615) 2021-10-17 23:49:02 +02:00
Wim
21305d93bf
Push docker images also to ghcr.io 2021-10-17 22:42:00 +02:00
KingPin
4478d5d904
Update GH actions to multi arch (arm64) (#1614)
add arm64 to the docker build
add the package to ghcr.io (github container registery)
this will make it so users can run matterbridge:latest and it will work on both amd64 & arm64
2021-10-17 22:18:20 +02:00
Wim
cc6253a6b8
Tag also latest on docker builds 2021-10-17 15:48:37 +02:00
Wim
85f66853bc
Fix docker build 2021-10-17 15:37:55 +02:00
Wim
7464fd149c
Add docker builds on tags 2021-10-17 15:36:43 +02:00
Wim
86f1a8019c
Add the githash to docker builds 2021-10-17 14:28:40 +02:00
Wim
b98d56dcf6
Fix docker build 2021-10-17 14:19:26 +02:00
Wim
a3a8a5769d
Add docker build 2021-10-17 14:12:39 +02:00
Wim
4dd8bae5c9
Update dependencies (#1610)
* Update dependencies

* Update module to go 1.17
2021-10-17 00:47:22 +02:00
Wim
7ae45c42e7
Update README to use go install instead of go get 2021-10-17 00:41:08 +02:00
Wim
7551b4e7a3
Need to update to Go 1.17 because of gopackage/ddp dependency (#1611) 2021-10-17 00:35:44 +02:00
Iris Morelle
61bab22dde
Add UserName and RealName options for IRC (#1590)
This allows setting custom values for the IRC username/ident and real
name (gecos) fields at server registration time with gIRC.

Co-authored-by: Wim <wim@42.be>
2021-10-16 23:59:39 +02:00
KingPin
6dcc23ebb6
Update arm dockerfile to build 1.22.3 fixes #1602 (#1603)
Co-authored-by: Wim <wim@42.be>
2021-10-16 23:42:00 +02:00
Jonathan Walker (Keenan)
b06a574cc5
Invalidate user in cache on user change event (#1604)
Co-authored-by: Wim <wim@42.be>
2021-10-16 23:36:30 +02:00
Wim
b56f80b1b8 Add support for mattermost v6 2021-10-16 23:23:24 +02:00
Wim
20f6c05ec5 Update vendor 2021-10-16 23:23:24 +02:00
Wim
57fce93af7
Disable exhaustivestruct linter 2021-10-16 22:38:12 +02:00
Wim
110b6a1431
Build static binaries on github 2021-10-14 15:20:58 +02:00
Benau
53cafa9f3d
Convert .tgs with go libraries (and cgo) (telegram) (#1569)
This commit adds support for go/cgo tgs conversion when building with the -tags `cgo`
The default binaries are still "pure" go and uses the old way of converting.

* Move lottie_convert.py conversion code to its own file

* Add optional libtgsconverter

* Update vendor

* Apply suggestions from code review

* Update bridge/helper/libtgsconverter.go

Co-authored-by: Wim <wim@42.be>
2021-08-24 22:32:50 +02:00
Wim
d4195deb3a
Disable errorlint,gci and nlreturn 2021-08-24 21:41:15 +02:00
Wim
400ecfb79c
Update github actions to go1.17 and increase deadline (#1573) 2021-08-22 23:33:58 +02:00
powerjungle
86151da271
Remove newline character in bridge multiline messages (mumble) (#1572) 2021-08-22 23:17:37 +02:00
Wim
44f3e2557d
Update vendor (#1560) 2021-07-31 18:27:55 +02:00
tytan652
1f365c716e
Add support for anonymous connection (xmpp) (#1548) 2021-07-31 17:26:36 +02:00
Wim
9efcc41ab2
Update matterbridge/go-xmpp vendor (#1559) 2021-07-31 17:17:43 +02:00
Brian V
13bbeeaceb
Add space before file upload comment (slack) (#1554) 2021-07-27 17:52:30 +01:00
tytan652
da4dcec14d
Fix XMPP parseNick function (#1547) 2021-07-20 23:39:14 +02:00
minecraftchest1
761c0b79c5
Add link to service files wiki page (#1545)
Added Systemd section to README.md. Added Systemd to table of contents.
2021-07-20 23:36:49 +02:00
Brian V
d93ab0496f
Use correct URL for Mediaserver Setup (#1550) 2021-07-20 23:35:52 +02:00
Wim
66b6f9749d
Update .goreleaser.yml 2021-06-25 00:47:24 +02:00
Wim
17c2d1f26a
Update matterbridge.toml.sample 2021-06-25 00:18:22 +02:00
Gary Kim
a79e632cdc
Add support for separate display name (nctalk) (#1506)
Signed-off-by: Gary Kim <gary@garykim.dev>
2021-06-19 21:45:19 +02:00
Wim
f36498421b
Bump version 2021-06-16 21:17:01 +02:00
Wim
e45bbe4571
Release v1.22.3 (#1522)
* Release v1.22.3
2021-06-16 21:11:12 +02:00
Wim
fb5a84212c
Update dependencies (#1521) 2021-06-16 21:00:49 +02:00
Nathanaël
dedc1c45a1
Update Rhymen/go-whatsapp module to latest master (2b8a3e9b8aa2) (#1518) 2021-06-16 20:35:09 +02:00
Wim
6a12f9ff84
Bump version 2021-06-02 00:05:46 +02:00
Wim
641ed1873b
Release v1.22.2 (#1504) 2021-06-01 23:36:53 +02:00
Gary Kim
1d50da4b1c
Add support for message deletion (nctalk) (#1492)
* nctalk: add message deletion support

Signed-off-by: Gary Kim <gary@garykim.dev>

* nctalk: seperate out deletion and sending logic

Signed-off-by: Gary Kim <gary@garykim.dev>

* nctalk: update library to v0.2.0

Signed-off-by: Gary Kim <gary@garykim.dev>

* Rename functions to be clearer

Signed-off-by: Gary Kim <gary@garykim.dev>

* Update to go-nc-talk v0.2.1

Signed-off-by: Gary Kim <gary@garykim.dev>

* Update to go-nc-talk v0.2.2

Signed-off-by: Gary Kim <gary@garykim.dev>

* Make deletions easier to debug

Signed-off-by: Gary Kim <gary@garykim.dev>
2021-06-01 23:17:07 +02:00
Wim
c7897cca5d
Update irc references (#1499) 2021-05-30 00:38:13 +02:00
Wim
4091b6f6b4
Update vendor (#1498) 2021-05-30 00:25:30 +02:00
Gary Kim
766f35554e
Fix content body issue for redactions (matrix) (#1496)
Signed-off-by: Gary Kim <gary@garykim.dev>
2021-05-29 23:47:36 +02:00
Wim
c86137449e
Add a MessageClipped option to set your own clipped message. Closes #1359 (#1487) 2021-05-27 21:45:23 +02:00
Gary Kim
efec01a92f
Support sending file URLs (nctalk) (#1489)
* nctalk: support sending file URLs

Signed-off-by: Gary Kim <gary@garykim.dev>

* nctalk: reduce nesting

Co-authored-by: Wim <wim@42.be>
Signed-off-by: Gary Kim <gary@garykim.dev>

Co-authored-by: Wim <wim@42.be>
2021-05-27 21:44:54 +02:00
Avinash Reddy
4fcad8e04b
Make DocumentMessage handler use FileName attribute (whatsapp) (#1488)
* [whatsapp] make DocumentMessage handler use FileName attribute.

referenced: https://github.com/Rhymen/go-whatsapp/blob/master/message.go#L582

* fix lint
2021-05-26 00:06:45 +02:00
Chris Bobbe
4b4b2d790e
Delete now-unused img/slack-setup-*.png files (#1491) 2021-05-25 03:51:52 +01:00
Avinash Reddy
ec6ae343dd
Fix crash on encountering VideoMessage (whatsapp) (#1483)
* [whatsapp] fix crash on encountering VideoMessage

* Update handlers.go

* gofmt
2021-05-23 21:21:30 +02:00
Wim
b9fb361959
Rename .jpe files to .jpg Fixes #1463 (whatsapp) (#1485) 2021-05-23 00:17:55 +02:00
Avinash Reddy
a189298ab0
Handle document messages (whatsapp) (#1475)
* [Whatsapp] Add DocumentMessage handler

* Fix typo

Thanks @42wim :)

Co-authored-by: Wim <wim@42.be>

Co-authored-by: Wim <wim@42.be>
2021-05-21 22:50:47 +02:00
Jason Robinson
714a2ad730
Add MxId/Token login option for Matrix (#1438)
* Add possibility for using MxId/Token with Matrix

Makes it possible to configure a Matrix bot to use Matrix ID + Access token instead of username/password. This makes it possible to use the bot in environments where password login is disabled (for example SSO environments).

Matrix user ID's are commonly referred to as "MXID's". I thought about (ab)using "Login" here but it felt like a bad idea given it's used as "username" for the password login. None of the other configuration items felt fitting.

Closes #1429

* MxId -> MxID

* Add err != nil to matrix.NewClient
2021-05-17 00:10:13 +02:00
Funatiker
fa8b96dfa1
Add libwebp-dev to tgs.Dockerfile fixes Telegram sticker to WebP rendering (#1476) 2021-05-15 23:54:46 +02:00
Bill Mcilhargey
01955a0df8
Add additional variable for TGS conversion in sample config (#1472)
this was buried and wanted to bring it up in the config

Convert Tgs (Telegram animated sticker) images to PNG before upload.
This is useful when your bridge also contains platforms that do not support animated WebP files, like Discord.
This requires the external dependency `lottie`, which can be installed like this:
`pip install lottie cairosvg`
https://github.com/42wim/matterbridge/issues/874

https://github.com/42wim/matterbridge/pull/1173
2021-05-13 22:49:41 +02:00
Alexandre GV
ac4aee39e3
discord: Add AllowMention to restrict allowed mentions (#1462)
* Add DisablePingEveryoneHere/DisablePingRoles/DisablePingUsers keys to config

* Add basic AllowedMentions behavior to discord webhooks

* Initialize b.AllowedMentions on Discord Bridger init

* Call b.getAllowedMentions on each webhook to allow config hot reloading

* Add AllowedMentions on all Discord webhooks/messages

* Add DisablePingEveryoneHere/DisablePingRoles/DisablePingUsers to matterbridge.toml.sample

* Change 'Disable' for 'Allow' and revert logic in Discord AllowedMentions

* Update Discord AllowedMentions in matterbridge.toml.sample

* Fix typo in DisableWebPagePreview

* Replace 'AllowPingEveryoneHere' with 'AllowPingEveryone'

* Replace 3 AllowPingEveryone/Roles/Users bools with an array

* Fix typo
2021-05-13 22:39:25 +02:00
Wim
a0bca42a7a
Update vendor (#1461)
* Update vendored libs

* Fix slack api changes
2021-05-05 22:03:28 +02:00
Sandro
af543dcd05
Follow up to 536823ce5559b92a3224061b72bcda6010abea17 for the tgs.Dockerfile (#1448) 2021-05-03 23:47:36 +02:00
Wim
af77109a47 Bump version 2021-04-04 00:09:43 +02:00
Wim
b979aff270
Release v1.22.1 (#1447) 2021-04-03 23:55:06 +02:00
Paul
b293e3fa75
Adding caption to send telegram images. Fixes #1357 (#1358)
* Used tgbotapi caption option to attach caption to photos / documents

* remove "text/template/parse"

* added TGGetParseMode to clean up. Added tg upload function for video, audio and voice

* fixed varname Textout. Changed fileextension logic to avoid chaining regex

* fixed textout varname

* fixed parsemode varname

* gofmt

Co-authored-by: Wim <wim@42.be>
2021-04-03 23:15:19 +02:00
Wim
21eb37e471
Update vendor (#1446)
* Update vendor

* Use upstream emoji lib again
2021-04-03 19:16:46 +02:00
Millesimus
d3b60cc445
Add MatterBukkit and Forge / Bukkit distinction (#1444) 2021-04-02 23:40:04 +02:00
Wim
7466e1d014
Set ogg as default audiomessage when none found (whatsapp). Fixes #1427 (#1431) 2021-03-20 23:12:59 +01:00
James Lu
2a7f28606c
Declare GUILD_MEMBERS privileged intent (discord) (#1428)
Without this declared, it seems that Discord will not send any member update
events after connection, even if the privileged gateway intent is enabled for
the bot in settings. This causes nick tracking to get out of sync when people
change their nicks after the bot connects.

See: https://discord.com/developers/docs/topics/gateway#gateway-intents
2021-03-20 22:46:36 +01:00
Ben Wiederhake
0450482e6e
Make lottie_convert work on platforms without /dev/stdout (#1424)
Fixes #1423.
2021-03-20 22:42:41 +01:00
Wim
ee5d9b43b5
Update vendor (#1414) 2021-03-20 22:40:23 +01:00
powerjungle
3a8857c8c9
Add Facebook messenger api bridge URL to README.md (#1425) 2021-03-13 22:06:01 +01:00
Wim
be3dfb251d
Check rune length instead of bytes (telegram). Fixes #1409 (#1412) 2021-02-25 23:28:54 +01:00
Wim
4e11e29f70
Use go1.16 as binary builder. Remove go1.14 (#1407) 2021-02-17 21:37:14 +01:00
Alexander
763bb95cea
Fix webhooks for channels with special characters (xmpp) (#1405) 2021-02-17 21:30:06 +01:00
Qais Patankar
668e7407e6
Change workflow from go 1.16.0-rc1 to go 1.16.x (#1406) 2021-02-17 21:14:21 +01:00
Tadeo Kondrak
c147ba1da1
Handle Rocket.Chat attachments (#1395) 2021-02-15 22:34:14 +01:00
Qais Patankar
10f044c3dd
Use valid transmitter Log default (discord) (#1402)
* Use valid transmitter Log default (discord)

Using a logger created by `log.NewEntry(nil)` would crash. (matterbridge does not encounter this issue as it updates the Log field manually.)
2021-02-15 22:20:08 +01:00
Alexander
ce5140febd
Fix panic when the webhook fails (xmpp) (#1401) 2021-02-15 22:18:30 +01:00
PeGaSuS
858cdc86f5
Fix missing word in matterbridge.toml.sample (#1398)
Co-authored-by: Qais Patankar <qaisjp@gmail.com>
2021-02-11 13:11:59 +00:00
Wim
9a25297d51 Add scoop repo 2021-02-02 01:02:36 +01:00
Wim
e24f7f5151
Add go 1.16.0-rc1 to github workflow (#1386) 2021-02-02 00:05:12 +01:00
Wim
eff5f1e119 Bump version 2021-02-01 23:59:35 +01:00
Wim
afcd362cd1
Release v1.22.0 (#1385) 2021-02-01 23:44:17 +01:00
Wim
0452be0cb3
Update vendor (#1384) 2021-02-01 21:29:04 +01:00
Wim
1624f10773
Pick up all the webhooks (discord) (#1383) 2021-02-01 20:44:34 +01:00
Ivanik
8764be7461
Add vk bridge (#1372)
* Add vk bridge

* Vk bridge attachments

* Vk bridge forwarded messages

* Vk bridge sample config and code cleanup

* Vk bridge add vendor

* Vk bridge message edit

* Vk bridge: fix fetching names of other bots

* Vk bridge: code cleanup

* Vk bridge: fix shadows declaration

* Vk bridge: remove UseFileURL
2021-01-29 00:25:14 +01:00
Wim
5dd15ef8e7
Add an even more debug option (discord) (#1368)
Enable discordgo debugging with debuglevel=1 under the [discord.xxx] section, for even more debugging fun.
2021-01-23 00:09:56 +01:00
Alexander
4ac6366706
Allow the XMPP bridge to use slack compatible webhooks (xmpp) (#1364)
* Add mod_slack_webhook support to the XMPP bridge

* Replace b.webhookURL with b.GetString

* Do not return a message ID on webhook POST

* Add the XMPP webhook to the sample configuration
2021-01-21 22:50:04 +01:00
Wim
adc0912efa
Update README (#1362) 2021-01-15 23:21:48 +01:00
Wim
536823ce55
Optimize Dockerfile (#1361) 2021-01-15 22:44:01 +01:00
Peter Dave Hello
207cd24edb
Unify/sync apk index cache control in Dockerfile (#1352)
The second stage is using a single `apk` command with `--no-cache` which is better.
2021-01-14 23:48:02 +01:00
Paul
b039da1eba
Add jpe as valid image filename extension (telegram) (#1360) 2021-01-14 23:42:13 +01:00
Qais Patankar
8fcd0f3b6f
Improve Markdown in transmitter docs (discord) (#1351) 2021-01-03 18:57:06 +00:00
Wim
16fde6935c
Rename .oga audio files to .ogg (telegram) (#1349) 2021-01-02 00:55:20 +01:00
Wim
9592cff9fa Update README about unsupported steam chat 2021-01-02 00:12:10 +01:00
Wim
109148988c Bump version 2020-12-31 23:42:41 +01:00
Wim
cf13fff7d2
Release v1.21.0 (#1348) 2020-12-31 22:32:13 +01:00
Qais Patankar
a9d8ac8bc0
Refactor "msg-parent-not-found" to config.ParentIDNotFound (#1347) 2020-12-31 18:01:57 +00:00
Qais Patankar
1a4717b366
Reject cross-channel message references (discord) (#1345)
Discord message references have been designed in a way for this to
support cross-channel or even cross-guild references in the future.

This will ensure the ParentID is *not* set when the message refers to a
message that was sent in a different channel.
2020-12-31 16:21:37 +00:00
Qais Patankar
6cadf12260
Add a prefix handler for unthreaded messages (discord) (#1346) 2020-12-31 16:15:42 +00:00
Wim
19d47784bd
Add threading support with token (discord) (#1342)
Webhooks don't support the threading yet, so this is token only.
In discord you can reply on each message of a thread, but this is not possible in mattermost (so some changes added there to make sure we always answer on the rootID of the thread).

Also needs some more testing with slack.

update : It now also uses the token when replying to a thread (even if webhooks are enabled), until webhooks have support for threads.
2020-12-31 16:59:47 +01:00
Qais Patankar
b89102c5fc
Fix 'webook' typo in discord/webhook.go (#1344) 2020-12-31 16:51:49 +01:00
Wim
4f20ebead3
Update vendor for next release (#1343) 2020-12-31 14:48:12 +01:00
James Lu
a9f89dbc64
Add support for stateless bridging via draft/relaymsg (irc) (#1339)
* irc: add support for stateless bridging via draft/relaymsg

As discussed at https://github.com/42wim/matterbridge/issues/667#issuecomment-634214165

* irc: handle the draft/relaymsg tag in spoofed messages too

* Apply suggestions from code review

Co-authored-by: Wim <wim@42.be>

* Run gofmt on irc.go

* Document relaymsg in matterbridge.toml.sample

Co-authored-by: Wim <wim@42.be>
2020-12-30 18:21:32 +01:00
wschwab
58ea1e07d2
Update README.md (#1340)
added using `chmod a+x` to make the binary executable in the installation section
2020-12-29 20:42:00 +01:00
Qais Patankar
6de4c7e971
Update webhook documentation (discord) (#1335) 2020-12-17 20:27:52 +01:00
Qais Patankar
03dc51ffa2 Split Bdiscord.Send into handleEventWebhook and handleEventBotUser (discord) 2020-12-13 23:19:48 +01:00
Qais Patankar
aef2dcdfdd Move webhook specific logic to its own file (discord) 2020-12-13 23:19:48 +01:00
Qais Patankar
0494119bf4 Extract maybeGetLocalAvatar into its own function (discord) 2020-12-13 23:19:48 +01:00
Qais Patankar
0a17e21119 Remove WebhookURL support (discord) 2020-12-13 23:19:48 +01:00
Qais Patankar
52e2f926f4 Add initial transmitter implementation (discord)
This has been tested with one webhook in one channel.

Sends, edits and deletions work fine
2020-12-13 23:19:48 +01:00
Qais Patankar
611fb279bc Revert "Disable webhook editing (#1296)" (discord)
This reverts commit c23252ab53182cc6e68086f29c7137fbc27917ee.
2020-12-13 23:19:48 +01:00
Gary Kim
41b4e64be9
Update go-nc-talk (nctalk) (#1333)
Signed-off-by: Gary Kim <gary@garykim.dev>
2020-12-10 00:06:27 +01:00
Wim
0d7315249d
Update vendor (#1330) 2020-12-06 23:16:02 +01:00
Wim
4913766d58
Parse fencedcode in ParseMarkdown. Fixes #1127 (#1329) 2020-12-06 19:38:32 +01:00
Wim
92da8c7044
Mark messages as read (matrix). Fixes #1317 (#1328) 2020-12-06 17:49:35 +01:00
Wim
9dba3d5385
Update rocketchat vendor (#1327)
Contains fixes for #992 and adds more random ID
2020-12-06 17:23:37 +01:00
Wim
2d3c26a4b2
Implement ratelimiting (matrix). Fixes #1238 (#1326) 2020-12-06 17:18:25 +01:00
Wim
8eba2d3e50
Make handlers run async (irc) (#1325)
This makes the handlers run in a seperate go-routine in girc, and makes
sure that girc isn't blocked on executing PONG requests when
matterbridge takes a long time handling the incoming message.

This can happen when another bridge is in a backoff state where the
backoff time exceeds the IRC ping timeout.
2020-12-05 21:41:45 +01:00
Qais Patankar
a8d4a27de1
Remove locale from golangci misspell (#1324) 2020-12-05 15:22:23 +01:00
Qais Patankar
c42167c6f4
Refactor guild finding code (discord) (#1319) 2020-12-03 22:36:08 +01:00
Sebastian P
44d182e2f9
Add nil checks to text message handling (mumble) (#1321) 2020-12-03 22:25:33 +01:00
Wim
ad95e35687 Rename jfif to jpg (whatsapp). Fixes #1292 2020-11-29 15:37:20 +01:00
Wim
640a9995f4 Refactor handleTextMessage (whatsapp) 2020-11-29 15:37:20 +01:00
Wim
95625f6871 Refactor image downloads (whatsapp) 2020-11-29 15:37:20 +01:00
Wim
2c20f72a9c Handle audio downloads (whatsapp) 2020-11-29 15:37:20 +01:00
Wim
5ad788e768 Handle video downloads (whatsapp) 2020-11-29 15:37:20 +01:00
Wim
ed98c586c6 Add support for deleting messages (whatsapp) 2020-11-29 15:37:20 +01:00
Wim
3e865708d6 Refactor/cleanup code (whatsapp) 2020-11-29 15:37:20 +01:00
JeremyRand
c3bcbd63c0
Add UserID to RemoteNickFormat and Tengo (#1308)
* Add UserID to RemoteNickFormat and Tengo

* Use strings.ReplaceAll in gateway.modifyUsername

Fixes a warning from gocritic linter.

* Use Unicode escape sequence instead of raw ZWSP in gateway.modifyUsername

Fixes a warning from stylecheck linter.
2020-11-25 23:54:27 +01:00
Simon THOBY
29e29439ee
Show mxids in case of clashing usernames (matrix) (#1309)
Fixes #1302.
2020-11-25 23:51:23 +01:00
Wim
0c19716f44
Join on invite (irc). Fixes #1231 (#1306) 2020-11-22 22:44:15 +01:00
Wim
b24e1bafa1
Add support for irc to irc notice (irc). Fixes #754 (#1305) 2020-11-22 22:21:02 +01:00
Wim
64b899ac89
Retry until we have contacts (whatsapp). Fixes #1122 (#1304) 2020-11-22 21:42:54 +01:00
Wim
aa274e5ab7
Update discordgo fork (#1303) 2020-11-22 19:21:34 +01:00
Wim
7b3eaf3ccf Bump version 2020-11-22 18:55:21 +01:00
Wim
1a5353d768
Release v1.20.0 (#1298) 2020-11-22 17:27:33 +01:00
Wim
eff41759bc
Add extra debug to log time spent sending a message per bridge (#1299) 2020-11-22 17:20:20 +01:00
Wim
c23252ab53
Disable webhook editing (discord) (#1296)
See https://github.com/42wim/matterbridge/issues/1255 and
https://github.com/qaisjp/go-discord-irc/issues/57

Webhook edits gets ratelimited which cause other problems with
matterbridge. Disabling for now.
2020-11-22 17:18:48 +01:00
Simon THOBY
1a3c57a031
Send the display name instead of the user name (matrix) (#1282)
* matrix: send the display name (the nickname in matrix parlance) instead of the user name

There is also the option UseUserName (already in use by the discord bridge) to turn back to the old behavior.

* matrix: update displayNames on join events

* matrix: introduce a helper.go file to keep matrix.go size reasonable
2020-11-22 15:57:41 +01:00
Wim
4cc2c914e6
Update vendor (#1297) 2020-11-22 15:55:57 +01:00
Simon THOBY
cbb46293ab
Update webhook messages via new endpoint (discord)
When using the webhook, the previous method to edit a message was to
delete the old one via the classical API, and to create a new message
via the webhook. While this works, this means that editing "old" messages
lead to a mess where the chronological order is no longer respected.

This uses an hidden API explained in https://support.discord.com/hc/en-us/community/posts/360034557771
to achieve a proper edition using the webhook API.

The obvious downside of this approach is that since it is an
undocumented API for now, so there is no stability guarantee :/
2020-11-14 04:08:09 +00:00
George
765e00c949
Add NoTLS option to allow plaintext XMPP connections (#1288)
Co-authored-by: George <zhoreeq@users.noreply.github.com>
2020-11-13 23:59:05 +01:00
Simon THOBY
662359908b
Allow message edits on matrix (#1286)
based on https://github.com/Half-Shot/matrix-doc/blob/hs/1695-message-edits-proposal/proposals/1695-message-edits.md
2020-11-13 23:42:14 +01:00
Wim
0d99766686 Update slack invite 2020-10-24 21:04:24 +02:00
Wim
ae3bc3358b
Allow tengo to drop messages using msgDrop (#1272) 2020-10-21 21:57:14 +02:00
Wim
1e0b4532bd
Show deprecate warnings about old tengo settings (#1271) 2020-10-21 20:35:22 +02:00
Wim
4f8b19c686
Add PingDelay option (irc) (#1269) 2020-10-21 01:14:13 +02:00
Wim
84ab223b81 Bump version 2020-10-21 00:59:11 +02:00
Wim
2bb21262d4
Release v1.19.0 (#1268) 2020-10-20 23:34:41 +02:00
Dellle
3188a9ffe6
Add username formatting for all events (matrix) (#1233) 2020-10-20 21:22:31 +02:00
Wim
61569a8610
Add even more debug for irc (#1266) 2020-10-20 00:33:15 +02:00
Wim
075a84427f
Update vendor (#1265) 2020-10-19 23:40:00 +02:00
Gary Kim
950f2759bd
Add support for downloading files (nctalk) (#1249)
Signed-off-by: Gary Kim <gary@garykim.dev>
2020-10-19 23:16:34 +02:00
Wim
25c82ddf02
Use vendored whatsapp version (#1258) 2020-10-12 00:05:17 +02:00
Wim
2d98df6122
Update vendor (#1257) 2020-10-11 23:07:00 +02:00
Gary Kim
219a5453f9
Append a suffix if user is a guest user (nctalk) (#1250)
Signed-off-by: Gary Kim <gary@garykim.dev>
2020-10-01 22:59:35 +02:00
Sebastian P
214a6a1386
Add Mumble support (#1245) 2020-10-01 22:50:56 +02:00
Wim
e7781dc79c Bump version 2020-10-01 22:46:18 +02:00
Wim
10c4bd1ac8
Create codeql-analysis.yml 2020-10-01 22:02:05 +02:00
Dellle
a42e488e58
Add username for images from WhatsApp (#1232) 2020-09-26 21:32:57 +02:00
Ben Wiederhake
06eb89b05b
Matrix: Permit uploading files of other mimetypes (#1237)
This includes at least c-source-files, cpp-source-files,
markdown-files, Rust-files, and plaintext files.
We already allow uploading arbitrary executables. (And javascript-files,
coincidentally.) Not permitting these other text files would be highly unexpected.
2020-09-26 21:28:24 +02:00
Wim
91c58ec027 Bump version 2020-09-05 00:10:30 +02:00
Wim
8b26e42a3a
Release v1.18.3 (#1229) 2020-09-05 00:02:37 +02:00
Wim
acca011f15 Use alpine stable for docker 2020-09-04 23:47:03 +02:00
Wim
2f59abdda7
Update vendor (#1228) 2020-09-04 23:29:13 +02:00
Wim
17747a5c88 Remove outdated link. Closes #1224 2020-09-04 22:52:55 +02:00
Wim
cec63546ff
Check location of avatarURL (zulip). Fixes #1214 (#1227) 2020-09-04 22:50:57 +02:00
Gary Kim
75f67d2de4
Update nc-talk dependency (#1226) 2020-09-04 22:14:36 +02:00
Tilo Spannagel
712385ffd5
Format rich object strings (nctalk) (#1222)
Signed-off-by: Tilo Spannagel <development@tilosp.de>
2020-08-31 01:49:43 +02:00
Tilo Spannagel
ad90cf09fe
Update nc-talk to version 0.1.2 (#1220)
Signed-off-by: Tilo Spannagel <development@tilosp.de>
2020-08-30 15:19:51 +02:00
Tilo Spannagel
f9928c9e25
Switch to upstream gomatrix (#1219)
Signed-off-by: Tilo Spannagel <development@tilosp.de>
2020-08-30 14:01:52 +02:00
Gary Kim
a0741d99b8
Add TLSConfig to nctalk (#1195)
Signed-off-by: Gary Kim <gary@garykim.dev>
2020-08-30 13:49:26 +02:00
NikkyAI
c63f08c811
Sent loopback messages to other websockets as well (api) (#1216) 2020-08-27 22:28:03 +02:00
escoand
58b6c4d277
Handle broadcasts as groups in Whatsapp (#1213)
The current way to get the correct JID of a WhatsApp group is to dump all JIDs to the log and grab the right one. This is working for for groups fine but not for broadcast, as they are not print out.

According to https://www.npmjs.com/package/@noamalffasy/js-whatsapp we have these possibilities:
* Chats: `[country code][phone number]@s.whatsapp.net`
* Groups: `[country code][phone number of creator]-[timestamp of group creation]@g.us`
* Broadcast Channels: `[timestamp of group creation]@broadcast`

But the bridge does currently interprets (and prints) the only second option.
2020-08-26 22:27:50 +02:00
NikkyAI
27c02549c8
Replace gorilla with melody for websocket API (#1205) 2020-08-26 22:27:00 +02:00
Wim
88d371c71c
Release v1.18.2 (#1212) 2020-08-25 13:21:53 +02:00
Sandro
b339524613
Add Dockerimage for tgs conversion (#1211)
* Add Dockerfile with tgs to png conversion support

* Add .dockerignore to keep cache busts while testing low
2020-08-25 13:15:24 +02:00
Wim
d5feda5c8a
Fix error loop (zulip) (#1210)
Fixes #1047
2020-08-25 00:12:13 +02:00
Wim
2f506425c2
Update whatsapp vendor and fix a panic (#1209)
* Fix another whatsapp panic

* Update whatsapp vendor
2020-08-24 23:35:08 +02:00
Wim
e8167ee3d7 Add link to nctalk in README 2020-08-24 00:50:32 +02:00
Wim
2f5e211065 Bump version 2020-08-24 00:40:26 +02:00
Wim
39f4fb3446
Release v1.18.1 (#1207) 2020-08-24 00:25:30 +02:00
Wim
56159b9bce
Sleep when ratelimited on joins (matrix). Fixes #1201 (#1206) 2020-08-24 00:12:30 +02:00
Ben Wiederhake
b2af76e7dc
Support Telegram animated stickers (tgs) format (#1173)
This is half a fix for #874

This patch introduces a new config flag:
- MediaConvertTgs

These need to be treated independently from the existing
MediaConvertWebPToPNG flag because Tgs→WebP results in an
*animated* WebP, and the WebP→PNG converter can't handle
animated WebP files yet.

Furthermore, some platforms (like discord) don't even support
animated WebP files, so the user may want to fall back to
static PNGs (not APNGs).

The final reason why this is only half a fix is that this
introduces an external dependency, namely lottie, to be
installed like this:

$ pip3 install lottie cairosvg

This patch works by writing the tgs to a temporary file in /tmp,
calling lottie to convert it (this conversion may take several seconds!),
and then deleting the temporary file.
The temporary file is absolutely necessary, as lottie refuses to
work on non-seekable files.
If anyone comes up with a reasonable use case where /tmp is
unavailable, I can add yet another config option for that, if desired.

Telegram will bail out if the option is configured but lottie isn't found.
2020-08-23 22:34:28 +02:00
Wim
491fe35397 Update workflow builds to go 1.15 2020-08-21 00:29:59 +02:00
Wim
b451285af7
Sync with upstream gozulipbot fixes (#1202) 2020-08-21 00:14:33 +02:00
Dellle
63a1847cdc
Remove HTML formatting for push messages (#1188) (#1189)
When there is a valid HTML formatting then remove this in the cleartext
field of the matrix client. This leads to nicer push messages on
smartphone apps.

Fix #1188
2020-08-20 22:41:53 +02:00
Wim
4e50fd8649
Use mattermost v5 module (#1192) 2020-08-10 00:29:54 +02:00
Wim
dfdffa0027 Add EnableAllEvents
Add option to have all events send to the messagechan
2020-08-09 21:46:45 +02:00
Wim
ebd2073144
Handle panic in whatsapp. Fixes #1180 (#1184) 2020-07-30 23:55:31 +02:00
Wim
1e94b716fb
Fix typo in documentation (#1183) 2020-07-30 23:46:03 +02:00
Wim
2a41abb3d1 Update linter config 2020-07-26 15:05:52 +02:00
Gary Kim
2d2bebe976
Fix Nextcloud Talk connection failure (#1179)
Fix #1177

Signed-off-by: Gary Kim <gary@garykim.dev>
2020-07-26 14:51:07 +02:00
Wim
e1629994bd Bump version 2020-07-24 21:31:28 +02:00
Wim
e3d8fe4fd8
Release v1.18.0 (#1176) 2020-07-24 21:00:57 +02:00
Wim
23d8742f0d
Update dependencies for 1.18.0 release (#1175) 2020-07-18 17:27:41 +02:00
Wim
3b6a8be07b
Update README.md 2020-07-18 17:26:19 +02:00
Gary Kim
71a5b72aff
Add Nextcloud Talk support (#1167)
Signed-off-by: Gary Kim <gary@garykim.dev>
2020-07-18 16:08:25 +02:00
z3bra
213bf349c3
Add an option to log into a file rather than stdout (#1168)
Use Logfile option in the `[general]` section
2020-07-18 15:46:19 +02:00
Andrey Groshev
a94fe55886
Fix MarkdownV2 support in Telegram (#1169) 2020-07-12 22:40:22 +02:00
haykam821
9b22f16497
Add websocket to API (#970)
Co-authored-by: Qais Patankar <qaisjp@gmail.com>
2020-07-12 21:13:28 +02:00
Wim
2977a5957e Update README 2020-06-28 19:02:28 +02:00
Wim
f70d1c897a Set fetch-depth to 0 to fetch all tags 2020-06-28 18:30:18 +02:00
Wim
a4a3525265 Set fetch-depth correct and use vendor when building in workflow 2020-06-28 18:23:46 +02:00
Wim
a6dd8446e4 Increase fetch depth in workflow 2020-06-28 18:17:26 +02:00
Wim
7bf9e1cfb3 Fix space in workflow 2020-06-28 18:13:50 +02:00
Wim
f291832a77 Upload artifacts on commit 2020-06-28 18:11:02 +02:00
Nathanaël
1fee323247
Reload user information when a new contact is detected (whatsapp) (#1160)
Before returning an empty string, we refresh the WhatsApp contacts and if we found the one we wanted, we can return a real name. Fixes #796
2020-06-25 00:35:49 +02:00
Qais Patankar
a41accd033
Add sane RemoteNickFormat default for API (#1157) 2020-06-25 00:25:10 +02:00
James Lu
37f7caf7f3
Skip gIRC built-in rate limiting (irc) (#1164)
By default, gIRC rate limits all outgoing messages. 
Since matterbridge already implements message throttling, this is extra layer of throttling is not necessary.
2020-06-24 23:57:37 +02:00
TheHolyRoger
5847f7758c
Only colour IRC nicks if there is one. (#1161) 2020-06-24 23:48:54 +02:00
Wim
bce736993e Remove travis as it isn't working anymore 2020-06-24 23:45:45 +02:00
Wim
5636992446 Increase fetch-depth in workflow 2020-06-24 23:37:02 +02:00
Wim
f996a2b7ae More linting fixes 2020-06-24 23:28:41 +02:00
Wim
587de96ab3 Update golangci-lint config 2020-06-24 23:21:15 +02:00
Wim
80eb1cd202 Fix duplicate name in workflow 2020-06-24 22:37:46 +02:00
Wim
bbf594c815 Use github workflows 2020-06-24 22:36:47 +02:00
Sandro
2f0f2ee40d
Combine runs to one layer (#1151) 2020-05-28 00:31:32 +02:00
Wim
96022d3aaf Bump version 2020-05-24 22:44:10 +02:00
Wim
8eb5e3cbf8
Release v1.17.5 (#1150) 2020-05-24 22:35:56 +02:00
xnaas
ddc2625934
Update Dockerfile so inotify works (#1148)
This change would be required for the Docker image to actually read `RELOADABLE` config options from the `matterbridge.toml`.

This edit would require https://github.com/42wim/matterbridge/wiki/Deploy:-Docker to be updated as well to mention that mounting would have to change to mounting a ***directory*** not a file. inotify inside Docker cannot read directly mounted files, only directories, for whatever reason.

This will preserve setups that were configured to run the old way without breaking them and new configs can be setup "correctly" without issue.
2020-05-24 22:01:52 +02:00
Wim
7f7ca697a0
Ignore non-user messages (msteams). Fixes #1141 (#1149)
Ignore these messages for now, also add a extra
debug option for msteams so we can dump the whole
message.
2020-05-24 15:49:24 +02:00
Alexander
900375679b
Prevent re-requesting avatar data (xmpp) (#1117)
Prevent asking the server again and again for a
user's avatar if the server does not respond to
our initial request.
2020-05-24 14:07:36 +02:00
Wim
9440b9e313
Increase debug logging with function,file and linenumber (#1147)
Show the function name,file and linenumber like this
[0000]  INFO main:         [setupLogger:matterbridge.go:100] Enabling debug logging.
[0000]  INFO main:         [main:matterbridge.go:46] Running version 1.17.5-dev

Only enable this for debug as this adds some overhead.
2020-05-24 13:58:15 +02:00
Wim
393f9e998b
Update dependencies / vendor (#1146) 2020-05-24 00:06:21 +02:00
Wim
ba0bfe70a8
Add StripMarkdown option (irc). (#1145)
Enable `StripMarkdown` to strip markdown for irc.
2020-05-23 21:46:15 +02:00
Wim
3c4a3e3f75
Implement xep-0245 (xmpp). Closes #1137 (#1144) 2020-05-23 20:51:04 +02:00
Wim
274fb09ed4
Fix forward from hidden users (telegram). Closes #1131 (#1143)
Use ForwardDate to check if a message is forwarded.
If we have a nil ForwardedFrom then make this an unknown user.
2020-05-23 19:15:26 +02:00
Wim
d44598a900
Add an option to disable sending HTML to matrix. Fixes #1022 (#1135) 2020-05-14 00:37:41 +02:00
Wim
c9cfa59f54
Do not use webhooks when token is configured (slack) (fixes #1123) (#1134) 2020-05-14 00:27:34 +02:00
Tiago Epifânio
7062234331
Avoid creating invalid url when the user doesn't have an avatar (matrix) (#1130) 2020-05-11 00:21:56 +02:00
Qais Patankar
9754569525
Fix webhook EventUserAction messages being skipped (discord) (#1133)
Fixes #1132
2020-05-11 00:20:35 +02:00
Qais Patankar
52a071e34d
Fix #1049: missing space before embeds (discord) (#1124) 2020-05-07 00:19:48 +02:00
Qais Patankar
2d8f749e36
Fix #1120: replaceAction "_" crash (discord) (#1121) 2020-04-25 14:22:22 +02:00
Wim
a18cb74f03 Bump version 2020-04-22 00:00:08 +02:00
Wim
6c442e239d
Release v1.17.4 (#1112) 2020-04-21 23:53:51 +02:00
Wim
eaf92fca4d
Add an ID cache (discord). Fixes #1106 (#1111)
When a webhook "edits" a message, it does this by deleting the message
and creating a new one with the new content.

On creation of this new message, we'll get another ID then already is
know by the gateway in its id cache. So we add it in our own cache and
replace it whenever we want to edit/delete it again.
2020-04-21 23:35:46 +02:00
Wim
06b7bad714
Lowercase account names. Fixes #1108 (#1110) 2020-04-21 20:42:11 +02:00
Wim
19eec2ed03
Update Rhymen/go-whatsapp. Fixes #1107 (#1109) 2020-04-21 19:55:47 +02:00
Wim
d99c54343a
Remove panics and retry polling on failure (msteams). Fixes #1104 (#1105) 2020-04-21 19:29:24 +02:00
Wim
308a110000 Bump version 2020-04-19 17:23:17 +02:00
Wim
4f406b2ce6
Release v1.17.3 (#1103) 2020-04-19 17:13:58 +02:00
Wim
e564c555d7
Clip too long messages on 3000 length (slack). Fixes #1081 (#1102) 2020-04-19 17:00:11 +02:00
Wim
f7ec9af9e8
Add extra space before colon in attachments (irc). Fixes #1089 (#1101) 2020-04-19 16:45:53 +02:00
Wim
4d93a774ce
Ignore non-critical errors (whatsapp). Fixes #1094 (#1100) 2020-04-19 13:45:35 +02:00
Wim
2595dd30bf
Update matterbridge/go-xmpp. Fixes #1097 (#1099) 2020-04-19 01:06:44 +02:00
Wim
9190365289
Add JoinDelay option (irc). Fixes #1084 (#1098) 2020-04-19 00:46:35 +02:00
Wim
57794b3b9f
Prevent image/message looping (slack). Fixes #1088 (#1096)
Also check for our matterbridge ID in Blocks set in SubMessages.
2020-04-18 22:30:49 +02:00
ldruschk
3c36f651be
Fix the behavior of ShowTopicChange and SyncTopic (#1086)
Currently, the "topic_change" events are ignored if both, 
ShowTopicChange and SyncTopic are set, and forwarded otherwise. 

This pull requests changes the behavior such that the events are 
only forwarded if one of those two config options is set to true 
and ignored otherwise.
2020-04-18 22:05:27 +02:00
ldruschk
8e6ddadba2
Relay Joins/Topic changes in RocketChat bridge (#1085)
This pull request properly sets the events EventJoinLeave and EventTopicChange for messages from the RocketChat bridge and drops messages which are neither one of those events nor plain messages.
2020-04-18 22:00:35 +02:00
Wim
8a87a71927
Update matterbridge/go-xmpp to add PEP-0030 support (#1095) 2020-04-18 20:58:55 +02:00
Qais Patankar
0047e6f523
Sort README bridge and library links (#1093) 2020-04-18 18:12:16 +02:00
Alexander
7183095a28
Implement User Avatar spoofing of XMPP users (#1090)
* Implement User Avatar spoofing of XMPP users
2020-04-16 22:16:25 +02:00
Wim
13c90893c7
Update matterbridge/Rocket.Chat.Go.SDK (#1087) 2020-04-16 21:48:53 +02:00
Wim
976fbcd07f Bump version 2020-04-09 23:03:45 +02:00
Wim
d97b077e85
Release v1.17.2 (#1080) 2020-04-09 22:54:51 +02:00
Wim
8950575bfb
Update Rhymen/go-whatsapp vendor and whatsapp version (#1078) 2020-04-09 22:30:08 +02:00
Jerry Heiselman
11fc4c286f
Clarify terminology used in mapping group chat IDs to channels in config (#1079)
* Clarify embedded docs for channel specification

Should help with #1072
2020-04-08 23:52:38 +02:00
Wim
8d08e348a9
Reset start timestamp on reconnect (whatsapp). Fixes #1059 (#1064) 2020-03-31 23:26:53 +02:00
Wim
a18807f19e
Update matterbridge/go-xmpp to add xmpp avatar support (#1070) 2020-03-29 17:35:40 +02:00
Wim
29f658fd3c Use DebugWriter after upstream changes (xmpp) 2020-03-29 15:03:24 +02:00
Wim
a30bb8fed0 Sync matterbridge/go-xmpp with upstream 2020-03-29 15:03:24 +02:00
Wim
092ca1cd67
Update vendor slack-go/slack (#1068) 2020-03-28 23:50:47 +01:00
Wim
0df2539641
Use upstream yaegashi/msgraph.go/msauth (msteams) (#1067) 2020-03-28 23:44:49 +01:00
Wim
0f2d8a599c
Update vendor d5/tengo (#1066) 2020-03-28 23:41:35 +01:00
Wim
54b3143a1d Bump version 2020-03-28 00:29:41 +01:00
Wim
148f7d2a91
Release v1.17.1 (#1063) 2020-03-28 00:18:29 +01:00
Wim
1aa662f763
Update client version whatsapp. Fixes #1061 (#1062)
See https://github.com/Rhymen/go-whatsapp/issues/305
2020-03-28 00:18:03 +01:00
Alex Wigen
0b86b88de7
Remove build dependencies from final docker image (multistage build) (#1057)
This multistage build takes the resulting image size down from 346MB to
90MB.
2020-03-22 22:55:29 +01:00
Qais Patankar
98033b1ba7
Don't transmit typing events from ourselves (slack/discord) (#1056) 2020-03-22 18:39:11 +01:00
Qais Patankar
2b7eab629d
Add support for build tags (#1054)
By default all bridges are available.

You can turn off certain bridges by providing
e.g. "nodiscord" as a build tag.

    go build -tags nomsteams,noapi
2020-03-22 18:34:14 +01:00
Wim
0e4973e15c
Exclude gateway/bridgemap from linting (#1055) 2020-03-22 14:35:48 +01:00
Qais Patankar
af0acf0dae
Strip extra info from emotes (discord) (#1052) 2020-03-22 14:16:31 +01:00
Qais Patankar
76e5fe5a87 Update vendor yaegashi/msgraph.go to v0.1.2 (2) 2020-03-22 00:02:48 +01:00
Qais Patankar
802c80f40c Update vendor yaegashi/msgraph.go to v0.1.2 (1) 2020-03-22 00:02:48 +01:00
Wim
a51c5bd905
Add more msteams docs (#1051) 2020-03-21 23:30:22 +01:00
Wim
8c68556f52 Bump version 2020-03-21 22:51:22 +01:00
Wim
cca1ea2404
Release v1.17.0 (#1050) 2020-03-21 21:33:16 +01:00
Wim
281016a501
Fix duplicate separator on empty description/url (discord). Fixes #1008 (#1035)
Make this work for all possible cases.
Add tests
2020-03-21 21:27:17 +01:00
Qais Patankar
d4acdf2f89
Use blocks not attachments (slack) (#1048)
This removes the extra space below messages, as shown in
https://user-images.githubusercontent.com/923242/77235190-a3359980-6bab-11ea-8b7b-697d730ae5c1.png
2020-03-21 21:03:12 +01:00
Qais Patankar
0951e75c85
Fix #1039: messages sent to Slack being synced back (#1046)
This is a regression from https://github.com/42wim/matterbridge/pull/581#issuecomment-562937576

Behaves the same as 95190f11bf
2020-03-21 20:12:30 +01:00
Jakub
6b017b226a
Support JSON and YAML config formats (#1045)
Signed-off-by: Jakub Sokołowski <jakub@status.im>
2020-03-18 23:20:29 +01:00
Qais Patankar
9e3bd7398c
Fix #1027: warning when handling inbound webhooks (discord) (#1044) 2020-03-18 23:12:48 +01:00
Qais Patankar
79f764c7a8 Refactor webhook permission checks 2020-03-18 23:10:47 +01:00
Qais Patankar
b5dc4353fb Fix #1040: spotty webhook permission verification 2020-03-18 23:10:47 +01:00
Qais Patankar
2fbac73c29
Ignore ConnectingEvent (slack) (#1041) 2020-03-18 23:03:20 +01:00
burner1024
6616d105d1
Add markdownv2 mode for telegram documentation, see #1032 (#1037) 2020-03-18 22:43:11 +01:00
Wim
6b4b19194e
Update vendor shazow/ssh-chat (#1029) 2020-03-08 23:55:09 +01:00
Wim
9785edd263
Remove replace directives and use own fork to make go get work again (#1028)
See https://github.com/golang/go/issues/30354
go get doesn't honor the go.mod replace options.
2020-03-08 17:08:18 +01:00
Qais Patankar
2a0bc11b68 Make some discord matterbridge.toml.sample lines less verbose 2020-03-08 14:30:54 +01:00
Qais Patankar
dd0325a88d Remove trailing newlines from matterbridge.toml.sample 2020-03-08 14:30:54 +01:00
Qais Patankar
20783c0978 Refactor matterbridge.toml.sample discord section 2020-03-08 14:30:54 +01:00
Wim
3f06a40bd5 Support code snippets from msteams 2020-03-01 22:19:33 +01:00
Wim
68f43985ad Add scopes again 2020-03-01 22:19:33 +01:00
Wim
915ca8f817 Make linter happy and cleanup (msteams) 2020-03-01 22:19:33 +01:00
Wim
a65a81610b Support threading from other bridges to msteams 2020-03-01 22:19:33 +01:00
Wim
8eb6ed5639 Support receiving attachments from msteams 2020-03-01 22:19:33 +01:00
Wim
795a8705c3 Add initial Microsoft Teams support
Documentation on https://github.com/42wim/matterbridge/wiki/MS-Teams-setup
2020-03-01 22:19:33 +01:00
Wim
3af0dc3b3a Vendor libraries needed for msteams support 2020-03-01 22:19:33 +01:00
Wim
9cf9b958a3
Do not lint gomnd (#1021) 2020-03-01 22:05:50 +01:00
Wim
3ac2ba8d5a
Update to go1.14 and golangci-lint 1.23.7 (#1020) 2020-03-01 21:50:21 +01:00
Wim
d893421c7b
Update vendor keybase/go-keybase-chat-bot (#1019) 2020-03-01 21:09:23 +01:00
Wim
250b3bb579
Use upstream slack-go/slack again (#1018) 2020-03-01 20:59:19 +01:00
Alexander Pushkov
e9edbfc051
Make Keybase link point to the team directly (#1013) 2020-02-19 21:02:44 +01:00
Wim
e343db6f72
Make avatars download work with mediaserverdownload (telegram). Fixes #920 (#1012) 2020-02-15 18:31:40 +01:00
Qais Patankar
4d57d66f85
Fix typo in feature_request.md (#1009) 2020-02-10 22:35:11 +01:00
Wim
54ed6320c2 Add support for avatars from matrix. #984 (#1007) 2020-02-10 00:06:54 +01:00
Wim
23083f3ae0
Rebase gomatrix vendor with upstream (#1006) 2020-02-09 23:49:17 +01:00
Wim
1985873494 Implement basic reconnect (whatsapp). Fixes #987 (#1003) 2020-02-09 22:11:46 +01:00
Qais Patankar
8ae5917659
Be less lossy when throttling IRC messages (#1004)
Note that msg.Text and chucking it through a chan is OK: https://play.golang.org/p/MTfT3YSsgPX
2020-02-09 22:10:18 +01:00
Qais Patankar
c91bfd08d8 Add ability to procure avatars from the destination bridge (#1000)
* remote_avatar: add UseLocalAvatar

* remote_avatar: make sure msg.Protocol is always set correctly

* remote_avatars: support msg.Account

* remote_avatar: add to matterbridge.toml.sample

* remote_avatar: clarify something
2020-02-09 22:07:26 +01:00
Qais Patankar
49110a5872
Assign automatically labels when creating issues (#1005)
* Update Bug_report.md

* Add 'label: enhancement' to feature_request.md
2020-02-09 22:03:53 +01:00
Wim
c01c8edeb8 Fix go-keybase-chat-bot api changes 2020-02-08 18:33:05 +01:00
Wim
ff8cf067b8 Update kekeybase/go-keybase-chat-bot vendor 2020-02-08 18:33:05 +01:00
Qais Patankar
1420f68050 Check only bridged channels for PermManageWebhooks (discord) (#1001)
* Check only bridged channels for PermManageWebhooks

* add note
2020-02-08 15:13:23 +01:00
Martijn Braam
c0be3e585a
Enable intra-word emphasis supression in markdown (#999)
This fixes plain links sent to Matrix being broken if they contain
underscores. Fixes issue #997
2020-02-04 13:22:05 +01:00
Wim
3049ef9151 Bump version 2020-02-02 22:40:44 +01:00
Wim
4be00bbe6b Release v1.16.5 2020-02-02 22:36:07 +01:00
Wim
9382dde098 Release v1.16.4 2020-02-02 22:22:39 +01:00
Wim
1bf46b7711
Fix duplicated messages (sshchat). Fixes #950 (#996) 2020-02-02 22:08:37 +01:00
Wim
b85bae31d9
Show file comment in webhook if normal message is empty (discord). Fixes #962 (#995) 2020-02-02 21:14:54 +01:00
Patrizio Bekerle
0898829313
Add Docker Compose configuration (#990)
* Add Docker Compose configuration

* Add docker wiki link
2020-02-02 21:14:19 +01:00
Wim
f8ad877601
Add DisableWebPagePreview option (telegram). Closes #980 (#994) 2020-02-02 18:53:04 +01:00
Wim
585d1556c1
Disable smartypants in markdown parser. Fixes #989, #983 (#993) 2020-02-02 18:35:43 +01:00
Wim
7486555875
Fail with message instead of panic. #988 (#991) 2020-02-01 15:23:50 +01:00
Humorhenker
fc30b1bacc
Add QuoteLengthLimit option (telegram) fixes #963 (#985)
* QuoteLengthLimit option added to limit max. quoted message length if QuoteLengthLimit = 0 the whole message will be quoted
2020-01-30 00:02:33 +01:00
c0ncord2
0dd19af6e8
Create outmessage-discordemoji.tengo (#979) 2020-01-30 00:00:57 +01:00
Wim
4c44515f9d
Fix channel ID problem with multiple gateways (discord). Fixes #953 (#977) 2020-01-09 23:54:04 +01:00
Wim
9d84d6dd64
Update to tengo v2 (#976) 2020-01-09 21:52:19 +01:00
Wim
0f708daf2d
Update dependencies (#975) 2020-01-09 21:02:56 +01:00
Wim
b9354de8fd Clean up go.mod and vendor 2020-01-09 18:21:10 +01:00
Guillaume Lazzara
c9d5f4c898 Add support for WhatsApp media (jpeg/png/gif) bridging (#974)
* Whatsapp image bridging

* Prevent double message in telegram when media with caption received

Co-authored-by: imShara <shara@protonmail.com>
2020-01-09 18:14:01 +01:00
c0ncord2
810c150781 move stripCustomoji logic to default Tengo script (#973)
*  move stripCustomoji logic to default Tengo script 

Removing the image ID from the message (without any possibility of recovering it later) is a loss of valuable data that prevents users from giving support to custom emoji via Tengo scripts.

* bugfix - do send colors to other irc bridges

"if we're not sending to an irc bridge we strip the IRC colors"

Co-authored-by: c0ncord <59654954+c0ncord@users.noreply.github.com>
2020-01-09 18:02:53 +01:00
Wim
31dd538c0b Add extra mimetypes to docker image. Fixes #969 2020-01-07 23:34:11 +01:00
Justin W. Flory
62e38e7c45 Add link to Ansible role for Matterbridge (#968)
This commit replaces the FOSSRIT/infrastructure link to the Matterbridge
role to a properly-defined Ansible role published in Ansible Galaxy. I
am the maintainer of the FOSSRIT/infrastructure repo and I decided to
split the Ansible role there into its own dedicated role. I figure this
might make it more accessible to others and also gives other folks a
chance to contribute. 😄

Signed-off-by: Justin W. Flory <git@jwf.io>
2020-01-01 21:38:26 +01:00
Wim
b9da28a29b Bump version 2019-12-16 00:01:00 +01:00
Wim
84bfa8a6b1 Release v1.16.3 2019-12-15 23:54:21 +01:00
Wim
1f830963f6
Return when we have only WebhookURL (mattermost). Fixes #954 (#960) 2019-12-15 23:49:17 +01:00
Wim
12d2c6fe89
Update slack vendor to fix regression (#959) 2019-12-08 21:05:02 +01:00
Wim
f43faf15f8
Update slack vendor to master (#958) 2019-12-07 22:54:36 +01:00
Wim
173a38a374 Bump version 2019-11-26 00:18:00 +01:00
Qais Patankar
1604ff15b5 Re-add binary to .gitignore (#951)
* Fix binary path and include windows
2019-11-26 00:16:40 +01:00
Wim
214fe502cd Release v1.16.2 2019-11-17 23:25:08 +01:00
Wim
aae45a8179
Upgrade linter and travis to go1.13 (#949) 2019-11-17 23:16:06 +01:00
Wim
075ca9ca47
Switch to new emoji library kyokomi/emoji (#948) 2019-11-17 23:01:03 +01:00
Wim
d4253d7a55
Update shazow/ssh-chat dependency (#947) 2019-11-17 21:42:41 +01:00
Benjamin
0917dc8766 Update markdown parsing library to github.com/gomarkdown/markdown (#944) 2019-11-17 21:18:01 +01:00
Wim
aba86855b5
Use own slack fork to fix #937 (#943) 2019-11-14 00:04:39 +01:00
Wim
ed5386c213 Add MatterAMXX link 2019-11-04 23:20:44 +01:00
Wim
455e75e92f Bump version 2019-11-01 22:32:39 +01:00
Gonçalo Ribeiro
c394de0c88 Add support for receiving attachments (keybase) (#923) 2019-11-01 22:29:52 +01:00
Wim
bad1990173 Release v1.16.1 2019-10-27 01:49:41 +02:00
Wim
0bc159341d
Update vendor (#932)
* Update vendor

* Fix godiscord api change
2019-10-27 01:45:57 +02:00
Wim
45bf1fd63a
Convert slack bold/strike to correct markdown (slack). Fixes #918 (#930) 2019-10-27 01:10:59 +02:00
Wim
ff0de85817
Remove obsolete file upload links (discord). Fixes #908 (#931)
Since v1.16.0 we now can upload files via webhook.
Old way of showing files with webhook only setup can be removed.
2019-10-27 01:10:43 +02:00
Wim
727fa9f929
Add support for uploading application/x and audio/x (matrix). Fixes #925 (#929) 2019-10-27 00:06:44 +02:00
Wim
0b9bc18236
Update vendor matterbridge/gomatrix fork (#928) 2019-10-26 23:31:44 +02:00
Wim
bad3b83d33
Update golang-commonmark/linkify vendor and use upstream again. Fixes #924 (#926) 2019-10-26 22:08:02 +02:00
Wim
00967a98ac
Fix panic on WebhookURL only setting (mattermost). Closes #916 (#917) 2019-10-04 01:01:24 +02:00
Qais Patankar
1d708ab351 Suppress unhandled HelloEvent message (slack) (#913) 2019-10-04 00:19:50 +02:00
Qais Patankar
ba6759010b Add UserTypingSupport (discord) (#914)
* Add Discord to UserTypingSupport

* discord: start typing in a channel on EventUserTyping receive

* discord: emit EventUserTyping to gateway
2019-10-04 00:18:56 +02:00
Wim
da3868c104 Try to fix blackfriday go modules mess 2019-09-22 00:34:37 +02:00
Wim
0abf4d5d5d
Specify correct GuildID on unknown user query (discord). Fixes #879 (#894) 2019-09-15 20:25:42 +02:00
Michal Suchánek
9b320cd43f Add token support (RocketChat) (#892)
Signed-off-by: Michal Suchanek <msuchanek@suse.de>
2019-09-13 23:41:02 +02:00
Wim
28783a4146
Do configuration validation on start-up. Fixes #888 (#889)
Fail if:
* we don't have any gateways configured
* we have gateways configured but with non-existing bridge configuration
* we have gateways configured without any configuration
2019-09-09 23:48:00 +02:00
Wim
f92927eae5 Fix deprecation in goreleaser 2019-09-07 23:37:49 +02:00
Wim
294139ce7a Bump version and fix changelog 2019-09-07 23:30:17 +02:00
Wim
45becd2573 Release v1.16.0 2019-09-07 23:17:55 +02:00
Wim
a3bee01e0a
Update dependencies (#886) 2019-09-07 22:46:58 +02:00
David Buckley
1dc93ec4f0 Make getChannelIdTeam behave like GetChannelId for groups (mattermost) (#873)
GetChannelId will support names generated from query groups when a team is not set,
but not when a team is set since it falls through to getChannelIdTeam which has a different inner loop. i
This pull makes the two implementations do the same thing.
2019-09-07 21:39:44 +02:00
Wim
3562d4220c
Bail if incorrect Jid (xmpp). Fixes #869 (#883) 2019-09-07 21:36:25 +02:00
Wim
1532f6e427
Update lrstanley/girc vendor (#884) 2019-09-07 21:35:45 +02:00
Wim
9327810bbf Add tengo example for nick color filter. See #881 2019-09-07 20:01:54 +02:00
Wim
f66d5f1e58 Add extra debug info (discord) 2019-09-05 22:39:43 +02:00
MOZGIII
cec086994e Add support for sending files via webhook (discord) (#872) 2019-08-29 00:13:10 +02:00
Wim
942d8f1ced
Create .fixmie.yml 2019-08-26 23:49:06 +02:00
Wim
1552dcb143
Replace bwmarrin/discordgo with matterbridge/discordgo (#878)
Needed for #872
2019-08-26 23:47:50 +02:00
Wim
d525f1c9e4
Update Rhymen/go-whatsapp vendor (#876) 2019-08-26 23:22:34 +02:00
cori hudson
921f2dfcdf Add initial Keybase Chat support (#877)
* initial work on native keybase bridging

* Hopefully make a functional keybase bridge

* add keybase to bridgemap

* send to right channel, try to figure out received msgs

* add account and userid

* i am a Dam Fool

* Fix formatting for messages, handle /me

* update vendors, ran golint and goimports

* move handlers to handlers.go, clean up unused config options

* add sample config, fix inconsistent remote nick handling

* Update readme with keybase links

* Resolve fixmie errors

* Error -> Errorf

* fix linting errors in go.mod and go.sum

* explicitly join channels, ignore messages from non-specified channels

* check that team names match before bridging message
2019-08-26 21:00:31 +02:00
Wim
79a006c8de
Fix regression (discord). Closes #864 (#866) 2019-07-29 23:37:38 +02:00
Wim
ff27746c0c Bump version 2019-07-15 23:23:32 +02:00
Wim
87788f354f Release v1.15.1 2019-07-15 23:09:46 +02:00
Wim
7d2e440c83
Add support for discord category channels (discord) (#863)
This adds support for the discord category option that can be used
to group channels in. This means we can have multiple channels with
the same name.

We add the option to specify a category in the channel option of a
discord account under [[gateway]]

Besides channel="channel" or channel="ID:channelID", now also
channel="category/channel" can be specified.

This change remains backwards compatible with people that haven't
specified the category and incorporates the fix in #861
2019-07-15 21:56:35 +02:00
Qais Patankar
5551f9d56f Fix discord channel & category name clash. #860 (#861) 2019-07-14 19:53:09 +02:00
Wim
1fb91c6316
Fix panic by checking slice bounds in handleEntities (telegram). Fixes #857 (#858)
Besides the bound checking, this now also use utf16 as suggested by
https://github.com/go-telegram-bot-api/telegram-bot-api/issues/231
2019-07-08 22:19:45 +02:00
Qais Patankar
e60949ff3f Support webhook message deletions (discord) (#853)
* Support webhook message deletions (discord)

Messages sent via webhook can now be deleted. It seems it can do this
without any special permissions.

This copies discordgo.WebhookExecute and makes it support the returning
of discordgo.Message.

A pull request has been sent upstream, so we should use that if
@bwmariin accepts the pull request:

https://github.com/bwmarrin/discordgo/pull/663

Changes in behaviour (webhook mode only):
- Previously messages *edited* on other platforms would just be
retransmitted as a brand new message to Discord.
- Message *edits* will now be ignored.
- Debug: message edits will now print out a "permission error".

In the future it may be good to send an "message edited" react to those
webhook messages, so at least people know that the message was edited on
other platforms. (Even though it can't actually show the new message.)

Alternatively, message edits could just send a brand new message with a
link back to the old one. This is a little ugly but it would ensure that
Discord users are able to see the edited message. These "message edit
notifications" would be sent from the bot user (not from a webhook), so
we could edit the "edit notification" if subsequent edits to the
original message are made.
2019-07-08 22:18:37 +02:00
Wim
278a3c6890 Update changelog 2019-06-30 18:50:45 +02:00
Wim
fcf734eb36 Update to golanci-lint v1.17.1 2019-06-30 18:43:54 +02:00
Wim
cf3cddafab Keep connection state. Fixes #856
Actually check if we're connected when trying to Send() a message.
Messages now will get dropped when not connected.

TODO: Ideally this should be in a ring buffer to retransmit when the
connection comes back up.
2019-06-30 18:34:41 +02:00
Wim
c52664f22e Update readme 2019-06-16 23:46:24 +02:00
Wim
cb712ff37d
Update vendor (#852) 2019-06-16 23:33:25 +02:00
Qais Patankar
f4ae610448 Add .gitignore (#850) 2019-06-16 16:37:38 +02:00
Wim
601b8bc98d Update documentation and changelog 2019-06-16 16:32:12 +02:00
Joona Hoikkala
80b4cec87a Add an option to skip the Mattermost server version check (#849)
Adds SkipVersionCheck bool option for mattermost
2019-06-16 16:23:50 +02:00
Qais Patankar
76c7b69e4e Support bulk deletions (discord) 2019-06-16 16:07:48 +02:00
Wim
a5bd3c4dda Bump version 2019-06-16 16:02:41 +02:00
Wim
f06e9b5605 Release v1.15.0 2019-06-14 01:36:55 +02:00
Nick
7a3bb0e55c Verify TLS against JID domain, not the host. (xmpp) (#834)
Partially fixes #820.

A full fix requires patching https://github.com/matterbridge/go-xmpp to use DNS SRV records.
2019-06-14 01:10:43 +02:00
Wim
6e8f535e8b Fix logic (xmpp) 2019-06-14 00:44:31 +02:00
Wim
5619a75b05
Fix regression in autojoining with legacy tokens (slack). Fixes #651 (#848) 2019-06-14 00:42:55 +02:00
Wim
53dfb78215
Allow messages with timestamp (xmpp). Fixes #835 (#847) 2019-06-14 00:24:42 +02:00
Wim
8e97cbab1e Fix noisy whatsapp error logging 2019-06-14 00:02:32 +02:00
Wim
ce7b749fd5 Update github.com/Rhymen/go-whatsapp vendor. Fixes #843 2019-06-14 00:02:32 +02:00
Wim
6617bd6609 Revert xmpp to orig behaviour. Closes #844 2019-06-13 23:35:04 +02:00
Wim
e610fb3201 Make config parse errors readable 2019-06-02 09:35:20 +01:00
Wim
40f1d35415 Fix go mod issue by removing whatsapp-ext 2019-06-02 09:35:20 +01:00
Duco van Amstel
b79bf7d414 Forward only user-typing messages if supported by protocol (#832)
Fixes issue #814.

This is a somewhat hacky way of achieving the required goal but it seems
like this is the least problematic way of getting there.

We might want to redesign some bridge information later such that we
have a standardised way of specifying what is and what isn't supported
by each chat protocol / bridge.
2019-05-30 15:00:40 +02:00
Duco van Amstel
3724cc3a15 Clean-up XMPP handling code (#831) 2019-05-30 12:31:54 +02:00
Wim
3418e8c9af
Use upstream whatsapp again (#809) 2019-05-30 12:20:56 +02:00
Duco van Amstel
9619dff334
Linter fixes 2019-05-27 17:38:31 +01:00
Wim
1b2feb19e5 Update channels of all teams (mattermost) 2019-05-02 00:46:49 +02:00
Wim
1829dc3d9f Allow messages from other bots (discord). Fixes #816 2019-05-01 18:10:31 +02:00
Wim
bd0e81f5a0 Add msg event to tengo 2019-04-24 22:47:37 +02:00
Wim
f04d360ee2 Update README with v1.14.4 2019-04-23 23:36:30 +02:00
Wim
92f27281fa Update changelog 2019-04-23 23:35:48 +02:00
Wim
65781b9316
Disable user lookups on delete messages (slack) (#812) 2019-04-23 23:29:15 +02:00
Duco van Amstel
9be0be0316 Add lacking clean-up in Slack synchronisation (#811) 2019-04-23 23:08:34 +02:00
Wim
9f5f004725 Use paging in initUser and UpdateUsers (mattermost) 2019-04-20 23:06:06 +02:00
Wim
fed77cccf3 Handle unthreaded messages (mattermost). Fixes #803 2019-04-19 23:31:45 +02:00
Wim
9b520dfb78 Fix panic on nil message.Post (mattermost). Fixes #804 2019-04-19 23:08:41 +02:00
Wim
8ad2be10b2 Add Id to EditMessage (mattermost). Fixes #802 2019-04-19 22:59:04 +02:00
Wim
2d277a15f5
Add scripting (tengo) support for every outgoing message (#806)
Adds a new key OutMessage under [tengo] table, which specifies the location of the script that
will be invoked on each message being sent to a bridge and can be used to modify the Username
and the Text of that message.

The script will have the following global variables:
read-only:
inAccount, inProtocol, inChannel, inGateway
outAccount, outProtocol, outChannel, outGateway

read-write:
msgText, msgUsername

The script is reloaded on every message, so you can modify the script on the fly.

The default script in https://github.com/42wim/matterbridge/tree/master/internal/tengo/outmessage.tengo
is compiled in and will be executed if no script is specified.
2019-04-19 18:27:31 +02:00
Wim
d60468bb05 Bump version 2019-04-19 18:24:13 +02:00
Wim
82d6210464 Update changelog 2019-04-19 18:23:50 +02:00
Wim
ff198042d2 Remove deprecated TengoModifyMessage
This has become InMessage under [tengo]
2019-04-19 00:13:28 +02:00
chotaire
6b47e29583 Add verbose IRC joins/parts (ident@host) (#805)
New configuration setting: VerboseJoinPart (default is false)
2019-04-18 23:56:05 +02:00
Wim
380c38674c Fix deadlock on reconnect (irc). Closes #757 2019-04-15 23:28:47 +02:00
Wim
3c14a0891e Remove hipchat 2019-04-14 23:54:05 +02:00
Wim
8513a07416 Update README 2019-04-14 23:48:54 +02:00
Qais Patankar
220485a849 Add remotenickformat-zerowidth.tengo to contrib (#799) 2019-04-14 23:42:16 +02:00
Wim
4db34b0506 Send channel_created and deleted event through message channel (mattermost) 2019-04-13 21:52:39 +02:00
Wim
5677c912a8
Add useraction support (rocketchat). Closes #772 (#794) 2019-04-08 23:30:22 +02:00
Wim
7a24de15e4
Add tengo support to RemoteNickFormat (#793)
This commit add support for using the result of a tengo script in RemoteNickFormat using {TENGO}
Also adds a new toml table [tengo] with key RemoteNickFormat and value location of the script.
This also moves the TengoModifyMessage from [general] to Message in [tengo]

Documentation:

RemoteNickFormat allows you to specify the location of a tengo (https://github.com/d5/tengo/) script.
The script will have the following global variables:
to modify: result
to read: channel, bridge, gateway, protocol, nick

The result will be set in {TENGO} in the RemoteNickFormat key of every bridge where {TENGO} is specified
The script is reloaded on every message, so you can modify the script on the fly.
Example script can be found in https://github.com/42wim/matterbridge/tree/master/contrib/remotenickformat.tengo

[tengo]
RemoteNickFormat="remotenickformat.tengo"
2019-04-08 20:58:21 +02:00
Wim
99d9ea283a Build on every branch (travis) 2019-04-07 23:57:47 +02:00
Wim
dac92a0e0a Add xmpp room to README. Closes #758 2019-04-07 15:48:19 +02:00
Wim
a25efb16f3 Bump version 2019-04-07 15:41:14 +02:00
Wim
e4d73b29a1 Release v1.14.2 2019-04-06 23:29:49 +02:00
Wim
8a875f292e Revert fix for #722. Closes #781
Revert "Fix typo"

This reverts commit dffd67eb311580aa2b14e7f5a069cfefdbf3aacc.

Revert "Handle quit message relay better on gateways with one channel on the irc bridge #722"

This reverts commit 240559581a225ff632c00a52e029b3728c61309e.

Revert "Support quits from irc correctly. Fixes #722 (#724)"

This reverts commit d76a04bd0a96772cec5b279aaa1ee45235adc707.
2019-04-06 23:12:48 +02:00
Wim
60a85621ea Return when not connected and drop a message (irc). Fixes #786 2019-04-06 22:34:41 +02:00
Wim
115d20373c
Update tengo vendor and load the stdlib. Fixes #789 (#792) 2019-04-06 22:18:25 +02:00
Wim
cdf33e5748 Use default nick if none specified (irc). Fixes #785 2019-04-05 00:17:46 +02:00
Wim
01d0a9f412 Handle nil message (telegram). Fixes #777 2019-04-05 00:04:08 +02:00
Wim
8cc2d3b4fe Revert "Bail if any vars are nil, not if all (telegram) (#778)"
This reverts commit efd2c9986270a8a1dd35f896a80d6465fc52054b.
2019-04-05 00:02:26 +02:00
Wim
aba9e4f3be Fix travis before_deploy 2019-04-04 23:22:56 +02:00
Wim
4d575ba13a Fix travis deploy condition and update to golangci-lint v1.16 2019-04-04 23:05:58 +02:00
Duco van Amstel
7f0e4ad448 Add CI fixes and improvements (#780)
* Update GolangCI-lint and lint config

The `algo` parameter for the `unparam` linter has been removed and we
should thus no longer specify it. Also, bumping the GolangCI-lint
version to the latest available minor release.

See: mvdan/unparam@e6a6d1c51b

* Fix and improve bintray CI script

* Further CI setup improvements

* Split-out CI steps into stand-alone scripts
2019-04-04 22:54:51 +02:00
Wim
17cc14a9d2 Send user_added and removed event through message channel (mattermost) 2019-04-02 00:15:58 +02:00
Wim
1f8016182c Return channelId for other channeltypes too (mattermost) 2019-04-01 22:50:19 +02:00
Wim
caf9ef2c4b Bump travis to go 1.12.x 2019-03-27 23:22:55 +01:00
Wim
64b57f2da3
Ignore message_replied and hidden messages (slack). Fixes #709 (#779) 2019-03-27 22:54:18 +01:00
David Hill
efd2c99862 Bail if any vars are nil, not if all (telegram) (#778) 2019-03-27 21:00:57 +01:00
Wim
cc05ba8907 Thank DigitalOcean (https://digitalocean.com) for another year of sponsorship 2019-03-25 21:13:11 +01:00
Wim
16763b715a
Look up #channel too (rocketchat). Fix #773 (#775) 2019-03-24 20:15:15 +01:00
Wim
ffaa598796 Bump version 2019-03-24 19:52:31 +01:00
Wim
858e16d34f Release v1.14.1 2019-03-21 21:07:11 +01:00
Wim
a60e62efb1 Update doc wrt rocketchat api issue 2019-03-21 21:05:27 +01:00
David Hill
97f9d4be67 Fix double unlock (slack) (#771) 2019-03-21 17:30:28 +01:00
Wim
fa4eec41f7 Release v1.14.0 2019-03-20 23:30:03 +01:00
Wim
77516c97db
Allow the # in rocketchat channels (backward compatible) (#769) 2019-03-20 23:19:27 +01:00
Wim
cba01f0865 Update rocketchat documentation 2019-03-20 23:18:40 +01:00
Duco van Amstel
8b754017ca Fix race-condition in populateUser() (#767)
Fix the root-cause of #759 by introducing synchronisation points for
individual user fetches.
2019-03-20 22:54:31 +01:00
Wim
a27600046e
Fix regression for legacy slack by #766 (#768) 2019-03-20 22:52:23 +01:00
Duco van Amstel
fb2667631d Refactor channel and user management (slack) (#766) 2019-03-15 21:23:09 +01:00
Duco van Amstel
b638f7037a Force Slack link unfurling (#763) 2019-03-12 22:56:43 +01:00
Duco van Amstel
74699a8262 Split-out Slack user and channel management (#762) 2019-03-12 22:52:36 +01:00
Duco van Amstel
eabf2a4582 Check module files in CI run (#761) 2019-03-12 22:47:18 +01:00
Wim
325d62b41c Update vendor d5/tengo 2019-03-05 23:10:45 +01:00
Wim
e955a056e2
Trim <p> and </p> tags (matrix). Closes #686 (#753) 2019-03-03 00:29:29 +01:00
Wim
723f8c5fd5 Only build travis on master branch 2019-03-03 00:24:49 +01:00
Wim
a16137f53f Bump version 2019-03-02 23:48:46 +01:00
Wim
d60b8b97f9 Add related projects to README 2019-03-02 23:48:30 +01:00
Wim
7b0bc51183 Release v1.14.0-rc2 2019-03-02 23:23:43 +01:00
Wim
53aa076555
Do not send duplicate messages (rocketchat). Fixes #745 (#752)
For an unknown reason we get duplicate messages (from the same channel)
using the realtime API when we have > 1 channel subscribed on.
Solution for now is caching the message ID in a LRU cache and ignoring
the duplicates.

This should be reviewed when we have actual editing support from the
realtime API
2019-03-02 22:58:14 +01:00
Wim
f57370f33a
Add support for URL in messageEntities (telegram). Fixes #735 (#736) 2019-03-02 22:38:44 +01:00
Wim
c557d51b6f
Need to specify /topic:mytopic for channel configuration (zulip). (#751)
Breaking change for zulip channel configuration.

For zulip the channel configuration will now need to specify also
the topic with /topic:yourtopic.

Example:
[[gateway.inout]]
account="zulip.streamchat"
channel="general/topic:mytopic"

This fixes the incorrect PR #701 which didn't work with multiple
gateways.
2019-03-02 20:31:38 +01:00
Wim
df3fdc26a0
Use whatsapp forks (#750) 2019-03-02 13:04:28 +01:00
Wim
af00c34aac
Do not relay any bot messages (discord) (#743) 2019-02-28 12:59:52 +01:00
Wim
120bf39f55
Handle file upload/download only once for each message (#742) 2019-02-27 20:52:05 +01:00
Wim
26a7e35f27
Add MediaConvertWebPToPNG option (telegram). (#741)
* Add MediaConvertWebPToPNG option (telegram).

When enabled matterbridge will convert .webp files to .png files
before uploading them to the mediaserver of the other bridges.

Fixes #398
2019-02-27 00:41:50 +01:00
Wim
d44d2a5f00 Build on all branches 2019-02-26 20:47:30 +01:00
Wim
7f1d86b338
Fail gracefully on incorrect human input. Fixes #739 (#740) 2019-02-26 18:03:50 +01:00
Wim
d8816280f0 Update changelog 2019-02-26 17:44:35 +01:00
Wim
b09a73040f
Print errors as string instead of %#v (#738) 2019-02-26 17:21:23 +01:00
Wim
740b5f2602
Keep reconnecting until succeed (zulip) (#737) 2019-02-26 17:08:20 +01:00
Wim
96841c70c7 Fix regression in HTML handling (telegram). Closes #734
* Revert back to blackfriday v1
* Add testing
2019-02-24 15:13:56 +01:00
Wim
f92735d35d Add goreleaser.yml 2019-02-24 01:09:53 +01:00
Wim
516fd3c92d Release v1.14.0-rc1 2019-02-23 23:20:25 +01:00
Wim
a775b57134
Do not send topic changes on connect (xmpp). Fixes #732 (#733)
This checks if we get a topic change < 5 seconds after connection.
If that's the case, ignore it.
Also this PR makes the topic change an actual EventTopicChange.
2019-02-23 23:03:21 +01:00
Wim
bf21604d42
Make all loggers derive from non-default instance (#728) 2019-02-23 22:51:27 +01:00
Wim
1bb39eba87
Add scripting (tengo) support for every incoming message (#731)
TengoModifyMessage allows you to specify the location of a tengo (https://github.com/d5/tengo/) script.
This script will receive every incoming message and can be used to modify the Username and the Text of that message.
The script will have the following global variables:
to modify: msgUsername and msgText
to read: msgChannel and msgAccount

The script is reloaded on every message, so you can modify the script on the fly.

Example script can be found in https://github.com/42wim/matterbridge/tree/master/gateway/bench.tengo
and https://github.com/42wim/matterbridge/tree/master/contrib/example.tengo

The example below will check if the text contains blah and if so, it'll replace the text and the username of that message.
text := import("text")
if text.re_match("blah",msgText) {
    msgText="replaced by this"
    msgUsername="fakeuser"
}

More information about tengo on: https://github.com/d5/tengo/blob/master/docs/tutorial.md and
https://github.com/d5/tengo/blob/master/docs/stdlib.md
2019-02-23 16:39:44 +01:00
Wim
3190703dc8
Support rewriting messages from relaybots using ExtractNicks. Fixes #466 (#730)
some examples:
this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
you can use multiple entries for multiplebots
this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
OPTIONAL (default empty)
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
2019-02-23 16:35:54 +01:00
Wim
5095db8a43 Update vendor 2019-02-23 14:14:29 +01:00
Wim
1f1634ea59 Add extra debug option (slack) 2019-02-22 19:36:50 +01:00
Declan Hoare
a7dd033c3b Allow sending discriminator with Discord username (#726) 2019-02-22 14:28:27 +01:00
Wim
95e78ffa05 Add telegram support to matterbridge chat channel 2019-02-21 21:35:34 +01:00
Wim
42276ea7d0 Disable updateChannelMembers for now 2019-02-21 21:26:12 +01:00
Wim
dffd67eb31 Fix typo 2019-02-21 20:33:49 +01:00
Krzysiek Madejski
55e79063d6 Add initial WhatsApp support (#711) 2019-02-21 20:28:13 +01:00
Wim
46f4bbb3b5 Update documentation wrt ShowJoinPart from discord 2019-02-21 18:15:01 +01:00
Wim
240559581a Handle quit message relay better on gateways with one channel on the irc bridge #722 2019-02-21 17:55:04 +01:00
Wim
48ba829465 Bump to 1.14.0-dev 2019-02-17 22:46:07 +01:00
Wim
eef654de98 Fix bug in #721 2019-02-17 22:45:23 +01:00
Wim
d76a04bd0a
Support quits from irc correctly. Fixes #722 (#724) 2019-02-17 22:43:04 +01:00
Wim
a8fe54a78d
Allow zulip bridge to specify topic per channel. Closes #701 (#723) 2019-02-17 21:50:05 +01:00
Wim
0bcb0b882f
Support join/leaves from discord. Closes #654 (#721) 2019-02-17 21:49:45 +01:00
Wim
4525fa31aa
Allow regexs in ignoreNicks. Closes #690 (#720) 2019-02-17 21:49:28 +01:00
Wim
aeaea0574f
Detect html nicks in RemoteNickFormat (matrix). Fixes #696 (#719) 2019-02-17 21:48:32 +01:00
Wim
99d71c2177
Send notices on join/parts (matrix). Fixes #712 (#716) 2019-02-16 18:36:09 +01:00
Wim
3e60cfafd3
Send username when uploading video/images (matrix). Fixes #715 (#717) 2019-02-16 18:35:36 +01:00
Wim
3123695869
Upgrade to latest girc version (irc) (#718) 2019-02-16 17:24:04 +01:00
Wim
777af73e2b Add blogpost about matterbridge 2019-02-15 19:02:29 +01:00
Wim
716751cf76
Refactor and update RocketChat bridge (#707)
* Add support for editing/deleting messages
* Add support for uploading files
* Add support for avatars
* Use the Rocket.Chat.Go.SDK
* Use the rest and streaming api
2019-02-15 18:20:32 +01:00
Wim
6ebd5cbbd8 Refactor and update RocketChat bridge
* Add support for editing/deleting messages
* Add support for uploading files
* Add support for avatars
* Use the Rocket.Chat.Go.SDK
* Use the rest and streaming api
2019-02-15 18:19:34 +01:00
Wim
077b818d82 Add extra debug of SubMessage to empty messages error (slack). #709 2019-02-15 18:05:10 +01:00
Wim
5af1d80055 Do not panic on non-json response from server (zulip) 2019-02-13 00:29:34 +01:00
Wim
f236d12166 Update golangci-lint. Disable hugeParam check for now 2019-02-12 20:30:36 +01:00
Wim
127eb908f3 Add fbridge to README 2019-02-12 17:21:27 +01:00
Wim
40d76b2296 Fix error handling on bad event queue id (zulip). Closes #694 2019-02-11 01:34:50 +01:00
Wim
8147815037 Update README. Add rocketchat 2019-02-10 23:32:17 +01:00
Wim
57f156be83
Hint at thread replies when messages are unthreaded (slack) (#684) 2019-02-10 17:23:50 +01:00
AJolly
2cfd880cdb Clarify dev chat info (#700) 2019-02-05 20:40:57 +01:00
Wim
430b38e770 Update README 2019-01-31 17:38:02 +01:00
Wim
e7f463a082 Bump version 2019-01-31 17:26:07 +01:00
Wim
1d39c771e4 Release v1.13.1 2019-01-31 17:10:57 +01:00
Wim
c81c0dd22a Update vendor, move to labstack/echo/v4 Fixes #698 2019-01-31 17:06:36 +01:00
Wim
f8a1ab4622 Bump version 2019-01-31 00:15:48 +01:00
Wim
5d3309fdcd Release v1.13.0 2019-01-30 23:55:37 +01:00
Wim
4ae028fe73 Optimize handling of very large slack teams. Fixes #695
Stop getting users if we reach 2000 users. Slack will rate-limit us
even if we follow their limits.
This means that we now have to lookup every user that says a message
for the first time. This should be less intensive on the API.

This also disables partly fb713ed91bfb48c0037e489f80be85c54e69953a for now.
ChannelMembers will not be filled.
2019-01-30 23:28:37 +01:00
Wim
707db950c8 Send GetChannelMembers event only to slack for now 2019-01-24 22:46:05 +01:00
Wim
94812d8648 Handle servers without MOTD (irc). Closes #692 2019-01-24 21:58:27 +01:00
Wim
8548b69e6e Fix possible data race (irc). Closes #693 2019-01-24 21:51:52 +01:00
Wim
e3cb665d92 Make discord user token work correctly (discord) #689 2019-01-19 20:39:58 +01:00
Wim
fb713ed91b
Add initial support for getting ChannelMember info of all bridges (#678)
* Add initial support for getting ChannelMember info of all bridges.

Adds an EventGetChannelMembers event, which gets send every x time to
all bridges. Bridges should respond on this event with a Message
containing ChannelMembers in the EventGetChannelMembers key in the
Extra field.

handleEventGetChannelMembers will handle this Message and sets the
contained ChannelMembers to the Bridge struct.

* Add ChannelMembers support to the slack bridge
2019-01-18 18:35:31 +01:00
Wim
d99eacc2e1 Run go fmt 2019-01-14 19:41:32 +01:00
Zomboy Alfrir
62e55214fc Allow to bridge non-bot Discord users (discord) (#689)
If you prefix a token with `User ` it'll treat is as a user token.

Co-Authored-By: zomboy-alfrir <zomboy@dancodes.com.ar>
2019-01-14 19:27:49 +01:00
Wim
464d27ad7e Revert "Update pinned golangci-lint version (#666)"
This reverts commit 015c0763153778779857597178a0aabd7ac1215a.
Goimports regression: https://github.com/golangci/golangci-lint/issues/347
And gocritic recommending fixes in tip instead of released versions.
2019-01-14 19:17:41 +01:00
David Hill
f88c5f6c08 Fix displaying usernames for plain text clients. (matrix) (#685) 2019-01-09 23:15:26 +01:00
Wim
da6ce791bc Add link to API page on the wiki 2019-01-09 23:10:32 +01:00
Patrick Connolly
b33b50987b Add support for mattermost threading (#627) 2019-01-09 21:50:03 +01:00
James Nylen
5193634a52 Use only one webhook if possible (discord) (#681) 2019-01-09 21:28:47 +01:00
Wim
0d94746f4a Add api.yaml to contrib 2019-01-09 00:32:04 +01:00
Wim
85680935d4 Add swaggerhub link to README (api) 2019-01-09 00:27:23 +01:00
Wim
46e2683995 Add file comment to webhook messages (discord). Fixes #358 2019-01-07 22:16:00 +01:00
James Nylen
492722af8b Improve error reporting on failure to join Discord. Fixes #672 (#680) 2019-01-07 21:39:53 +01:00
Wim
56749dfb20 Fail if channel starts with hashtag (mattermost). Closes #625 2019-01-07 00:26:11 +01:00
Wim
04567c765e
Add support for markdown to HTML conversion (matrix). Closes #663 (#670)
This uses our own gomatrix lib with the SendHTML function which
adds HTML to formatted_body in matrix.
golang-commonmark is used to convert markdown into valid HTML.
2019-01-06 22:25:19 +01:00
Neustradamus
048158ad6d Update README about xmpp. Fixes #676 (#677) 2019-01-06 19:34:47 +01:00
ValdikSS
7326b9e10d Add various sshchat fixes (#675)
* SSH-Chat: set quiet mode to filter joins/quits
* SSH-Chat: Trim newlines in the end of relayed messages
* SSH-Chat: fix media links
* SSH-Chat: do not relay "Rate limiting is in effect" message
2019-01-05 15:42:36 +01:00
Qais Patankar
8522d8f29c Fix #668 strip lang in code fences sent to Slack (#673) 2019-01-04 20:32:58 +01:00
Wim
bab385c342 Remove unused key (config) 2019-01-04 16:37:45 +01:00
Wim
bb27ef7939 Add link to matterbridge and k8s article 2019-01-04 16:28:12 +01:00
Wim
d2044c647b Update vendor
* go-telegram-bot-api/telegram-bot-api
* lrstanley/girc
* matterbridge/gomatrix
2019-01-03 00:07:50 +01:00
Wim
c585d00f16 Ignore LatencyReport event (slack) 2019-01-02 23:55:00 +01:00
Duco van Amstel
015c076315 Update pinned golangci-lint version (#666) 2018-12-30 21:29:05 +01:00
Wim
426aa33723 Try building arm docker image 2018-12-26 17:27:25 +01:00
Duco van Amstel
da8e415ae1 Use logrus imports instead of log (#662) 2018-12-26 15:16:09 +01:00
Duco van Amstel
1b834c6858 Fix sshchat connection logic (#661) 2018-12-26 15:09:36 +01:00
Jerry Heiselman
d82726cd1b Try downloading files again if slack is too slow (slack). Closes #655 (#656) 2018-12-19 22:01:05 +01:00
Wim
288f0a06bb Bump version 2018-12-15 23:33:13 +01:00
Wim
0121d75032 Update changelog 2018-12-15 23:32:48 +01:00
Wim
53c86702a3
Add wait option for populateUsers/Channels (slack) Fixes #579 (#653)
When setting wait to true, we wait until the populating isn't in progress anymore.
This is used on startup connections where we really need the initial information
which could take a long time on big servers.
2018-12-15 23:11:03 +01:00
David Hill
192fe89789 Populate user on channel join (slack) (#644) 2018-12-15 22:57:54 +01:00
Wim
959ca3cef3 Fix bot (legacy token) messages not being send. Closes #571 2018-12-13 20:49:14 +01:00
Wim
ccd55d2a28
Refactor gateway (#648)
* Decrease complexity of handleMessage, handleReceive, handleFiles
* Move handlers to handlers.go
* Split ignoreMessage up in ignoreTextEmpty, ignoreNicks and IgnoreTexts
* Add ignoreEvent
* Add testcase for ignoreTextEmpty, ignoreNicks, ignoreTexts and ignoreEvent
2018-12-12 23:57:17 +01:00
Wim
bfa9a83d31
Refactor telegram (#649)
* Decrease complexity in Send() (makes codeclimate happy)
2018-12-12 23:50:08 +01:00
Wim
2f7b4d7f68
Refactor sshchat bridge (#650)
* Decrease complexity in Send()
* Add handleUploadFile() function
2018-12-12 23:47:07 +01:00
Wim
d887855e16 Add bot debug info (slack) 2018-12-12 00:27:55 +01:00
Wim
1a1e68ec98 Enable gocyclo linter 2018-12-09 14:25:17 +01:00
Duco van Amstel
a2754f15fc Enable errcheck linter (#646) 2018-12-08 17:04:10 +01:00
Wim
b6d81f34ba Add repology link 2018-12-07 23:55:08 +01:00
Wim
f9fb33e696
Refactor steam bridge (#630)
* split up in different files
* decrease complexity
2018-12-07 23:48:24 +01:00
Wim
f72d5de2d7 Disable some unparam checks (discord) 2018-12-07 23:48:00 +01:00
Duco van Amstel
0365c0786a Split Discord bridge in multiple files (#632) 2018-12-07 23:36:01 +01:00
Duco van Amstel
af7a00d030 Enable gosec linter (#645) 2018-12-06 00:40:55 +01:00
Duco van Amstel
8a7efce941 Move golangci-lint configuration to file (#635) 2018-12-05 11:34:34 +01:00
Justin W. Flory
ce73aa5a74 Add FOSSRIT/infrastructure, alphabetically sort related project list (#643) 2018-12-04 23:39:31 +01:00
Wim
64d63a25cc Bump version 2018-12-04 10:36:02 +01:00
Wim
2cef9c4fcf Update changelog 2018-12-04 10:35:15 +01:00
Wim
4265d43096
Refactor handleUploadFile (matrix) (#629) 2018-12-03 16:51:11 +01:00
Patrick Connolly
25857591a2 Add note about slack/slack-legacy issues on threading. (#634) 2018-12-03 16:50:37 +01:00
Wim
27f5a1a685 Fix multiple channel join regression. Closes #639 2018-12-03 16:37:12 +01:00
Duco van Amstel
84da2d6a29 Clean-up TravisCI config and add test coverage (#633) 2018-12-03 00:07:15 +01:00
Wim
859ebad55d
Make slack-legacy change less restrictive (#626) 2018-12-02 23:09:21 +01:00
Patrick Connolly
e538a4d304 Update nlopes/slack to 4.1-dev (#595) 2018-12-01 19:55:35 +01:00
Wim
f94c2b40a3
Refactor mattermost bridge (#622)
* Split up in different files
* Decrease complexity
* Fix linting issues
2018-12-01 00:49:08 +01:00
Patrick Connolly
47d29ecf63 Tidy up fetching of config values. (#616) 2018-12-01 00:24:07 +01:00
Patrick Connolly
f2088a687e Extract bridgeMap into own package to improve testability (#601) 2018-11-30 23:53:00 +01:00
Wim
faeeee2948
Refactor matterclient (#613)
* Split up in different files
* Decrease complexity
2018-11-29 23:53:43 +01:00
Wim
7923cfe8f8 Fix telegram crash #620 2018-11-29 23:03:50 +01:00
Wim
b51d0a9b05 Bump version 2018-11-29 00:20:09 +01:00
Wim
09f22a801e Release v1.12.1 2018-11-29 00:19:37 +01:00
Wim
3a824c5f9d Update changelog 2018-11-29 00:19:27 +01:00
Wim
df02f51c56 Fix regression on using server ID (discord). #619 #617 2018-11-28 23:50:40 +01:00
Patrick Connolly
fc5e3a6728 Create getChannelsByX functions to make codeclimate happy (slack) (#610) 2018-11-28 11:04:26 +01:00
Wim
57fbd3c723
Refactor irc handlers. Fix linting (#611) 2018-11-28 10:58:56 +01:00
Wim
25cd1e2cc1
Refactor telegram handlers. Fix linting (#609)
* Refactor telegram handlers. Fix linting
2018-11-28 10:57:59 +01:00
Patrick Connolly
f5659d455d Sync channel topics between Slack bridges (#585)
Added logic to allow for configurable synchronisation of topics and purposes of channels between Slack bridges.
2018-11-26 09:47:04 +00:00
Wim
5ed7abdbeb Drop support for mattermost 3.x 2018-11-25 22:38:15 +01:00
Duco van Amstel
09875fe160 Update direct dependencies where possible 2018-11-25 21:21:04 +01:00
Wim
f716b8fc0f
Merge pull request #606 from 42wim/fix-590 2018-11-25 20:40:22 +01:00
Wim
9f66f93641
Add option to send RAW commands after connection (irc). Fixes #490 (#604) 2018-11-25 19:32:16 +01:00
Wim
f00d4d7d3f Make sure threaded files stay in thread (slack). Fixes #590 2018-11-25 19:27:45 +01:00
Wim
0929535b2e Do not post empty messages (slack). Fixes #574 2018-11-25 19:26:47 +01:00
Wim
8869e253ca Handle deleted/edited thread starting messages (slack). Fixes #600 (#605) 2018-11-25 10:08:57 +00:00
jamoffat
f3a5ea2956 Remove double " from Discord gateway webhookurl= (#607) 2018-11-25 10:37:14 +01:00
Wim
f4d4dc91b1
Add option to ignore failing bridge on start. Fixes #455 (#603) 2018-11-25 10:35:35 +01:00
Wim
c6fd65d1d7 Limit discord username via webhook to 32 chars 2018-11-23 20:52:31 +01:00
Wim
0795906533 Rework connection logic (irc)
If IRC connection fails on first connect, bail out.
Wait until after nickserv auth until joining channels (also after reconnects)
Don't do a separate irc timeout, some connections take a while #503
2018-11-23 00:26:50 +01:00
Wim
a2b45bc799 Fix Nickserv logic (irc) #602 2018-11-22 22:46:38 +01:00
Wim
757657f29c Bump version 2018-11-19 21:49:21 +01:00
Wim
219c7659e1 Release v1.12.0 2018-11-19 21:40:46 +01:00
Wim
ae32bae791
Add protocol to msg.ID in cache (#596) 2018-11-19 21:28:23 +01:00
Wim
57eba77561 Update changelog, bump dev version 2018-11-18 18:52:37 +01:00
Duco van Amstel
d5bc7c4343
Merge pull request #598 from Helcaraxan/feature/update-deps
Upgrade logrus / testify to stable versions
2018-11-18 16:32:26 +00:00
Wim
32f57b7c26 Add links to slack bot and legacy config in error message (slack) 2018-11-18 17:14:47 +01:00
Duco van Amstel
692bb8faa7
Upgrade logrus / testify to stable versions 2018-11-18 01:10:15 +00:00
Duco van Amstel
455a0fc239
Replace documentation image 2018-11-18 00:16:49 +00:00
Duco van Amstel
b2cbd13251
Images for Slack documentation on the wiki 2018-11-18 00:03:11 +00:00
Duco van Amstel
ce21ba1545 Fix golint linter issues and enable it in CI (#593) 2018-11-15 20:43:43 +01:00
Duco van Amstel
c89085bf44 Fix and enable goimports linter (#591) 2018-11-15 19:24:22 +01:00
Patrick Connolly
4254ed3c63 Fix regression in skip logic (slack). (#592) 2018-11-15 19:23:46 +01:00
Duco van Amstel
85564a35fd Fix IRC line splitting. Closes #584 (#587) 2018-11-14 22:43:52 +01:00
Patrick Connolly
09713d40ba Fix file caching issue (slack). #572 (#575) 2018-11-14 21:00:21 +01:00
Duco van Amstel
16d5aeac7c Make config.Config more unit-test friendly (#586) 2018-11-13 23:30:56 +01:00
Duco van Amstel
e19ba5a06a Add new Slack connection and forked legacy Slack bridge (#582) 2018-11-13 20:51:19 +01:00
Wim
f7a5077d5d Fix goconst linter failure 2018-11-13 20:40:15 +01:00
Wim
f8dc24bc09 Switch back go upstream bwmarrin/discordgo
Commit ffa9956c9b got merged in.
2018-11-13 00:02:07 +01:00
Duco van Amstel
e9419f10d3 Restore file comments coming from Slack (#583) 2018-11-12 15:58:00 +01:00
Wim
cded603c27 Add note about matterbridge mattermost-plugin 2018-11-11 23:39:36 +01:00
Wim
d2ae3ebf9e Disable Connect(), JoinChannel(), Send() for mattermost.plugin 2018-11-11 22:44:10 +01:00
Wim
730ccdd456 Add support for mattermost matterbridge plugin 2018-11-11 21:56:12 +01:00
Duco van Amstel
2f042ad915 Add more rate-limit handling (slack) (#581) 2018-11-10 22:09:41 +01:00
Wim
ba70691877 Increase git depth for travis 2018-11-10 19:35:38 +01:00
Patrick Connolly
ed11686a99 Improve user_typing botname suggestion. (#580) 2018-11-09 21:52:37 +01:00
Wim
5c50d86908 Add demo explanation 2018-11-09 21:25:04 +01:00
Patrick Connolly
fea31753b0 Improve README formatting (incl codeclimate badges) (#578)
* Updated header, removed whitespace, added codeclimate badges, adjusted titles.

* TOML formatting in README.
2018-11-09 21:19:36 +01:00
Wim
0d64cd8bab Switch to golangci-lint 2018-11-08 23:09:58 +01:00
Wim
9be0f8f000 Make gochecknoinits linter happy 2018-11-08 22:33:03 +01:00
Wim
78401214b0 Make scopelint happy 2018-11-08 22:29:34 +01:00
Wim
b2a07aba3a Make goconst linter happy 2018-11-08 22:20:03 +01:00
Wim
1e0bb3da95 Make gocritic linter happier 2018-11-08 22:01:29 +01:00
Wim
59994da176 Act only on UserTypingEvents when enabled 2018-11-08 21:52:10 +01:00
Patrick Connolly
3d281b3316 Add ability to show when user is typing across Slack bridges (#559) 2018-11-08 20:45:40 +01:00
Duco van Amstel
ea86849a58 Fix Slack edit usernames (#570) 2018-11-08 20:07:21 +01:00
Wim
399789811e Make gocritic linter happy 2018-11-08 00:46:34 +01:00
Wim
8d117cb0a4 Make structcheck linter happy 2018-11-08 00:38:33 +01:00
Wim
588b8e0303 Make interfacer linter happy 2018-11-08 00:35:30 +01:00
Wim
1794922263 Make unparam linter happy 2018-11-08 00:29:30 +01:00
Wim
0ededb8863 Merge branch 'master' of github.com:42wim/matterbridge 2018-11-08 00:26:13 +01:00
Wim
aa59bb1a41 Enable go vet 2018-11-08 00:17:38 +01:00
Patrick Connolly
f2703979a4 Clean up config loading. (#561) 2018-11-07 22:32:12 +01:00
Duco van Amstel
d2a1dc792f Refactor and clean-up handlers. (slack) (#533) 2018-11-07 21:35:59 +01:00
Wim
06d66a0b2b Fix travis typo 2018-11-07 20:40:15 +01:00
David Hill
0e2522279e Clean up various stuff (#508)
* various cleanups
2018-11-07 20:36:50 +01:00
Wim
141a42a75b Add go fmt test again to travis 2018-11-07 20:32:39 +01:00
Duco van Amstel
a1bf37e457 Do not join Slack channel without API access (slack) (#563) 2018-11-07 17:25:00 +01:00
Patrick Connolly
a20b7895a9 Preserve threading between Slack instances (#529)
* Opportunistically preserve Slack threading when parent thread in cache. [#529]

* Removed slack-specific processing from gateway.

* Added docs.

* Add option to enable threading, with default to off.

* Did cleanup on @42wim's comments.

* Update gateway/gateway.go

Co-Authored-By: patcon <patrick.c.connolly@gmail.com>

* Suggestion from @42wim :)

* Suggestions from @42wim.

* More suggestions.
2018-11-07 09:14:31 +01:00
Patrick Connolly
5666821e7b Add a health endpoint to API (#554) 2018-11-07 09:11:59 +01:00
Patrick Connolly
5132d8f097 Stop setting API ring buffer capacity if not specified. (#552) 2018-11-05 21:53:51 +01:00
Wim
b81ff9c008 Add SendDirectMessageProps to send a DM with extra props (mattermost) 2018-11-03 21:51:04 +01:00
Patrick Connolly
7e62bc4819 Remove hyphens when auto-loading envvars from viper config (#545)
* When auto-loading envvars from toml keys, remove hyphens.

See: https://unix.stackexchange.com/questions/23659/can-shell-variable-include-character
2018-11-03 14:42:27 +01:00
NikkyAI
d058be25ad Respond with message on connect (api) (#550)
fix #549
2018-11-02 16:35:13 +01:00
Duco van Amstel
1269be1d04 Prevent Slack API rate-limit overflow (#539) 2018-11-01 21:28:22 +01:00
Wim
3b8837a16b Update README 2018-10-28 14:54:25 +01:00
Wim
32f478e4a0 Check for expiring sessions and reconnect (mattermost) 2018-10-27 22:03:41 +02:00
Wim
e2b50d6194 Add better support for multiperson DM (mattermost) 2018-10-27 22:02:25 +02:00
Wim
74e33b0a51 Update channels when a new group is created (mattermost) 2018-10-27 13:20:40 +02:00
Wim
107969c09a
Split up cookie token and personal token (mattermost). Fixes #530 (#540) 2018-10-26 16:47:56 +02:00
Patrick Connolly
d379118772 Fix bridge no longer POSTing username and avatar (slack) (#536)
* Fixed pointer/reference issue in populateUsers. [#536]

* Accepted codestyle suggestion.

* Update bridge/slack/helpers.go

Co-Authored-By: patcon <patrick.c.connolly@gmail.com>

* Update helpers.go
2018-10-24 21:12:20 +02:00
Patrick Connolly
291594b99c Allow origin CHANNEL to be used in RemoteNickFormat (#515)
* Added origin CHANNEL to RemoteNickFormat. Updated config docs. [Fixes #515]

* Update matterbridge.toml.sample

Co-Authored-By: patcon <patrick.c.connolly@gmail.com>
2018-10-23 21:53:11 +02:00
Duco van Amstel
f2cdda7278 Update Blackfriday dependency (closes #522) (#532)
- Fixup Telegram bridge implementation to support updated dependency.
2018-10-22 19:48:29 +02:00
Duco van Amstel
6911458d15 Clean up message send logic (slack). (#531) 2018-10-22 19:43:57 +02:00
Duco van Amstel
6238effdc2 Clean up user and channel information management (slack) (#521) 2018-10-16 20:34:09 +02:00
Duco van Amstel
498377a230 Clean up code and strengthening (slack) (#519)
Changes include:
- Refactor of strings into package-wide constants.
- Predeclaration of regexps to be instantiated at package load time.
- Checking of unchecked errors.
- Structural changes:
  - Adding verifications to type-casting code.
  - Remove unnecessary 'len(X) > 0' checks before iterating over X.
  - Remove unnecessary 'else' clause after 'if' with 'return'.
  - Unexporting of public fields of Bridge struct.
- Formatting:
  - One-field-per-line struct definitions.
2018-10-13 01:02:14 +02:00
Duco van Amstel
3dd4ec57ff Fix race in gateway test. (#520) 2018-10-13 00:47:18 +02:00
Duco van Amstel
e15b0e04b8 Refactor slack bridge prelude (#517)
Distributing the source of the Slack bridge across multiple files to
increase readability and as a prelude to various refactors and
clean-ups.
2018-10-12 23:16:34 +02:00
Duco van Amstel
97b1fc813b Bump Go version in Travis CI (#518) 2018-10-12 23:14:36 +02:00
Duco van Amstel
917040b044 Update of nlopes/slack dependency (#511) 2018-10-07 23:17:46 +02:00
Duco van Amstel
69646a160d Add Gateway's name to RemoteNickFormat (#501)
In order to support extra use cases we should add the `{GATEWAY}` tag to the `RemoteNickFormat` string which would be replaced by the value of the `name=` field from a gateway's configuration.

This is _very_ useful when you are forwarding, for example, multiple channels from one chat to a single channel on another one (one-way). It will help you identify the source channel of a message on the target chat.
2018-10-07 15:22:15 +02:00
NikkyAI
54adb0509e Fix mentions cuttíng off all text after the mention (discord) (#506) 2018-09-29 20:02:59 +02:00
Wim
bd3a3b6eaf Let webhook also replace mentions (discord). Closes #502 2018-09-22 22:15:19 +02:00
NikkyAI
296428d53e Fix Discord mentions by populating the nickMemberMap at connect (#498) 2018-09-17 21:25:06 +02:00
Wim
e0ca876de2 Update vendor lrstanley/girc 2018-09-14 00:18:20 +02:00
Jerry Heiselman
a431a4fa04 Replace @... string with user mention if match found (discord) (#492). Closes #460
* Added check for @-mention pattern and replacing it with a user with a matching Nick on incoming messages
2018-09-12 22:30:14 +02:00
Declan Hoare
cc2bd03ec9 Add Mattereddit to README.md (#493) 2018-09-01 18:45:41 +02:00
Wim
1fe81b7d1e Bump version 2018-08-30 23:14:37 +02:00
Wim
0bd5a0d92d Release v1.11.3 2018-08-30 23:10:05 +02:00
Wim
330ddb6a30 Fix panic by using matterclient calls in the right place. Related to cb7278eb (mattermost). Closes #491 2018-08-30 23:04:50 +02:00
Wim
52dbd702ad Get up to 1000 channels and private/mp/im channels (slack). Related to #489 2018-08-28 22:33:07 +02:00
Wim
d7c3570ba3 Check nickname on kick (irc). Closes #488 2018-08-27 21:20:41 +02:00
Wim
ab4d51b40b Bump version 2018-08-19 23:32:42 +02:00
Wim
1665c93d3b Release v1.11.2 2018-08-19 23:29:40 +02:00
Wim
b51fdbce9f Add caching to fix issue with slack API changes (slack). #481 2018-08-18 00:12:05 +02:00
Wim
351b423e15 Add a bit more debugging (irc). #482 2018-08-16 23:02:28 +02:00
Wim
7690be1647 Fix slack file/image downloads after api changes (slack) 2018-08-10 00:39:07 +02:00
Wim
68aeb93afa Update nlopes/slack vendor 2018-08-10 00:38:19 +02:00
Wim
51062863a5 Use mod vendor for vendored directory (backwards compatible) 2018-08-06 21:47:05 +02:00
Wim
4fb4b7aa6c Start using go mod 2018-08-06 21:43:34 +02:00
Wim
7f3cbcedc0 Use own forks for logrus-prefixed-formatter and discordgo 2018-08-06 21:11:13 +02:00
Wim
6ef09def81 Bump version 2018-08-06 17:53:09 +02:00
Wim
c4c6aff9a5 Release v1.11.1 2018-08-06 17:49:14 +02:00
Wim
d71850cef6 Use UserID to look for avatar instead of username (slack). Closes #472 2018-08-06 16:44:15 +02:00
Wim
2597c9bfac Clip too long messages sent to discord (discord). Closes #440 2018-07-22 00:28:17 +02:00
Wim
93307b57aa Skip empty messages being sent with the webhook (discord). #469 2018-07-21 23:19:11 +02:00
Wim
618953c865 Remove ununsed function (slack) 2018-07-13 23:28:23 +02:00
Wim
e04dd78624 Add support for slack channels by ID. Closes #436 2018-07-13 23:23:11 +02:00
Wim
fa0c4025f7 Fix avatar uploads to work with MediaDownloadPath. Closes #454 2018-07-11 23:44:29 +02:00
John
2d2d185200 Stop numbers being stripped after non-color control codes (irc) (#465)
Currently numbers are stripped not just after the color control code (\x03) but also after other formatting such as bold (\x02) and italic (\x1D), which is both unnecessary and leads to missing text from irc. This fixes that by only stripping numbers after the color control code.
2018-07-11 22:50:49 +02:00
Wim
cb7278eb50 Use nickname instead of username if defined (mattermost). Closes #452 2018-07-03 22:41:09 +02:00
Wim
89aa114192 Add GetNickname and UpdateUser functions
When we get an user_updated event from mattermost we also actually update
the user, so the nicknames/usernames are also updated
2018-07-03 22:35:44 +02:00
Wim
ed062e0ce5 Add a space before url in file uploads (discord). Closes #461 2018-06-29 22:35:29 +02:00
Wim
a69ef8402b Fix previous commit 2018-06-28 21:19:02 +02:00
Wim
8779f67d2d Allow join-leave and topic changes to webhook (discord) 2018-06-28 21:14:31 +02:00
Wim
e4b72136b8 Fix possible panic. #448 2018-06-19 22:53:45 +02:00
Wim
4ff5091bc2 Bump version 2018-06-19 00:41:49 +02:00
Wim
6f131250f1 Release v1.11.0 2018-06-19 00:28:16 +02:00
Wim
221a63d980 Fix build (telegram) 2018-06-18 23:49:28 +02:00
Wim
d02eda147c Add support for MessageFormat=htmlnick (telegram). #444 2018-06-18 23:38:52 +02:00
ckartchner
9b25716136 Fix typo and add minor clarification. (#450) 2018-06-18 23:00:12 +02:00
Bruno Bierbaumer
6628a47f23 Add channel password support for XMPP (#451) 2018-06-18 22:55:45 +02:00
Wim
ec0e6bc3f8 Add support for mattermost 5.x 2018-06-17 23:48:06 +02:00
Wim
d2c02be3a0 Handle slack attachments sent to mattermost. Closes #447 2018-06-16 00:11:15 +02:00
Maxim
594492fbdd Add Title from attachment slack message (#446) 2018-06-13 21:58:51 +02:00
Wim
bd9ea7a88d Add MediaDownloadBlacklist option. Closes #442 2018-06-09 14:35:02 +02:00
Liam Stanley
51327a4056 Reconnect on quit. (irc) See #431 (#445)
* potential fixes for #431
* go: fix formatting/gofmt/goreturns
2018-06-09 12:47:40 +02:00
Remi Reuvekamp
33bd60528b Add config option MediaDownloadPath (#443)
* Add config option MediaUploadPath

MediaDownloadPath can be used instead of MediaServerUpload, for when your
webserver is on the same system as matterbridge and matterbridge has
write access to the serve dir.

* Limit length of hash in MediaServer urls to 8chars

Full SHA256 is unnecessary for uniqueness.
Also; if a file has the same first 8 charachters of the SHA256 hash,
it's still not a problem, as long as the filename is not the same.
2018-06-08 22:30:35 +02:00
Wim
7e54474111 Add info about markdown (telegram) 2018-06-06 01:00:00 +02:00
Wim
e307069d62 Ignore messages from ourself. (sshchat) Closes #439 2018-06-06 00:51:42 +02:00
ValdikSS
91db63294c Add message correction support for XMPP (#437)
It works worse than it could be, since message correction in XMPP
works differently compared to other messengers. XMPP replaces old
message with old ID with new message with new ID. Matterbridge
remembers only old ID, that's why you can edit a message from
XMPP to the gateway only once.

Edited messages from other networks to XMPP are handled correctly
though.
2018-05-29 23:29:51 +02:00
Wim
fd04e08c9c Update vendor matterbridge/go-xmpp 2018-05-29 23:28:19 +02:00
Yuval Langer
6576409d60 Prevent white or black color codes (irc) (#434) 2018-05-29 22:52:01 +02:00
Patrick Connolly
045cb2058c Fix regexp in replaceMention (slack). (#435) 2018-05-29 22:49:10 +02:00
Wim
d03afc12fd Update changelog 2018-05-27 23:02:32 +02:00
Wim
48799a3cff Bump version 2018-05-27 22:46:30 +02:00
Wim
dba259e9f1 Release v1.10.1 2018-05-27 22:45:37 +02:00
Wim
07885f5810 Fix iconurl regression (mattermost,slack,rocketchat). Closes #430 2018-05-27 22:30:17 +02:00
Wim
696c518550 Add error message about webhook (slack) 2018-05-27 22:14:31 +02:00
Wim
411ef2691c Use uuid instead of userid. Fixes #429 2018-05-27 21:50:00 +02:00
Wim
fc6074ea9f Add vendor github.com/rs/xid 2018-05-27 21:48:57 +02:00
Wim
ab1670e2ce Update sponsor image 2018-05-26 14:09:07 +02:00
Wim
9142a33bbf Add sponsor and zulip to README 2018-05-26 14:01:01 +02:00
OyyoDams
f6eefa4ecc Fix issue #432 - Avatar spoofing from Slack to Discord with uppercase in nick doesn't work (#433) 2018-05-26 13:25:26 +02:00
Kazuhiro NISHIYAMA
f1db166ac4 Fix format string bug (irc) (#428) 2018-05-18 21:45:39 +02:00
Yuval Langer
887c2bc56d End IRC username formatting with a total formatting reset (irc) (#425)
* Add zero padding to the color code

* Change color ending into total formatting reset
2018-05-18 21:33:37 +02:00
Yuval Langer
f0738a93c3 [WIP] Colorize username sent to IRC using its crc32 IEEE checksum (#423)
* Colorize username sent to IRC using its crc32 IEEE checksum

* Add `ColorNicks` configuration variable

* Add `ColorNicks` setting
2018-05-11 23:02:43 +02:00
Wim
75381c2c6e Add support for CJK to/from utf-8 (irc). #400 2018-05-11 21:55:53 +02:00
Wim
bf0b9959d1 Add vendor github.com/dfordsoft/golib/ic 2018-05-11 21:54:32 +02:00
Wim
406a54b597 Add QuoteFormat option (telegram). Closes #413 2018-05-11 20:59:15 +02:00
ValdikSS
be04d1a862 Send attached files to XMPP in different message with OOB data and without body (#421)
Conversations can't show inline pictures if there's anything besides URL in the message body.
Workaround this issue by sending one usual message and one message with OOB data and without message body.
The second message should not be shown in the clients without OOB support, so the user won't see the empty message.
2018-05-09 23:04:10 +02:00
Wim
85b2d5a124 Update vendor lrstanley/girc 2018-05-09 22:50:44 +02:00
Wim
521a7ed7b0 Update vendor lrstanley/girc 2018-05-09 22:48:39 +02:00
Wim
529b188164 Update vendor go-telegram-bot-api/telegram-bot-api 2018-05-09 22:46:10 +02:00
Wim
8d307d8134 Update vendor matterbridge/go-xmpp 2018-05-09 22:38:17 +02:00
Wim
8c675b52bc Add zulipchat badge 2018-05-07 22:33:29 +02:00
Wim
aa51aa2aa0 Bump version 2018-05-07 22:19:05 +02:00
Wim
86865c6da5 Release v1.10.0 2018-05-07 22:07:17 +02:00
Wim
45296100df Add initial zulip support 2018-05-07 21:35:48 +02:00
Wim
1605fbc012 Add vendor matterbridge/gozulipbot 2018-05-07 21:06:25 +02:00
Wim
c6c92e273d Use only alphanumeric for file uploads to mediaserver. Closes #416 2018-05-06 20:32:09 +02:00
Wim
467b373c43 Fix crash on invalid filenames 2018-05-06 20:14:16 +02:00
Wim
72ce7f06e9 Handle file comment better 2018-05-06 16:57:59 +02:00
Wim
346a7284f7 Handle file uploads to mediaserver (steam) 2018-05-06 16:32:24 +02:00
Wim
ee4ac67081 Fix possible nil when using channels (telegram). #410 2018-05-05 23:15:50 +02:00
Wim
5a93d14d75 Update issue templates 2018-05-05 18:04:03 +02:00
Wim
96a47a60ad Add support for reloading all settings automatically after changing config except connection and gateway configuration. Closes #373 2018-05-01 22:23:37 +02:00
Wim
b24a47ad7f Handle channel posts correctly (telegram) 2018-04-29 22:31:11 +02:00
Wim
cd1fd1bb7c Fix panic (telegram). Closes #410 2018-04-29 15:46:40 +02:00
Wim
d44df7b6e6 Fix alignment 2018-04-25 22:21:16 +02:00
Wim
9d1ac0c84b Add image to repo. Make more clear that mattermost is not required to run matterbridge 2018-04-25 22:20:06 +02:00
Jerry Heiselman
76af9cba5a Properly set Slack user who initiated slash command (#394)
* Properly set Slack user who initiated slash command
2018-04-25 21:27:34 +02:00
Wim
b69fc30902 Fix regression in ReplaceMessages and ReplaceNicks. Closes #407 2018-04-21 23:26:39 +02:00
Wim
c3174f4de9 Update GetFileLinks to API_V4 2018-04-21 20:49:44 +02:00
Wim
99ce68e9ba Use username if bot name is Slack API Tester (slack) 2018-04-20 01:01:45 +02:00
Wim
0cf73673a9 Bump version 2018-04-20 00:39:57 +02:00
Wim
08f442dc7b Release v1.9.1 2018-04-20 00:32:11 +02:00
Wim
8a8b95228c Remove message newline (telegram). #399 2018-04-19 22:05:00 +02:00
Wim
31a752fa21 Add missing import 2018-04-19 13:04:12 +02:00
Wim
a83831e68d Remove empty newlines from messages (telegram) #399 2018-04-19 12:53:49 +02:00
ValdikSS
a12a8d4fe2 Send mediaserver link to Discord in Webhook mode (discord) (#405) 2018-04-17 23:52:48 +02:00
Wim
e57f3a7e6c Add QuoteDisable option (telegram). Closes #399 2018-04-17 23:26:41 +02:00
Wim
68fbed9281 Make our callbackid more unique. Fixes issue with running multiple matterbridge on the same channel (slack,mattermost) 2018-04-13 22:01:03 +02:00
Wim
8bfaa007d5 Add UpdateStatus function 2018-04-01 22:53:12 +02:00
Jerry Heiselman
76360f89c1 Strip markdown URLs with blank text (slack) (#392) 2018-03-22 22:28:27 +01:00
Wim
d525230abd Fix bintray build
See https://github.com/travis-ci/travis-ci/issues/9314
2018-03-17 23:13:27 +01:00
Wim
b4aa637d41 Add channel debug (discord) 2018-03-17 22:56:58 +01:00
Wim
7c4334d0de Remove unused import 2018-03-17 22:54:54 +01:00
Wim
062be8d7c9 Revert #378 2018-03-17 18:02:00 +01:00
Alec WM
db25ee59c5 Print list of valid team names when team not found (#390) 2018-03-15 20:50:32 +01:00
Wim
4b0bc6d0bf Release v1.9.0 2018-03-12 23:09:16 +01:00
Wim
8c0b04b995 Ignore restricted_acton on channel join (slack). Closes #387 2018-03-12 21:14:13 +01:00
Wim
e5989adf92 Add support for NoSendJoinPart. Closes #382 2018-03-06 21:35:47 +01:00
Wim
9e5da2f9d7 Fix regression on empty text with files attached 2018-03-06 21:30:59 +01:00
Wim
a284a228a3 Get the correct config values (gateway) 2018-03-06 21:19:00 +01:00
Wim
2133e0d1be Use default values part 2 (irc) 2018-03-06 20:51:02 +01:00
Wim
a6f37f1d61 Use default values (irc) 2018-03-06 20:41:34 +01:00
Wim
9de9151826 Fix panic on sending messages between reconnects (irc). Closes #385 2018-03-05 22:50:38 +01:00
Wim
fdd5ada98c Fix panic on empty config. Closes #386 2018-03-05 22:23:01 +01:00
Wim
80fcf18e24 Remove debug messsage (mattermost) 2018-03-05 22:22:20 +01:00
Wim
ab94b5ca7a Update regex for usergroup matching. Closes #379 2018-03-05 20:56:33 +01:00
Wim
8d2ce56c37 Fix regression (slack). Closes #384 2018-03-05 20:19:43 +01:00
Wim
1ec324354b Fix empty messages (telegram) 2018-03-05 00:43:59 +01:00
Wim
16be6601c8 Fix incorrect skipmessage (xmpp) 2018-03-05 00:36:54 +01:00
Wim
98027446c8 Fix tests and make megacheck happy 2018-03-05 00:30:46 +01:00
Wim
f2f1d874e1 Use viper (github.com/spf13/viper) for configuration 2018-03-04 23:52:14 +01:00
Wim
25a72113b1 Add vendor files for spf13/viper 2018-03-04 23:46:13 +01:00
Wim
79c4ad5015 Remove unused function 2018-03-03 11:08:39 +01:00
Wim
e24f1c7c87 Use replaceVariable for usergroups (slack) #379 2018-03-02 22:32:27 +01:00
Wim
dbf8a326d5 Escape html on username (telegram). Closes #378 2018-02-28 23:25:00 +01:00
Wim
0bc9c70c66 Add usergroup support (slack). Closes #379 2018-02-28 22:54:47 +01:00
Wim
594d2155e3 Improve debug messages 2018-02-28 22:23:29 +01:00
Wim
20dbd71306 Make megacheck happy 2018-02-27 23:38:36 +01:00
Wim
6a727b9723 Use our own version of go-xmpp with debug output to logrus 2018-02-27 23:22:12 +01:00
Wim
02a5bc096f Do some small cleanups 2018-02-27 23:22:12 +01:00
Wim
2110db6f0c Add environment override back 2018-02-27 23:22:12 +01:00
Wim
2bac867382 Refactor using factory 2018-02-27 23:22:12 +01:00
Wim
5fbd8a3be0 Refactor xmpp 2018-02-27 23:22:11 +01:00
Wim
ad6440b603 Refactor telegram 2018-02-27 23:22:10 +01:00
Wim
064b6a915f Small fixes to irc 2018-02-27 23:22:10 +01:00
Wim
1578ebb0e2 Refactor slack 2018-02-27 23:22:10 +01:00
Wim
73525a4bbc Make gometalinter happier 2018-02-27 23:22:10 +01:00
Wim
d62f49d1fc Skip events for webhook 2018-02-27 23:22:10 +01:00
Wim
63b88e77f2 Refactor matrix 2018-02-27 23:22:10 +01:00
Wim
3d8f15c20b Refactor discord 2018-02-27 23:22:09 +01:00
Wim
cac5d56d60 Refactor gitter 2018-02-27 23:22:09 +01:00
Wim
bd2a672c14 Refactor mattermost 2018-02-27 23:22:09 +01:00
Wim
82396e73f5 Allow empty messages with file urls (irc) 2018-02-25 00:40:07 +01:00
Wim
ba928b169d Disable go vet for now (travis) 2018-02-23 01:15:32 +01:00
Wim
4fed720f97 Update travis to go 1.10 2018-02-23 00:56:43 +01:00
Wim
78238c85d4 Add share support between slack instances. Closes #369 2018-02-23 00:49:32 +01:00
Wim
4f2ae7b73f Add slack attachment support to matterhook 2018-02-23 00:48:25 +01:00
Wim
f82a9cc7ac Fix Update userlist on join (slack). Closes #372 2018-02-22 23:56:00 +01:00
Wim
cce7624ab8 Update userlist on join (slack). Closes #372 2018-02-22 23:36:22 +01:00
Wim
c5ecd09172 Use always formatted logging when debug is enabled 2018-02-22 22:57:34 +01:00
Wim
7b21c1c2f4 Set event channels to lowercase (irc). Closes #375 2018-02-22 22:51:32 +01:00
Wim
f8714d81f5 Add DebugLevel option (irc) 2018-02-22 18:56:21 +01:00
Wim
8622656005 Add more debug for events (irc) 2018-02-22 18:23:22 +01:00
Wim
52237fadb6 Bump version 2018-02-21 20:54:29 +01:00
Wim
222cccf388 Release v1.8.0 2018-02-21 20:42:26 +01:00
Wim
bab308508e Fix the UseInsecureURL text (telegram). Closes #184 2018-02-21 13:30:38 +01:00
Wim
dedb83c867 Add ssh-chat to README 2018-02-21 01:42:43 +01:00
Wim
723a90cdd6 Exclude gofmt test from travis for now 2018-02-21 01:20:38 +01:00
Wim
67d2398fa8 Make matterclient work with prefixed log 2018-02-21 01:11:41 +01:00
Wim
5f3b6ec007 Disable echo banner and output (api) 2018-02-21 00:49:10 +01:00
Wim
55ab0c12f1 Update vendor labstack/echo 2018-02-21 00:48:10 +01:00
Wim
d1227b5fc9 Use prefixed-formatter for better logging 2018-02-21 00:20:25 +01:00
Wim
6ea368c383 Move Sirupsen => sirupsen 2018-02-20 23:41:09 +01:00
Wim
e92b6de09f Add more debug 2018-02-20 23:36:29 +01:00
Wim
e622587db4 Add label support in RemoteNickFormat 2018-02-20 18:57:46 +01:00
Wim
f2efc06d1f Give api access to whole config.Message (and events). Closes #374 2018-02-20 18:36:44 +01:00
Wim
a2b94452db Add more debug (telegram) 2018-02-20 17:51:23 +01:00
Wim
4c506f7cc3 Use MediaServerDownload instead of MediaServerUpload for avatars 2018-02-20 17:15:54 +01:00
Wim
7886f05e88 Download (and upload) avatar images from mattermost and telegram when mediaserver is configured. Closes #362
An extra avatarMap (cache) is created for mattermost and telegram.
If MediaServerUpload is configured, the avatar images of users are downloaded the first time a
user sends a message.
If this download succeeds a message with EVENT_AVATAR_DOWNLOAD is sent to the originating protocol.
This message also contains a SHA field (in msg.Extra["file"]), if this is not empty, the sha will
be added to the avatarMap. (so we now have a userid-sha cache)

Next time this user sends a message, the MediaServerUpload/sha/userid.png URL will be used as the
avatar field.
2018-02-20 01:15:25 +01:00
Wim
f58be0d1c1 Add SHA to FileInfo 2018-02-15 23:18:58 +01:00
Wim
1152394bc1 Update issue template 2018-02-15 22:35:29 +01:00
Wim
a082b5a590 Remove unused code 2018-02-15 00:07:25 +01:00
Wim
bae9484df2 Use discordgo ContentWithMoreMentionsReplace (discord) 2018-02-14 23:05:50 +01:00
Wim
6f78485878 Fix role replace 2018-02-14 23:05:16 +01:00
Wim
fd0fe3390b Update vendor bwmarrin/discordgo 2018-02-14 22:22:35 +01:00
Wim
2522158127 Add avator to fileinfo 2018-02-14 22:20:27 +01:00
Wim
8be107cecc Fix mattermost API change 2018-02-09 00:11:20 +01:00
Wim
5aab158c0b Update vendor (github.com/mattermost) 2018-02-09 00:11:04 +01:00
tsudoko
1d33e60e36 Truncate messages sent to IRC based on byte count (#368)
* Truncate messages sent to IRC based on byte count

* Avoid unnecessary string allocations
2018-02-08 23:28:33 +01:00
Wim
83c28cb857 Check for a valid WebhookURL (discord). Closes #367 2018-02-07 14:57:38 +01:00
Wim
df5bce27b0 Fix panic on nil messages (telegram). Closes #366 2018-02-07 14:28:48 +01:00
Wim
2b15739b48 Remove double close 2018-02-07 00:05:10 +01:00
Wim
3480c88e90 Do not close body on err. Closes #364 2018-02-07 00:04:02 +01:00
Wim
432cd0f99d Add more parsemode debug (telegram) 2018-02-04 17:55:20 +01:00
Wim
e8b3e9b22d Update readme 2018-02-04 16:07:37 +01:00
Wim
d4a47671ea Add markdown support (telegram). #355 2018-02-03 23:31:21 +01:00
Wim
0bcd1e62f3 Add channel_purpose to ShowTopicChange. Ignore (un)pinned_item (slack). #353 2018-02-03 01:15:57 +01:00
Wim
80822b7fff Send chat notification if media is too big to be re-uploaded to MediaServer. See #359 2018-02-03 01:11:11 +01:00
Wim
78f1011f52 Add support for file comments (slack). Closes #346 2018-02-02 23:16:10 +01:00
Wim
67f6257617 Add ShowTopicChange option. Allow/disable topic change messages (currently only from slack). Closes #353 2018-02-02 21:08:13 +01:00
Wim
169c614489 Download files and reupload to supported bridges (mattermost). Closes #357 2018-02-02 20:23:55 +01:00
ValdikSS
da908c438a Add space between colon and URL for uploaded media (#360) 2018-02-01 17:46:10 +01:00
Wim
9c9c4bf1f9 Fix build 2018-02-01 01:01:25 +01:00
Wim
7764493298 Add comment to file upload from telegram. Show comments on all bridges. Closes #358 2018-02-01 00:41:09 +01:00
Wim
64a20ee61b Add URL to message in webhook if available (mattermost). See #356 2018-01-31 17:35:13 +01:00
Wim
62d1af8c37 Bump version 2018-01-29 12:41:35 +01:00
Wim
0f5274fdf6 Release v1.7.1 2018-01-29 12:35:35 +01:00
ValdikSS
2e2187ebf4 Enable Long Polling for Telegram. Reduces bandwidth consumption. (#350)
Fixes #349.
2018-01-29 12:07:26 +01:00
Wim
762c3350f4 Bump version 2018-01-28 19:48:02 +01:00
Wim
e1a4d7f77e Update readme about REST api projects (matterlink,pycord) 2018-01-28 19:47:48 +01:00
Wim
a7a4554a85 Release v1.7.0 2018-01-28 19:36:02 +01:00
Wim
6bd808ce91 Lowercase irc channels in config. Closes #348 2018-01-28 19:15:13 +01:00
Wim
a5c143bc46 Allow xmpp to receive the extra messages when text is empty. #295 2018-01-27 16:32:38 +01:00
Florent Fayolle
87c9cac756 Use cmosh/alpine-arm to build arm docker images (#347) 2018-01-27 13:49:13 +01:00
Wim
6a047f8722 Print only debug messages when specified (xmpp). Closes #345 2018-01-26 21:54:09 +01:00
Wim
6523494e83 Obey the Gateway value from the json (api). Closes #344 2018-01-21 12:21:55 +01:00
Wim
7c6ce8bb90 Fix xmpp badge, add twitch badge 2018-01-20 23:59:54 +01:00
Wim
dafbfe4021 Add twitch support (irc) to README 2018-01-20 23:38:58 +01:00
Wim
a4d5c94d9b Make edits/delete work for bridges that gets reused. Closes #342 2018-01-20 21:58:59 +01:00
Wim
7119e378a7 Add an extension to images without one (matrix). #331 2018-01-20 18:19:17 +01:00
Wim
e1dc3032c1 Ignore <subject> messages (xmpp). #272 2018-01-14 23:43:34 +01:00
Wim
5de03b8921 Update xmpp 2018-01-14 22:31:45 +01:00
Wim
7631d43c48 Change RemoteNickFormat replacement order. Closes #336 2018-01-14 16:55:32 +01:00
Wim
d0b2ee5c85 Add support for docker arm builds. #328 2018-01-10 00:04:24 +01:00
Wim
8830a5a1df Fix possible panics (matrix). Closes #333 2018-01-09 23:25:58 +01:00
Wim
ee87626a93 Update for 1.6.3 2018-01-09 00:13:46 +01:00
Wim
9f15d38c1c Use upstream again (slack) 2018-01-08 22:41:58 +01:00
Wim
4a96a977c0 Update vendor (slack) 2018-01-08 22:41:38 +01:00
Anssi Kolehmainen
9a95293bdf Convert received IRC channel names to lowercase. Fixes #329 (#330) 2018-01-06 22:55:03 +01:00
Wim
0b3a06d263 Log ConnectionErrorEvent (slack) 2018-01-03 14:06:28 +01:00
Wim
9a6249c4f5 Increase debug logging (slack) 2018-01-02 14:39:27 +01:00
Wim
50bd51e461 Use a better check to join channel (slack) 2018-01-02 14:31:44 +01:00
Wim
04f8013314 Bump version 2018-01-01 15:13:05 +01:00
Wim
a0aaf0057a Update for 1.6.2 2018-01-01 15:12:32 +01:00
Wim
8e78b3e6be Fix regression in mattermost bridge (mattermost). Closes #327 2018-01-01 14:20:16 +01:00
Wim
57a503818d Release v1.6.1 2017-12-26 19:22:50 +01:00
Wim
25d2ff3e9b Fix regression. Closes #323 2017-12-26 19:13:27 +01:00
Wim
31902d3e57 Add support for deleting messages from/to matrix (matrix). Closes #320 2017-12-25 00:55:39 +01:00
Wim
16f3fa6bae Vendor github.com/matterbridge/gomatrix 2017-12-25 00:54:39 +01:00
Wim
1f706673cf Bump version 2017-12-23 00:53:12 +01:00
Wim
fac5f69ad2 Release v1.6.0 2017-12-23 00:28:01 +01:00
Wim
97c944bb63 Add RejoinDelay option. Delay to rejoin after channel kick (irc). Closes #322 2017-12-23 00:11:30 +01:00
Wim
d0c4fe78ee Allow specifying maximum download size of media using MediaDownloadSize (slack,telegram,matrix) 2017-12-19 23:44:13 +01:00
Wim
265457b451 Refactor and add MediaDownloadSize to General 2017-12-19 23:15:03 +01:00
Wim
4a4a29c9f6 Fix panic (matrix). Closes #316 2017-12-11 12:25:28 +01:00
Wim
0a91b9e1c9 Fix incorrect forward from text line (telegram) 2017-12-11 12:15:26 +01:00
Wim
f56163295c Remove unreachable code (api) 2017-12-10 15:20:17 +01:00
Wim
d1c87c068b Also use HTML in edited messages (telegram). Closes #315 2017-12-10 15:16:17 +01:00
Wim
fa20761110 Add support for Audio/Voice files (telegram). Closes #314 2017-12-10 15:08:23 +01:00
Wim
e4a0e0a0e9 Add support for forwarded messages. Closes #313 2017-12-10 14:52:29 +01:00
Wim
d30ae19e2a Add (simple, one listener) long-polling support (api). Closes #307 2017-12-07 23:48:44 +01:00
Wim
5c919e6bff Update vendor labstack/echo 2017-12-07 23:00:56 +01:00
Wim
434393d1c3 Update README 2017-12-07 22:30:17 +01:00
Wim
af9aa5d7cb Update changelog 2017-12-07 22:27:17 +01:00
Wim
05eb75442a Split on UTF-8 for MessageSplit (irc). Closes #308 2017-12-07 22:21:54 +01:00
Wim
3496ed0c7e Fix irc ACTION regression (irc). Closes #306 2017-12-07 22:07:45 +01:00
Wim
1b89604c7a Bump version 2017-12-03 01:29:54 +01:00
Wim
67a9d133e9 Add quick & dirty sshchat support (https://github.com/shazow/ssh-chat) 2017-12-03 01:29:25 +01:00
Wim
ed9118b346 Add sshchat dependencies in vendor 2017-12-03 01:24:05 +01:00
Wim
59e55cfbd5 Release v1.5.0 2017-12-03 00:01:05 +01:00
Wim
788d3b32ac Update vendor lrstanley/girc and readme 2017-12-02 23:58:02 +01:00
Wim
1d414cf2fd Allow ^ in nick (irc). Closes #305 2017-11-30 00:28:17 +01:00
Wim
cc3c168162 Update vendor lrstanley/girc 2017-11-30 00:27:31 +01:00
Wim
1ee6837f0e Update changelog 2017-11-24 23:56:22 +01:00
Wim
27dcea7c5b Update documentation about ReplaceMessages and ReplaceNicks 2017-11-24 23:45:00 +01:00
Wim
dcda7f7b8c Add documentation about MediaServerUpload and MediaServerDownload 2017-11-24 23:35:25 +01:00
Wim
e0cbb69a4f Add MessageSplit option to split messages on MessageLength (irc). Closes #281 2017-11-24 23:29:00 +01:00
Wim
7ec95f786d Use mediaserver urls for irc,gitter and xmpp 2017-11-24 22:55:24 +01:00
Wim
1efe40add5 Add initial support for an external mediaserver. #278
Add 2 extra options `MediaServerUpload` and `MediaServerDownload`, where
the URL for upload and download can be specified.

See https://github.com/42wim/matterbridge/wiki/Mediaserver-setup-%5Badvanced%5D
for an example with caddy
2017-11-24 22:36:19 +01:00
Wim
cbd73ee313 Add support for uploaded images/video/files (matrix) 2017-11-22 00:28:40 +01:00
Wim
34227a7a39 Add support for uploading images/video (matrix). Closes #302 2017-11-21 23:50:27 +01:00
Wim
71cb9b2d1d Update vendor github.com/matrix-org/gomatrix 2017-11-21 23:48:39 +01:00
Wim
cd4c9b194f Add support for ReplaceNicks using regexp to replace nicks. Closes #269 2017-11-20 23:27:27 +01:00
Wim
98762a0235 Add webp extension to stickers if necessary (telegram) 2017-11-20 22:12:51 +01:00
Wim
2fd1fd9573 Break when re-login fails (mattermost) 2017-11-16 20:19:52 +01:00
Wim
aff3964078 Add support for ReplaceMessages using regexp to replace messages. #269 2017-11-15 23:33:00 +01:00
Wim
2778580397 Bump version 2017-11-13 20:13:32 +01:00
Wim
962062fe44 Release v1.4.1 2017-11-13 20:10:04 +01:00
Wim
0578b21270 Fix message sending (slack) 2017-11-13 19:50:18 +01:00
Wim
36a800c3f5 Add support for comments from slack file uploads (slack) 2017-11-13 00:20:31 +01:00
Wim
6d21f84187 Add extension to sticker/video/photo (telegram) 2017-11-12 22:04:35 +01:00
Wim
f1e9833310 Do not ignore empty messages with files for bridges that support it 2017-11-12 18:34:16 +01:00
Wim
46f5acc4f9 Add the download actually to the message (telegram) 2017-11-12 18:09:38 +01:00
Wim
95d4dcaeb3 Add more debug info (telegram) 2017-11-12 17:49:10 +01:00
Wim
64c542e614 Add more debug info (telegram) 2017-11-12 17:46:44 +01:00
Wim
13d081ea80 Fix document bug (telegram) 2017-11-12 17:15:53 +01:00
Wim
c0f9d86287 Fix telegram photo/document input handling (telegram) 2017-11-12 11:46:32 +01:00
Wim
bcdecdaa73 Fix strict user handling of girc (irc). Closes #298 2017-11-11 23:16:58 +01:00
Wim
daac3ebca2 Release v1.4.0 2017-11-08 23:22:31 +01:00
Wim
639f9cf966 Update vendor/github.com/mattn/go-xmpp 2017-11-08 23:04:23 +01:00
Wim
4fc48b5aa4 Fix panic on empty params 2017-11-08 22:55:48 +01:00
Wim
307ff77b42 Add ServerName to TLSConfig 2017-11-08 22:55:37 +01:00
Wim
9b500bc5f7 Replace sorcix/irc and go-ircevent with girc 2017-11-08 22:54:31 +01:00
Wim
e313154134 Vendor github.com/lrstanley/girc 2017-11-08 22:47:18 +01:00
rrigby
27e94c438d Add support for bridging to individual steam chats. (steam) (#294) 2017-11-08 00:36:20 +01:00
Patrick Connolly
58392876df Use room.URI instead of room.Name. (gitter) (#293) 2017-11-08 00:35:08 +01:00
Wim
115c4b1aa7 Fix missing arg for Errorf 2017-11-04 15:01:03 +01:00
Wim
ba5649d259 Add helper 2017-11-04 14:55:25 +01:00
Wim
1b30575510 Download files from telegram and reupload to supported bridges (telegram). #278 2017-11-04 14:50:17 +01:00
Wim
7dbebd3ea7 Show error message when file upload fails (discord) 2017-11-04 14:47:14 +01:00
Wim
6f18790352 Add support to upload files to slack, from bridges with private urls like slack/mattermost/telegram. (slack) 2017-11-03 23:10:16 +01:00
heinrich5991
d1e04a2ece Add systemd service file (#291)
Supersedes #176.
2017-11-03 20:42:50 +01:00
Patrick Connolly
bea0bbd0c2 Allow slack messages with variables (eg. @here) to be formatted correctly. (slack) (#288) 2017-11-03 20:32:28 +01:00
Wim
0530503ef2 Make megacheck happy again 2017-11-03 20:13:58 +01:00
Wim
d1e8ff814b Add support to upload files to discord, from bridges with private urls like slack/mattermost/telegram. (discord) 2017-11-03 00:05:10 +01:00
Patrick Connolly
4f8ae761a2 Resolve slack channel to human-readable name. (slack) (#282) 2017-11-02 21:21:46 +01:00
Wim
b530e92834 Use DisplayName instead of deprecated username (slack). Closes #276 2017-11-02 17:11:42 +01:00
Wim
b2a6777995 Use matterbridge vendored slack 2017-11-02 17:09:34 +01:00
Wim
b461fc5e40 Add support for DEBUG=1 envvar to enable debug. Closes #283 2017-10-28 14:50:35 +02:00
Wim
b7a8c6b60f Try again to strip colors correct. #286 2017-10-28 14:28:15 +02:00
Wim
41aa8ad799 Add StripNick option, only allow alphanumerical nicks. Closes #285 2017-10-27 00:07:33 +02:00
Wim
7973baedd0 Bump version 2017-10-26 23:05:14 +02:00
Wim
299b71d982 Strip irc colors correct, strip also ctrl chars (irc). Closes #286 2017-10-26 23:04:44 +02:00
Patrick Connolly
76aafe1fa8 Allowed Slack bridge to extract simpler link format. (#287)
Links sometimes exist without bar delimiters.

See: https://api.slack.com/docs/message-formatting#linking_to_urls
2017-10-26 21:58:43 +02:00
Patrick Connolly
95a0229aaf Fix outdated sample config on slack channel format. (#280) 2017-10-20 21:01:11 +02:00
Patrick Connolly
915a8fbad7 Make [general] settings default, not total override (specifically RemoteNickFormat) (#279)
* Use general settings as default, that specific protocols override.

* Fixed tab formatting.

* Clarified override precedence of [general] config.
2017-10-20 20:58:39 +02:00
Wim
d4d7fef313 Release v1.3.1 2017-10-15 22:57:14 +02:00
Wim
4e1dc9f885 Use bot username if specified (slack). Closes #273 2017-10-12 20:33:37 +02:00
Wim
155ae80d22 Support mattermost 4.x as api4 should be stable (mattermost) 2017-09-28 22:34:44 +02:00
Wim
c7e336efd9 Bump version 2017-09-28 21:57:59 +02:00
Wim
ac3c65a0cc Release v1.3.0 2017-09-27 22:35:07 +02:00
Wim
df74df475b Update vendor 2017-09-25 21:14:08 +02:00
Wim
a61e2db7cb Backoff for 60 seconds when reconnecting too fast 2017-09-25 21:12:23 +02:00
Wim
7aabe12acf Fix loop, make megacheck happy 2017-09-21 23:15:04 +02:00
Wim
c4b75e5754 Download files from slack and reupload to mattermost (slack/mattermost). Closes #255
Refactor message.Extra to a map[string][]interface{} to have a bit more flexibility
for stuffing extra stuff.

For attached files from slack, files < 1MB size get downloaded (in memory), and get
put into Extra["file"][]config.FileInfo (containing a pointer to the buffer and
the filename). This is not async so slack channels with lots of attached files
may suffer a slowdown. (the download timeout is set at 5 seconds).
2017-09-21 22:35:21 +02:00
Wim
6a7adb20a8 Add functions to upload files 2017-09-21 21:27:44 +02:00
Wim
b49fb2b69c Add support for Quakenet auth (irc). Closes #263 2017-09-20 22:47:26 +02:00
Wim
4bda29cb38 Try quoting previous messsage (telegram). #237 2017-09-19 23:58:05 +02:00
Wim
5f14141ec9 Try to not forward slack unfurls. Closes #266 2017-09-19 22:33:26 +02:00
Wim
c088e45d85 Add more debug info (telegram) 2017-09-19 21:41:35 +02:00
Wim
d59c51a94b Remove unnecessary check, make megacheck happy 2017-09-19 00:04:27 +02:00
Wim
47b7fae61b Fix loop from webhook by adding matterbridge prop (mattermost). Closes #261 2017-09-18 23:53:30 +02:00
Wim
1a40b0c1e9 Relay attachments from mattermost to slack (slack). Closes #260 2017-09-18 23:51:27 +02:00
Wim
27d886826c Allow empty message if we have a slack attachment 2017-09-18 23:44:16 +02:00
Wim
18981cb636 Add props 2017-09-18 23:43:21 +02:00
Wim
ffa8f65aa8 Bump version 2017-09-18 21:18:59 +02:00
Wim
82588b00c5 Use override username if specified (mattermost). #260 2017-09-18 21:18:31 +02:00
Wim
603449e850 Update readme 2017-09-11 23:49:15 +02:00
Wim
248d88c849 Release v1.2.0 2017-09-11 23:41:13 +02:00
Wim
d19535fa21 Update vendor (nlopes/slack) 2017-09-11 23:33:58 +02:00
Wim
49204cafcc Update vendor (bwmarrin/discordgo) apiv6 2017-09-11 23:23:54 +02:00
Wim
812db2d267 Bump version 2017-09-11 23:17:33 +02:00
Wim
14490bea9f Add partial support for deleted messages (telegram) 2017-09-11 23:12:33 +02:00
Wim
0352970051 Update vendor (go-telegram-bot-api/telegram-bot-api) 2017-09-11 23:11:48 +02:00
Wim
ed01820722 Add support for deleting messages across bridges.
Currently fully support mattermost,slack and discord.
Message deleted on the bridge or received from other bridges will be
deleted.

Partially support for Gitter.
Gitter bridge will delete messages received from other bridges.
But if you delete a message on gitter, this deletion will not be sent to
other bridges (this is a gitter API limitation, it doesn't propogate edits
or deletes via the API)
2017-09-11 22:45:15 +02:00
Wim
90a61f15cc Do not break messages on newline (slack). Closes #258 2017-09-10 18:19:33 +02:00
Wim
86cd7f1ba6 Add UpdateUserNick 2017-09-10 16:33:29 +02:00
Wim
d6ee55e35f Release v1.1.2 2017-09-09 17:06:40 +02:00
Wim
aef64eec32 Update changelog 2017-09-09 17:04:01 +02:00
Wim
c4193d5ccd Add 4.2 support (mattermost) 2017-09-09 17:00:26 +02:00
Wim
0c94186818 Add darwin-amd64 to nightly builds 2017-09-09 14:42:45 +02:00
Wim
9039720013 Send images when text is empty regression. (mattermost). Closes #254 2017-09-08 00:16:17 +02:00
Wim
a3470f8aec Send first message after connect (slack). Closes #252 2017-09-07 23:47:23 +02:00
Wim
01badde21d Add message debugging (gitter) 2017-09-07 20:35:12 +02:00
Ryan Mulligan
a37b232dd9 remove comment about useAPI in sample configuration (#251) 2017-09-04 15:16:58 +02:00
Ryan Mulligan
579ee48385 remove useAPI from sample configuration (#250) 2017-09-04 15:16:29 +02:00
Wim
dd985d1dad Fix sending direct messages with APIv4 2017-09-04 14:24:22 +02:00
Wim
d2caea70a2 Release v1.1.1 2017-09-04 13:43:30 +02:00
Wim
21143cf5ee Fix public links (mattermost) 2017-09-04 12:50:42 +02:00
Wim
dc2aed698d Release v1.1.0 2017-09-01 20:20:46 +02:00
Wim
37c350f19f Convert utf-8 back to charset (irc). #247 2017-08-30 20:59:54 +02:00
Wim
9e03fcf162 Fix private channel joining bug (mattermost). Closes #248 2017-08-30 14:01:17 +02:00
Wim
8d4521c1df Update changelog 2017-08-29 23:45:39 +02:00
Wim
9226252336 Replace mentions from other bridges. (slack). Closes #233 2017-08-29 23:34:50 +02:00
Wim
f4fb83e787 Use the detected charset (irc) 2017-08-29 21:35:36 +02:00
Wim
e7fcb25107 Add a charset option (irc). Closes #247 2017-08-29 21:31:03 +02:00
Wim
5a85258f74 Update travis to go 1.9 2017-08-29 20:34:32 +02:00
Wim
2f7df2df43 Do not add messages without ID to cache 2017-08-29 20:28:44 +02:00
Wim
ad3a753718 Remove debug message 2017-08-28 23:07:13 +02:00
Wim
e45c551880 Add support for editing messages. Remove ZWSP as loopcheck (gitter) 2017-08-28 23:07:12 +02:00
Wim
e59d338d4e Use github.com/42wim/go-gitter for now 2017-08-28 23:07:11 +02:00
Wim
7a86044f7a Add support for editing messages (telegram) 2017-08-28 23:07:03 +02:00
Wim
8b98f605bc Add support for editing messages (slack) 2017-08-28 20:29:02 +02:00
Wim
7c773ebae0 Add support for editing messages across bridges. Currently mattermost/discord.
Our Message type has an extra ID field which contains the message ID of the specific bridge.
The Send() function has been modified to return a msg ID (after the message to that specific
bridge has been created).

There is a lru cache of 5000 entries (message IDs). All in memory, so editing messages
will only work for messages the bot has seen.

Currently we go out from the idea that every message ID is unique, so we don't keep
the ID separate for each bridge. (we do for each gateway though)

If there's a new message from a bridge, we put that message ID in the LRU cache as key
and the []*BrMsgID as value (this slice contains the message ID's of each bridge that
received the new message)

If there's a new message and this message ID already exists in the cache, it must be
an updated message. The value from the cache gets checked for each bridge and if there
is a message ID for this bridge, the ID will be added to the Message{} sent to that
bridge. If the bridge sees that the ID isn't empty, it'll know it has to update the
message with that specific ID instead of creating a new message.
2017-08-28 00:33:17 +02:00
Wim
e84417430d Update PostMessage to also return and error. Add EditMessage function 2017-08-28 00:32:56 +02:00
Wim
5a8d7b5f6d Modify Send() to return also a message id 2017-08-27 22:59:37 +02:00
Wim
cfb8107138 Relay notices (matrix). Closes #243 2017-08-27 01:01:35 +02:00
Wim
43bd779fb7 Handle leave/join events (slack). Closes #246 2017-08-27 00:00:02 +02:00
Wim
7f9a400776 Add support for personal access tokens (mattermost)
* https://docs.mattermost.com/developer/personal-access-tokens.html
2017-08-23 22:49:42 +02:00
Wim
ce1c5873ac Make megacheck happy 2017-08-17 00:00:41 +02:00
Wim
85ff1995fd Use mattermost v4 api (drops support for mattermost < 3.8) 2017-08-16 23:41:35 +02:00
Wim
b963f83c6a Update mattermost vendor (3.7 => 4.1) 2017-08-16 23:37:37 +02:00
Wim
f6297ebbb0 Bump version 2017-08-16 23:28:11 +02:00
Wim
a5259f56c5 Release v1.0.1 2017-08-16 22:25:44 +02:00
Wim
3f75ed9c18 Add 4.1 support (mattermost) 2017-08-16 22:02:13 +02:00
Thracky
49ece51167 Add new file_ids parameter for Mattermost outgoing webhook (#240)
* Added file_id parameter for outgoing webhook

* Typo in the new fileids field name
2017-08-16 21:27:17 +02:00
Wim
e77c3eb20a Swap token/id. Also check for default webhookURL in isWebhookID (discord) 2017-08-12 16:30:00 +02:00
Wim
59b2a5f8d0 Bump version 2017-08-12 14:54:19 +02:00
Wim
28710d0bc7 Allow a webhookurl per channel (discord). #239 2017-08-12 14:51:41 +02:00
Wim
ad4d461606 Release v1.0.0 2017-08-05 15:50:21 +02:00
anon724
67905089ba Add UseUserName option (discord) (#234) 2017-08-01 18:18:55 +02:00
Wim
f2483af561 Do not modify username in action (discord) 2017-07-31 21:37:19 +02:00
Wim
c28b87641e Release v1.0.0-rc1 2017-07-30 18:05:27 +02:00
Wim
f8e6a69d6e Add action support for slack,mattermost,irc,gitter,matrix,xmpp,discord. #199 2017-07-30 17:48:23 +02:00
Wim
54216cec4b Remove unused function 2017-07-30 16:12:33 +02:00
Wim
12989bbd99 Handle same account in multiple gateways better 2017-07-30 16:09:05 +02:00
Wim
38d09dba2e Update vendor (go-irc) 2017-07-28 14:26:26 +02:00
Wim
fafd0c68e9 Update readme 2017-07-26 22:37:48 +02:00
Wim
41195c8e48 Fix double posting of edited messages by using lru cache (mattermost) 2017-07-25 23:57:27 +02:00
Wim
a97804548e Add vendor (github.com/hashicorp/golang-lru) 2017-07-25 23:56:12 +02:00
Wim
ba653c0841 Ignore edited messages with reactions (mattermost) 2017-07-25 23:19:50 +02:00
Wim
5b191f78a0 Update tests with gofmt 2017-07-25 20:20:55 +02:00
Wim
83ef61287e Refactor. Add tests 2017-07-25 20:11:52 +02:00
Wim
3527e09bc5 Update vendor 2017-07-25 20:10:40 +02:00
Wim
ddc5b3268f Add screenshots 2017-07-24 17:36:57 +02:00
Wim
22307b1934 Release v0.16.3 2017-07-24 16:20:34 +02:00
Wim
bd97357f8d Disable message from other bots when using webhooks (slack) 2017-07-22 20:03:40 +02:00
Wim
10dab1366e Return better error messages on mattermost connect 2017-07-22 18:13:13 +02:00
Wim
52fc94c1fe Remove old files. Update readme 2017-07-22 17:50:34 +02:00
Wim
c1c7961dd6 Fix in/out logic. Closes #224 2017-07-22 17:25:22 +02:00
Wim
d3eef051b1 Fix message modification 2017-07-21 17:04:03 +02:00
Wim
57654df81e Bump version 2017-07-20 23:17:02 +02:00
Wim
0f791d7a9a Handle reconnections better (xmpp). Closes #222 2017-07-20 23:16:43 +02:00
Wim
58779e0d65 Update readme 2017-07-19 00:31:26 +02:00
Wim
4ac361b5fd Add xmpp badge 2017-07-19 00:29:46 +02:00
Wim
1e2f27c061 Release v0.16.2 2017-07-18 23:48:00 +02:00
Wim
0302e4da82 Fix webhookurl/webhookbindaddress panic (mattermost). Closes #221 2017-07-17 23:10:32 +02:00
Wim
dc8743e0c0 Tag messages we send ourself using CallbackID hack (slack). Closes #219 2017-07-17 21:28:31 +02:00
Jerry Heiselman
cc5ce3d5ae Suppress parent message when child message is received (slack) (#218)
* Suppress parent message when child message is received

When a thread is started in Slack and a user makes a comment on the thread, matterbridge sends the original parent message again on each child comment. This change suppresses that.

* Update slack.go

Moved determination of ThreadTimestamp to handleSlackClient so the MMMessage struct doesn't need to be modified

* Ran 'go fmt'
2017-07-17 18:33:28 +02:00
Wim
caaf6f3012 Fix stable/dev shields 2017-07-16 23:14:18 +02:00
Wim
c5de8fd1cc Fix readme 2017-07-16 22:57:45 +02:00
Wim
c9f23869e3 Add stable/devel shields 2017-07-16 22:56:26 +02:00
Wim
61208c0e35 Update readme 2017-07-16 22:27:53 +02:00
Wim
dcffc74255 Set correct binaries path 2017-07-16 22:15:06 +02:00
Wim
23e23be1a6 Try travis bintray integration (6) 2017-07-16 22:06:33 +02:00
Wim
710427248a Try travis bintray integration (5) 2017-07-16 22:02:46 +02:00
Wim
a868042de2 Try travis bintray integration (4) 2017-07-16 21:43:19 +02:00
Wim
15296cd8b4 Try travis bintray integration (3) 2017-07-16 21:32:41 +02:00
Wim
717023245f Try travis bintray integration (2) 2017-07-16 21:05:29 +02:00
Wim
320be5bffa Try travis bintray integration 2017-07-16 20:57:32 +02:00
Wim
778abea2d9 Add support for fallback/text in attachments (slack) 2017-07-16 18:08:26 +02:00
Wim
835a1ac3a6 Update travis for crossplatform 2017-07-16 17:15:00 +02:00
Wim
20a7ef33f1 Make sure bot doesn't loop now we relay bot messages (slack) 2017-07-16 15:03:46 +02:00
Wim
e72612c7ff Bump version 2017-07-16 15:02:15 +02:00
Wim
04e0f001b0 Fix discordgo api changes 2017-07-16 14:39:00 +02:00
Wim
5db24aa901 Update vendor (bwmarrin/discordgo) 2017-07-16 14:38:45 +02:00
Wim
aec5e3d77b Update vendor (nlopes/slack) 2017-07-16 14:29:46 +02:00
Wim
335ddf8db5 Fix lookup bot username (slack). #213 2017-07-16 14:18:33 +02:00
Wim
4abaf2b236 Fix mattermost shield 2017-07-16 00:47:29 +02:00
Wim
183d212431 Add mattermost chat/badge 2017-07-16 00:43:32 +02:00
Wim
e99532fb89 Release v0.16.1 2017-07-15 16:59:57 +02:00
Wim
4aa646f6b0 Use GetFileLinks. Also show links to non-public files (mattermost) 2017-07-15 16:51:10 +02:00
Wim
9dcd51fb80 Refactor connecting logic slack/mattermost. Fixes #216 2017-07-15 16:49:47 +02:00
Wim
6dee988b76 Fix megacheck / go vet issues 2017-07-14 00:35:01 +02:00
Wim
5af40db396 Update travis 2017-07-14 00:28:46 +02:00
Wim
b3553bee7a Add travis 2017-07-13 23:54:07 +02:00
Wim
ac19c94b9f Add GetFileLinks, also get files if public links is disabled 2017-07-12 22:47:30 +02:00
Wim
845f7dc331 Update readme 2017-07-10 22:21:11 +02:00
Wim
2adeae37e1 Update readme 2017-07-10 22:19:51 +02:00
Wim
16eb12b2a0 Bump version 2017-07-10 21:59:17 +02:00
Wim
8411f2aa32 Lookup bot username (slack). #213 2017-07-10 21:58:43 +02:00
Wim
e8acc49cbd Add slack badge / invitation 2017-07-09 18:08:30 +02:00
Wim
4bed073c65 Release v0.16.0 2017-07-09 15:37:59 +02:00
Wim
272735fb26 Add 4.0 support (mattermost) 2017-07-09 15:15:22 +02:00
Wim
b75cf2c189 Replace HTML entities (slack). #215 2017-07-09 14:26:56 +02:00
Wim
1aaa992250 Update acknowledgements 2017-07-09 14:05:17 +02:00
Wim
6256c066f1 Replace :emoji: with unicode chars. #215
Add vendor github.com/peterhellberg/emojilib
2017-07-09 14:00:28 +02:00
Wim
870b89a8f0 Fix embeds (discord). Closes #202 2017-07-09 13:41:46 +02:00
Wim
65ac96913c Update issue template 2017-07-08 12:25:52 +02:00
Wim
480945cb09 Release v0.16.0-rc2 2017-07-07 23:49:34 +02:00
Wim
bfc7130ed8 Try to detect the charset and convert it to utf-8. (irc). Closes #209 #210 2017-07-07 23:39:38 +02:00
Wim
a0938d9386 Add go-charset and chardet to vendor 2017-07-07 23:34:05 +02:00
Wim
2338c69d40 Add UseInsecureURL option (telegram) 2017-07-04 01:35:30 +02:00
Wim
c714501a0e Fix channel id off by 0x18000000000000 (steam) 2017-07-03 22:10:26 +02:00
Wim
a58a3e5000 Optimize StatusLoop. Execute function when specified in OnWsConnect 2017-07-01 23:28:16 +02:00
Wim
ba35212b67 Optimize GetStatus. (from @recht matterircd fork) 2017-07-01 23:05:39 +02:00
Wim
f3e0358de7 Optimize UpdateUsers usage. (from @recht matterircd fork) 2017-07-01 23:02:56 +02:00
Wim
8064744d3a Fix possible panics. (from @recht matterircd fork) 2017-07-01 22:49:06 +02:00
Wim
d261949db2 Don't logout if logging in through token. (from @recht matterircd fork) 2017-07-01 22:41:28 +02:00
Wim
877f0fe2e8 Reestablish the socket when websocket is disconnected. (from @recht matterircd fork) 2017-07-01 17:49:12 +02:00
Wim
003d85772c Add link to wiki 2017-06-30 01:10:38 +02:00
Wim
e7e10131de Release v0.16.0-rc1 2017-06-30 00:01:33 +02:00
Wim
830361e48b Deprecate URL,useAPI,BindAddress (slack,mattermost,rocketchat) 2017-06-29 23:38:48 +02:00
Wim
1b1a9ce250 Fix samechannel gateway issue. Closes #207 2017-06-27 00:28:18 +02:00
Wim
25ac4c708f Add more debugging (discord) 2017-06-26 23:01:35 +02:00
Wim
c268e90f49 Remove label from URLs (slack). Closes #205
If slack detects a text contains an url it changes it to <http://url|url>.
Strip the |url so that http://url remains.
2017-06-26 22:16:19 +02:00
Sacha Aury - Wolfman
c17512b7ab Add webhook posting mode for discord. (#204)
Using it implies to configure a Webhook on discord and set the parameter :
- WebhookURL (New parameter, discord-specific)

Discord API does not allow to change the name of the user posting, but webhooks does.
This makes the relay much more elegant, even if we might lose some more advanced features.

Signed-off-by: saury07 <sacha.aury@gmail.com>
2017-06-26 20:07:27 +02:00
Wim
1b837b3dc7 Add ShowEmbeds option (discord). #202 2017-06-24 23:17:57 +02:00
Wim
2ece724f75 Fix example 2017-06-22 01:10:15 +02:00
Wim
276ac840aa Add initial steam support 2017-06-22 01:02:05 +02:00
Wim
1f91461853 Add vendor (steam) 2017-06-22 01:00:27 +02:00
Wim
1f9874102a Bump version 2017-06-22 00:56:52 +02:00
Wim
822605c157 Release v0.15.0 2017-06-19 20:47:41 +02:00
Wim
e49266ae43 Update gitter vendor. (fixes crash) 2017-06-19 20:27:14 +02:00
Wim
62e9de1a3b Use the last (and biggest) photo to relay (telegram). Closes #184 2017-06-18 23:59:52 +02:00
Wim
2ddc4f7ae9 Add UserID to each message. Closes #200 2017-06-18 15:44:54 +02:00
Wim
2dd402675d Sent only the biggest picture to bridges (telegram) 2017-06-18 01:23:15 +02:00
Wim
25b1af1e11 Add option IgnoreMessages to ignore messages based on regexp. (all). Closes #70 2017-06-18 01:08:11 +02:00
Wim
75fb2b8156 Make reconnection more robust (irc). #153 2017-06-18 00:13:10 +02:00
Wim
2a403f8b85 Add initial sticker/video/photo/document support (telegram). #184 2017-06-17 18:25:17 +02:00
Wim
c3d45a9f06 Do not relay join/part of ourselves (irc). Closes #190 2017-06-17 17:58:56 +02:00
Wim
c07b85b625 Add note about private channels (rocketchat). See #180 2017-06-15 23:05:59 +02:00
Wim
511f653e6e Fix incorrect behaviour of EditDisable (mattermost). Fixes #197 2017-06-15 22:45:34 +02:00
Wim
5636eaca6d Bump version 2017-06-15 22:45:23 +02:00
Wim
4b839b9958 Avoid nil in usermembermap (discord). See #198 2017-06-15 22:29:01 +02:00
Wim
3f79da84d5 Release v0.14.0 2017-06-15 01:44:46 +02:00
Wim
d540638223 Remove debug 2017-06-15 01:30:58 +02:00
Wim
4ec9b6dd4e Add 3.10.0 support (mattermost) 2017-06-15 01:30:05 +02:00
Wim
3bc219167a Remove need for channel when using api. Closes #195 2017-06-15 00:40:23 +02:00
Wim
8a55c97b4e Fix utf-8 issues #193 2017-06-15 00:07:12 +02:00
Syam.G.Krishnan
9e34162a09 remove second flag.Parse() (#196)
flag.Parse() is already being called on line 28 https://github.com/42wim/matterbridge/blob/master/matterbridge.go#L28
and there is no need for calling it again
2017-06-14 17:15:35 +02:00
Wim
860a371eeb Use cache for teamid 2017-06-12 20:30:30 +02:00
Wim
41a46526a1 Add note about file permissions 2017-06-08 23:42:00 +02:00
Wim
46b798ac1b Update documentation (api) 2017-06-08 00:03:06 +02:00
Wim
359d0f2910 Allow reuse of api in different gateways. See #189 2017-06-07 23:54:50 +02:00
Wim
ad3cb0386b Add token authentication (api) 2017-06-06 00:05:32 +02:00
Wim
3a183cb218 Update vendor 2017-06-06 00:04:18 +02:00
Wim
2eecaccd1c Change to lowercase JSON keys (api) 2017-06-05 23:18:13 +02:00
Wim
5f30a98bc1 Add gateway name to messages 2017-06-05 23:12:19 +02:00
Wim
b8a2fcbaff Post valid JSON (api). See #185 2017-06-05 23:08:36 +02:00
Wim
01496cd080 Fix panic (mattermost). Closes #186 2017-06-05 21:35:38 +02:00
Wim
6a968ab82a Bump version 2017-06-03 18:22:09 +02:00
Wim
c0c4890887 Add hashtag to channel (discord) 2017-06-03 18:21:47 +02:00
Wim
171a53592d Add note about lowercase channel (irc) 2017-06-01 21:00:58 +02:00
Wim
7811c330db Release v0.13.0 2017-05-31 23:32:38 +02:00
Wim
9bcd131e66 Reset variables each loop (telegram). Closes #181 2017-05-30 21:14:03 +02:00
Wim
c791423dd5 Add NOPINGNICK option. Closes #175 2017-05-30 00:11:53 +02:00
Wim
80bdf38388 Bump version 2017-05-29 23:54:43 +02:00
Wim
9d9cb32f4e Limit message length (irc). Closes #179 2017-05-29 21:54:34 +02:00
Wim
87229bab13 Fix sending to different channels on same account (slack). Closes #177 2017-05-24 22:10:21 +02:00
Wim
f065e9e4d5 Release v0.12.1 2017-05-23 22:48:05 +02:00
Wim
3812693111 Replace long ids in channel metions (discord). Fixes #174 2017-05-23 22:26:37 +02:00
Wim
dd3c572256 Fix possible crash on nil (discord) 2017-05-22 21:57:19 +02:00
Wim
c5dfe40326 Update documentation about encrypted rooms in matrix 2017-05-21 15:36:40 +02:00
siinus
ef278301e3 Fix JoinChannel argument to use IRC channel key (#172) 2017-05-21 15:23:56 +02:00
Wim
2888fd64b0 Add UseFirstName option (telegram). Closes #144 2017-05-15 23:23:10 +02:00
Wim
27c0f37e49 Update matterbridge.toml.sample about NoHomeServerSuffix 2017-05-15 23:11:27 +02:00
Wim
0774f6a5e7 Bump version 2017-05-12 23:20:22 +02:00
Wim
4036d4459b Add NoHomeServerSuffix. Option to disable homeserver on username (matrix). Closes #160. 2017-05-12 23:04:58 +02:00
Frank
ee643de5b6 Add Compatibility for Cisco Jabber (xmpp) (#166) 2017-05-11 20:10:53 +02:00
Wim
8c7549a09e Update changelog 2017-05-09 23:46:20 +02:00
Wim
7a16146304 Release v0.12.0 2017-05-09 23:31:26 +02:00
Wim
3d3809a21b Add 3.9.0 support (mattermost) 2017-05-09 23:30:53 +02:00
ryarnyah
29465397dd Add support for HTTP{S}_PROXY env variables (#162) 2017-05-08 21:20:52 +02:00
Wim
d300bb1735 Relay messages starting with ! (irc). Closes #164 2017-05-08 21:15:01 +02:00
Wim
2e703472f1 Fix crash on reconnects when server is down. Closes #163 2017-05-08 20:44:36 +02:00
Wim
8fede90b9e Remove examples (vendor issues) 2017-05-05 20:45:11 +02:00
Wim
d128f157c4 Update doc 2017-04-19 21:57:40 +02:00
Wim
4fcedabfd0 Revert "Add support for edited messages (gitter)"
This reverts commit 17b8b86d68badfe27c29c7448548f9eb97dd26e9.
Reverted because of lingering file descriptors (memory leak)
2017-04-19 19:51:33 +02:00
Wim
246c8e4f74 Ignore error on private channel join (slack) Fixes #150 2017-04-17 18:01:24 +02:00
Wim
4d2207aba7 Add support for edited messages (slack) 2017-04-16 00:16:24 +02:00
Wim
17b8b86d68 Add support for edited messages (gitter) 2017-04-15 23:46:01 +02:00
Wim
fdb57230a3 Add support for edited messages (mattermost) 2017-04-15 20:21:57 +02:00
Wim
7469732bbc Add support for edited messages (telegram) 2017-04-15 19:07:35 +02:00
Wim
d1dd6c3440 Add support for edited messages (discord) 2017-04-15 19:00:15 +02:00
Wim
02612c0061 Add support for sending edited messages 2017-04-15 18:46:25 +02:00
Wim
a4db63a773 Bump version 2017-04-15 16:24:25 +02:00
Wim
035c2b906a Strip custom emoji metadata (discord). Closes #148 2017-04-15 16:23:34 +02:00
Wim
6ea8be5749 Release v0.11.0 2017-04-11 21:51:23 +02:00
Wim
36024d5439 Add 3.8.0 support (mattermost) 2017-04-09 23:15:11 +02:00
Wim
8d52c98373 Update README 2017-04-08 00:57:11 +02:00
Wim
b4a4eb0057 Update changelog 2017-04-08 00:50:17 +02:00
Wim
b469c8ddbd Rejoin channel when kicked (irc). Closes #146 2017-04-08 00:42:37 +02:00
Wim
eee0036c7f Modify iconurl correctly (mattermost). Closes #145 2017-04-08 00:16:46 +02:00
Wim
89c66b9430 Reconnect on session removal (mattermost) 2017-04-07 23:27:41 +02:00
Wim
bd38319d83 Add support for showing/hiding join/leave messages from mattermost. Closes #147 2017-04-07 22:27:36 +02:00
Wim
33dffd5ea8 Fix join/leave regression (irc) 2017-04-03 22:18:29 +02:00
Wim
57176dadd4 Support edited messages (telegram). See #141 2017-04-01 18:18:38 +02:00
Wim
dd449a8705 Remove debug info (irc) 2017-04-01 18:10:11 +02:00
Wim
587ad9f41d Remove space after nick (mattermost). Closes #142 2017-04-01 17:44:17 +02:00
Wim
a16ad8bf3b Reuse connection when using same bridge with another gateway. See #87 2017-04-01 17:24:19 +02:00
Wim
1e0490bd36 Merge branch 'channelinfo' 2017-03-28 23:58:22 +02:00
Wim
8afc641f0c Bump version 2017-03-28 23:58:07 +02:00
Wim
2e4d58cb92 Refactor 2017-03-28 23:56:58 +02:00
Wim
02d7e2db65 Release v0.10.3 2017-03-27 20:40:57 +02:00
Wim
f935c573e9 Allow bot tokens for now without warning (slack). Closes #140 2017-03-27 20:15:05 +02:00
Wim
4a25e66c00 Release v0.10.2 2017-03-25 21:35:13 +01:00
Wim
95f4e3448e Use API_URL_SUFFIX_V3 (mattermost) 2017-03-25 21:05:02 +01:00
Wim
eacb1c1771 Update vendor (mattermost) 2017-03-25 21:04:10 +01:00
Wim
07fd825349 Update vendor 2017-03-25 20:45:10 +01:00
Wim
be15cc8a36 Update vendored toml. Adds support for inline tables 2017-03-25 19:13:47 +01:00
Wim
2f68519b3c Add gops agent 2017-03-23 23:28:55 +01:00
Wim
efe641f202 Add link about token (slack) 2017-03-23 23:02:00 +01:00
Wim
9bd663046a Fix slack/discord example 2017-03-20 12:12:12 +01:00
Wim
11b07f01ba Add more startup messages 2017-03-19 19:41:57 +01:00
Wim
6c2f370e6b Add badges 2017-03-18 22:16:25 +01:00
Wim
936bccccd2 Release v0.10.1 2017-03-18 21:19:22 +01:00
Wim
c30ffeb81e Fix roomid bug (gitter) 2017-03-18 21:17:21 +01:00
Wim
e05a323afd Release v0.10.0 2017-03-18 17:00:32 +01:00
Wim
80895deae2 Replace role ids in mentions to role names (discord). Closes #133
* The bot needs to have the "Manage Roles" permission for this to work.
(see Server settings - Roles - General Permissions)
2017-03-18 16:50:09 +01:00
Wim
eddc691fc9 Join rooms not already joined by the bot (gitter). See #135 2017-03-18 15:34:19 +01:00
Wim
deb2d7194d Add badges 2017-03-16 23:45:24 +01:00
Wim
fd8cfb11fb Fail when bridge is unable to join a channel (general) 2017-03-16 23:05:11 +01:00
Wim
9407aa4600 Check if room exists when joining channel (gitter). Closes #135 2017-03-16 23:01:18 +01:00
Wim
263b8da37d Add 3.7.0 support (mattermost) 2017-03-15 01:04:52 +01:00
Wim
b95988b4e2 Fix URL / Server mistake in sample (matrix) 2017-03-14 00:26:05 +01:00
Wim
35025e164a Do not forward empty message from any bridge (general). Closes #128 2017-03-02 23:51:19 +01:00
Wim
32bbab8518 Do not use HTML parsemode by default. Set MessageFormat="HTML" to use it. (telegram) Closes #126 2017-02-24 18:50:16 +01:00
Wim
84c0b745af Use roomalias instead of internal ID (matrix) 2017-02-24 17:58:51 +01:00
Wim
8b286fb009 Add ReadTimeout to close lingering connections (mattermost). See #125 2017-02-21 22:13:34 +01:00
Wim
386fa58b67 Update README 2017-02-20 13:48:45 +01:00
Wim
c5cfbc2297 Add matrix support 2017-02-20 00:50:37 +01:00
Wim
cd0a2beb11 Release v0.9.3 2017-02-18 23:32:21 +01:00
Wim
73f01ad8d8 Add REST API support 2017-02-18 23:13:46 +01:00
Wim
930b639cc9 Update vendor 2017-02-18 23:11:48 +01:00
Wim
58483ea70c Update changelog 2017-02-17 23:21:43 +01:00
Wim
072cac0347 Do not relay slackbot messages (slack). Closes #119 2017-02-17 23:13:02 +01:00
Wim
956d7cf3f3 Add githash to docker builds 2017-02-17 22:32:42 +01:00
Wim
7558a2162e Merge branch 'status' 2017-02-17 22:12:53 +01:00
Wim
62b165c0b4 Refactor samechannelgateway 2017-02-17 22:08:30 +01:00
Wim
fe258e1b67 Set http timeout to 10 seconds 2017-02-17 17:51:07 +01:00
Wim
dc37232100 Refactor. Make extra options easier for other protocols 2017-02-14 23:52:45 +01:00
Wim
163f55f9c2 Refactor to handle disconnects/reconnects better.
Now try to reconnect every 60 seconds until forever.
2017-02-14 21:12:02 +01:00
Wim
2d16fd085e Use nickname when present (discord). Closes #122 2017-02-13 18:52:52 +01:00
Wim
e1a5f5bca5 Add more error checking 2017-02-03 16:43:21 +01:00
Wim
6e772ee189 Update changelog 2017-02-03 16:41:34 +01:00
Wim
2b0f178ba3 Fix receiving messages from private channels (slack). See #118 2017-02-03 16:40:15 +01:00
Wim
79e6c9fa6c Update vendor 2017-01-28 22:45:32 +01:00
Wim
1426ddec5f Bump version 2017-01-28 22:29:19 +01:00
Wim
e9105003b0 Release v0.9.2 2017-01-28 22:15:32 +01:00
Wim
587bb06558 Update changelog 2017-01-28 00:39:33 +01:00
Wim
53e9664cde Add support for private channels (slack). Closes #118 2017-01-28 00:36:53 +01:00
Wim
482fbac68f Update vendor (slack) 2017-01-28 00:36:22 +01:00
Wim
dcccd43427 Use unknown as username if unsigned channel (telegram) 2017-01-27 23:59:24 +01:00
Wim
397b8ff892 Update changelog 2017-01-27 23:40:20 +01:00
Wim
38a4cf315a Add telegram links about channel and tokens 2017-01-27 23:36:14 +01:00
Wim
5f8b24e32c Fix username (telegram) 2017-01-27 23:30:46 +01:00
Wim
678a7ceb4e Fix channel and group messages (telegram) 2017-01-27 23:26:06 +01:00
Wim
077d494c7b Update vendor (telegram) 2017-01-27 00:23:14 +01:00
Wim
09b243d8c2 Update vendor (irc) 2017-01-24 21:24:57 +01:00
Wim
991183e514 Fix IgnoreNicks (global). Closes #115 2017-01-21 21:00:40 +01:00
Josip Janžić
9bf10e4b58 Fix tls by setting ServerName (xmpp) (#114)
Fixes error message shown by tls: "either ServerName or InsecureSkipVerify must be specified in the tls.Config"
2017-01-18 21:01:42 +01:00
Wim
884599d27d Bump version 2017-01-18 20:06:52 +01:00
Wim
f8a6e65bfd Release v0.9.1 2017-01-18 00:02:37 +01:00
Wim
6df6c5d615 Add GetStatuses() 2017-01-17 22:47:59 +01:00
Wim
93114b7682 Sync with mattermost 3.6.0 2017-01-17 00:00:26 +01:00
Wim
9987ac3f13 Update changelog 2017-01-16 23:55:03 +01:00
Stefan Haller
01a32b2154 Handle SkipTLSVerify for XMPP client (#106). Closes #81
* Handle SkipTLSVerify for XMPP client

* Mention SkipTLSVerify for XMPP in sample config
2017-01-14 00:35:45 +01:00
Wim
b3c3142bb2 Do not use API functions in webhook (slack). Closes #110 2017-01-12 00:40:50 +01:00
Wim
77f1a959c3 Handle errors in initUser() 2017-01-06 23:51:44 +01:00
Tatsuyuki Ishi
e3dda0e812 Telegram: add markdown (#103)
* Add support for markdown (telegram)

Close #98

* Telegram: add more Markdown Render blacklist
2017-01-06 23:32:17 +01:00
Wim
38103d36b4 Update changelog. Remove obsolete info 2017-01-04 14:20:24 +01:00
Wim
7685fe1724 Add channel key support (irc). Closes #27 2017-01-04 14:10:35 +01:00
Wim
01afe03a3f Merge pull request #104 from markusgraube/patch-2
Update matterbridge.toml.sample
2017-01-03 23:07:25 +01:00
Markus Graube
7fbbf89c58 Update matterbridge.toml.sample
Add password entry to irc section matterbride.toml.sample
2016-12-26 10:12:59 +01:00
@42wim
84d259d8b3 Merge pull request #102 from ishitatsuyuki/patch-1
Fix the missing username (telegram). Closes #93
2016-12-17 15:44:26 +01:00
Tatsuyuki Ishi
8b47670a74 Telegram: Fix the missing username 2016-12-17 18:43:54 +09:00
Wim
7f5dc1d461 Update changelog 2016-12-08 00:16:59 +01:00
Wim
43e765f4f9 Exit when a bridge fails to start 2016-12-08 00:14:17 +01:00
Wim
adec73f542 Check errors only on first connect. Keep retrying after first connection succeeds. (mattermost) Closes #95 2016-12-08 00:07:24 +01:00
Wim
fee159541f Add initial Rocket.Chat support 2016-12-03 00:10:29 +01:00
Wim
d81e6bf6ce Release v0.9.0 2016-12-01 22:15:40 +01:00
Wim
70c93d970c Update public links to new API (mattermost) 2016-11-26 15:46:39 +01:00
Wim
4960273832 Do not relay empty or delayed messages (xmpp) 2016-11-26 15:08:41 +01:00
Wim
6c018ee6fe Enable keepalive (xmpp) 2016-11-26 15:04:06 +01:00
Wim
4ef32103ca Update xmpp vendor 2016-11-26 14:44:33 +01:00
Wim
e4ec27c5e2 Add sample documentation for hipchat support via xmpp 2016-11-26 00:40:21 +01:00
Wim
20c04f7977 Fix loop because of closed channel. Fixes #89 2016-11-23 23:51:51 +01:00
Wim
571f50d734 Support mattermost setup with up to 50k users 2016-11-23 21:24:35 +01:00
@42wim
780ea6f7c0 Create ISSUE_TEMPLATE.md 2016-11-23 20:22:27 +01:00
Wim
4279906f6e Add logo 2016-11-23 00:15:17 +01:00
Wim
2e54b97fc2 Add support for RemoteNickFormat in general configuration (samechannelgateway) 2016-11-20 23:50:12 +01:00
Wim
e1641b2c2e Add support for RemoteNickFormat in general configuration 2016-11-20 23:33:41 +01:00
Wim
e0e1e4be80 Add gateway.inout config for bidirectional bridges. Closes #85 2016-11-20 23:01:44 +01:00
Wim
d5845ce900 Replace id-mentions to usernames (slack). Closes #86 2016-11-20 22:40:09 +01:00
Wim
85f2cde4c3 Update documentation 2016-11-20 18:01:59 +01:00
Wim
cef64e01b3 Remove callbacks after being called. Fixes #88 (irc) 2016-11-20 17:21:15 +01:00
Wim
94ea775232 Merge branch 'telegram'
Add telegram support
2016-11-20 17:02:17 +01:00
Wim
2e4b7fac11 Update documentation and sample (telegram) 2016-11-20 17:01:53 +01:00
Wim
2867ec459a Add missing imports 2016-11-19 15:05:11 +01:00
Wim
cd18d89894 Add initial telegram support 2016-11-15 23:15:57 +01:00
Wim
449ed31e25 Fix ShowJoinPart from irc bridge. Closes #72 2016-11-14 22:53:06 +01:00
Wim
1f36904588 Update sample config. Closes #75 2016-11-14 16:42:32 +01:00
Wim
f7495dd0c3 Add bot tag to api if not specified (discord) 2016-11-14 16:30:43 +01:00
Wim
a11f77835d Fix !users command for irc. Closes #78. 2016-11-14 00:12:48 +01:00
Wim
af1ad82c8e Fix merge issue 2016-11-13 23:12:17 +01:00
Wim
4976338677 Merge branch 'refactor' 2016-11-13 23:09:06 +01:00
Wim
99d130d1ed Refactor 2016-11-13 23:06:37 +01:00
Wim
4fb0544b0e Fix GetLastViewedAt 2016-11-13 16:03:04 +01:00
Wim
0b4ac61435 Update documentation 2016-11-12 22:33:58 +01:00
Wim
1d5cd1d7c4 Sync with mattermost 3.5.0 2016-11-12 22:00:53 +01:00
Wim
08ebee6b4f Validate channels for samechannelgateway. Fixes #73. 2016-11-11 15:23:22 +01:00
Wim
14830d9f1c Refactor gateway 2016-11-08 23:44:16 +01:00
Wim
a3dd0f1345 Add support for using avatars from discord,slack and gitter in slack 2016-11-06 00:46:32 +01:00
Wim
37873acfcd Update vendor (slack) 2016-11-06 00:07:24 +01:00
Wim
2dbe0eb557 Add support for dynamic IconURL (slack). Closes #43 2016-11-05 01:11:51 +01:00
Wim
50a0df4279 Reconnect on connection timed out (mattermost). Fixes #71 2016-11-04 23:17:49 +01:00
Wim
c3a8b7a997 Refactor modifyMessage 2016-11-04 23:03:31 +01:00
Wim
95fac548bb Reconnect on connection reset by peer (mattermost). Fixes #69 2016-11-02 20:00:00 +01:00
Wim
581847f415 Update to latest go-gitter API changes 2016-11-02 16:28:23 +01:00
Wim
1b15897135 Fix tight loop (gitter). Closes #68. 2016-11-02 16:13:22 +01:00
Wim
8e606e3cef Update documentation 2016-11-01 23:10:29 +01:00
Wim
be513622ac Add anti-flooding settings (irc). See #40 2016-11-01 22:52:28 +01:00
Wim
6f309f2108 Use names instead of id's for mentions (discord). Fixes #66 2016-10-30 22:55:34 +01:00
Wim
92d9db5a2d Override config from environment. See #50
Expects uppercase environment variables of MATTERBRIDGE_PROTOCOL_ACCOUNT_KEY="value"
e.g. you can override this config

[mattermost]
    [mattermost.work]
    Team="yourteam"
    Login="yourlogin"
    Password="yourpass"

by using
MATTERBRIDGE_MATTERMOST_WORK_TEAM="newteam"
MATTERBRIDGE_MATTERMOST_WORK_LOGIN="newlogin"
MATTERBRIDGE_MATTERMOST_WORK_PASSWORD="newpassword"
2016-10-30 22:32:29 +01:00
Wim
96620a3c2c Drop first received message on connection to avoid duplicates (slack). Fixes #55 2016-10-29 21:05:56 +02:00
Wim
5249568b8e Wait until the welcome message before connection is ok (irc). Fixes #62 2016-10-29 18:59:12 +02:00
Wim
4a336a6bba Forward channel notices too (irc) 2016-10-29 18:01:16 +02:00
Wim
60223d7f63 Update changelog 2016-10-29 17:54:37 +02:00
Wim
5131253191 Update documentation 2016-10-29 17:34:27 +02:00
Wim
035dc042a1 Fix teamid bug (mattermost) 2016-10-29 16:46:02 +02:00
Wim
dfc513530b Ignore messages from ourself (irc bridge) 2016-10-29 16:35:16 +02:00
Wim
721e0a2dcd Ignore private queries (irc bridge) 2016-10-29 16:27:07 +02:00
Wim
8452eb12da Only respond to notices from nickserv (irc bridge) 2016-10-29 16:09:58 +02:00
Wim
475bed5e19 Add support for discord channel ID. See #57 2016-10-26 01:01:36 +02:00
Wim
40a967523c Ignore empty content from discord. Fixes #58 2016-10-26 00:12:31 +02:00
Wim
d3a34af073 Add support for discord attachments. Fixes #59 2016-10-26 00:09:22 +02:00
Wim
e7107cf782 Use RTM only on API (slack). Fix #56 2016-10-25 23:29:32 +02:00
@42wim
b7c918a195 Merge pull request #54 from markusgraube/patch-1
Close open strings in matterbridge.conf.sample
2016-10-24 12:55:43 +02:00
Markus Graube
61e4c9b28c Update matterbridge.conf.sample
Close open strings
2016-10-24 12:14:51 +02:00
Wim
e93847a95e Launch every account only once. Fixes #48 2016-10-23 22:23:20 +02:00
Wim
545377742c Drop messages not from our mattermost team. Fixes #49 2016-10-23 21:16:14 +02:00
Wim
47d38192b2 Only send to channels defined in config. Fixes #53 2016-10-23 20:58:04 +02:00
Wim
ac80c47036 Update documentation in sample config about channelnames 2016-10-23 19:51:46 +02:00
Wim
1e84afbd90 Rename discord guild to server. 2016-10-23 19:51:41 +02:00
Wim
d31e641bac Add documentation about bot tag for discord 2016-10-23 18:19:18 +02:00
Wim
4380c48b4b Add irc names callback only on command. Fixes #51 2016-10-23 18:19:11 +02:00
Wim
db0e4ba8c5 Add error message about non-existing channels (slack) 2016-10-08 21:57:03 +02:00
Wim
2d6ed51d94 Bail out on samechannel gateway when a bridge fails to start 2016-10-03 09:23:55 +02:00
Wim
9ca4fe7a5e Fix matterbridge.toml.sample 2016-10-01 20:14:06 +02:00
Wim
e52b040b9c Add more irc debug on connect (when debugging enabled) 2016-10-01 20:07:59 +02:00
Wim
1accee1653 Bail out when a bridge fails to start 2016-10-01 20:07:04 +02:00
Wim
fff6f08cb6 Add samechannel gateway. See #35 2016-09-30 23:19:47 +02:00
Wim
0e527a4252 Fix slack channel join 2016-09-30 23:15:35 +02:00
Wim
f10251a1a3 Fix mattermost bridge channel join 2016-09-30 22:59:30 +02:00
Wim
0d4bad16a3 Fix sample config. Closes #38 2016-09-30 20:35:16 +02:00
Wim
8c6be434ac Remove newline splitting from outgoing mattermost messages. Should be handled by receiving bridge. 2016-09-29 23:32:12 +02:00
Wim
3ca4309e8a Split newlines for irc (#37) 2016-09-29 21:21:24 +02:00
Wim
e8a2e1af63 Fix IRC colors regexp 2016-09-22 23:48:05 +02:00
Wim
1d240140c9 Strip IRC colors. Closes #33 2016-09-21 00:33:40 +02:00
Wim
272eef544f Add support for mattermost attachments. Shows public link on bridges. Closes #32 2016-09-20 23:48:58 +02:00
Wim
fd756c5332 Use specified config file 2016-09-20 23:18:51 +02:00
Wim
dce600ad51 Fix joining slack/mattermost channels using the webhook 2016-09-20 12:20:44 +02:00
Wim
d02a737e0c Cleanup debug messages 2016-09-20 00:21:14 +02:00
Wim
98ff59c716 Cleanup discord bridge debug/info messages 2016-09-20 00:15:30 +02:00
Wim
0e96e9f9be Cleanup slack bridge debug/info messages 2016-09-20 00:13:57 +02:00
Wim
e8c7898583 Cleanup gitter bridge debug/info messages 2016-09-20 00:06:19 +02:00
Wim
11f4a6897a Cleanup xmpp bridge debug/info messages 2016-09-20 00:03:01 +02:00
Wim
002c5fd0d1 Cleanup mattermost bridge debug/info messages 2016-09-19 23:58:57 +02:00
Wim
18504ec08d Cleanup irc bridge debug/info messages 2016-09-19 23:35:47 +02:00
Wim
4737442185 Connect only once to each bridge 2016-09-19 23:05:49 +02:00
Wim
596096d6da Add the discord bridge for real 2016-09-19 21:05:13 +02:00
Wim
6af82401fc Add forgotten vendor for discord 2016-09-19 21:04:06 +02:00
Wim
a0b84beb9b Add Discord support 2016-09-19 20:53:26 +02:00
Wim
0816e96831 Update documentation 2016-09-18 21:04:28 +02:00
Wim
7baf386ede Refactor for more flexibility
* Move from gcfg to toml configuration because gcfg was too restrictive
* Implemented gateway which has support multiple in and out bridges.
* Allow for bridging the same bridges, which means eg you can now bridge between multiple mattermosts.
* Support multiple gateways
2016-09-18 19:21:15 +02:00
Wim
6e410b096e Release v0.6.1 2016-09-17 15:34:59 +02:00
Wim
f9e5994348 Fix mattermost API change for UpdateLastViewedAt 2016-09-17 15:33:02 +02:00
Wim
ee77272cfd Release v0.6.0 2016-09-17 15:25:34 +02:00
Wim
16ed2aca6a Sync with mattermost 3.4.0 2016-09-17 15:19:18 +02:00
Wim
0f530e7902 Fix spinning for loop 2016-09-05 23:08:17 +02:00
Wim
4ed66ce20e Update documentation 2016-09-05 16:42:46 +02:00
Wim
b30e85836e Add Slack support 2016-09-05 16:34:37 +02:00
Wim
e449a97bd0 Release v0.6.0-beta2 2016-09-04 20:42:24 +02:00
Wim
39043f3fa4 Update documentation 2016-09-04 20:41:03 +02:00
Wim
12389d602e Add Gitter support 2016-09-04 20:04:43 +02:00
Wim
44144587a0 Get correct teamname for non-joined channels. Closes 42wim/matterircd#65 2016-09-01 00:15:48 +02:00
Wim
d0a30e354b Fix documentation layout 2016-08-20 18:16:22 +02:00
Wim
c261dc89d5 Fix documentation layout 2016-08-20 18:15:06 +02:00
Wim
c2c135bca2 Release v0.6.0-beta1 2016-08-20 18:09:00 +02:00
Wim
eb20cb237d Update documentation 2016-08-20 18:08:59 +02:00
Wim
106404d32f Fix info message 2016-08-20 18:08:59 +02:00
Wim
e06efbad9f Remove unused code 2016-08-20 18:08:58 +02:00
Wim
3311c7f923 Refactor handleReceive 2016-08-20 18:08:58 +02:00
Wim
3a6c655dfb Remove redundant function 2016-08-20 18:08:58 +02:00
Wim
e11d786775 Move nickformatting into bridge 2016-08-20 18:08:57 +02:00
Wim
889b6debc4 Add Connect() to Bridger interface 2016-08-20 18:08:57 +02:00
Wim
9cb3413d9c Add Enable per section (protocol) instead of in general section 2016-08-20 18:08:57 +02:00
Wim
131826e1d1 Fix crash on exit 2016-08-19 22:58:42 +02:00
Wim
96e21dd051 Add documentation about breaking API changes for mattermost 3.3.0. Start work on 0.6.0-dev 2016-08-15 21:11:50 +02:00
Wim
32e5f396e7 Make sure login works after logout 2016-08-15 20:27:36 +02:00
Wim
6c6000dbbd Update code to mattermost 3.3.0 API changes 2016-08-15 18:49:17 +02:00
Wim
24defcb970 Sync with mattermost 3.3.0 2016-08-15 18:47:31 +02:00
Wim
a1a11a88b3 Fix nil pointers 2016-08-14 23:04:28 +02:00
Wim
a997ae29ad Add StatusLoop(), keeps connection alive 2016-08-14 22:54:57 +02:00
Wim
ff94796700 Refactor bridge. Allows bridging between every protocol 2016-08-14 22:44:59 +02:00
Wim
1f72ca4c4e Add initial XMPP support 2016-08-14 22:40:26 +02:00
Wim
46faad8b57 Vendor go-xmpp 2016-08-14 22:40:25 +02:00
Wim
30f30364d5 Release v0.5.0 2016-07-27 22:42:59 +02:00
Wim
073d90da88 Fix docker build 2016-07-23 00:03:54 +02:00
Wim
c769e23a9a Fix crash on invalid team 2016-07-22 23:47:25 +02:00
Wim
9db48f4794 Update readme 2016-07-22 23:36:54 +02:00
Wim
911c597377 Sync with mattermost 3.2.0 2016-07-22 23:15:59 +02:00
Wim
28244ffd9a Fix pointer reuse problem 2016-07-22 23:04:08 +02:00
Wim
3e38c7945c Actually add sasl.go 2016-07-22 22:51:11 +02:00
Wim
79ffb76f6e Add (PLAIN) SASL support 2016-07-21 23:47:44 +02:00
Wim
5fe4b749cf Do not check bindaddress when not using the server 2016-07-17 22:15:19 +02:00
Wim
6991d85da9 Add FAQ section 2016-07-15 21:59:14 +02:00
Wim
c1c187a1ab Fix markdown 2016-07-12 22:00:38 +02:00
Wim
055d12e3ef Release v0.5.0-beta1 2016-07-12 21:32:15 +02:00
Wim
b49429d722 Add migration info 2016-07-12 01:23:36 +02:00
Wim
815c7f8d64 Update version 2016-07-12 01:07:37 +02:00
Wim
c879f79456 Update documentation 2016-07-12 01:02:56 +02:00
Wim
3bc25f4707 Update documentation 2016-07-12 00:25:32 +02:00
Wim
300cfe044a Remove token check 2016-07-12 00:23:36 +02:00
Wim
fb586f4a96 Remove Port from IRC config. Specify it with server 2016-07-11 23:30:42 +02:00
Wim
ced371bece Add port to BindAddress 2016-07-11 23:22:56 +02:00
Wim
a87cac1982 Remove multiple Token config. Use same channel setup as from matterbridge-plus 2016-07-11 22:55:58 +02:00
Wim
8fb5c7afa6 Remove UseSlackCircumfix. Use RemoteNickFormat 2016-07-11 21:26:13 +02:00
Wim
aceb830378 Converge with matterbridge-plus 2016-07-11 21:23:33 +02:00
Wim
0f2976c5ce Release v0.4.2 2016-06-23 20:31:12 +02:00
Wim
78b17977c5 Fix mattermost 3.1.0 API change. Closes #24 2016-06-23 20:29:51 +02:00
Wim
6ec77e06ea Sync with mattermost 3.1.0 2016-06-23 20:28:05 +02:00
Wim
e48db67649 Fix sample documentation. Closes #23 2016-06-16 21:01:46 +02:00
Wim
e03f331f55 Add Dockerfile 2016-05-21 16:30:39 +02:00
Wim
ff5aeeb1e1 Add support for ignoring messages from specific users. 2016-05-21 16:03:19 +02:00
Wim
33844fa60c Commit mattermost vendoring 2016-05-21 14:14:08 +02:00
Wim
85faa43145 Release v0.4 2016-05-15 23:05:03 +02:00
Wim
59e6abcc11 Sync with mattermost 3.0 2016-05-15 23:02:30 +02:00
Wim
38e3bbe5c9 Mention go1.6 requiremÃent 2016-04-25 21:40:26 +02:00
Wim
51265d5464 Sync with mattermost 2.2.0 release 2016-04-16 20:39:43 +02:00
Wim
de4c780410 Vendor libs 2016-04-10 23:39:38 +02:00
Wim
6b18257185 Move common code to matterbridge-plus bridge package 2016-03-23 00:25:04 +01:00
Wim
4b1ebaf7d5 Update documentation and sample config 2016-03-22 00:42:49 +01:00
Wim
93db74e7e1 Add -debug option. Run go fmt. Some cleanups 2016-03-22 00:26:27 +01:00
@42wim
0e6fe4070a Merge pull request #16 from fdevibe/nickserv-auth
Add Freenode style NickServ-based authentication.
2016-03-22 00:04:26 +01:00
Fredrik de Vibe
69b534ee99 Add Freenode style NickServ-based authentication.
If the server sends a NOTICE requesting identification, identify.
2016-03-21 23:52:10 +01:00
@42wim
71a504945b Merge pull request #15 from fdevibe/async-connect
Handle connects asynchroneously.
2016-03-21 23:49:52 +01:00
Fredrik de Vibe
99ac7dc114 Handle connects asynchroneously.
On connect events, update the nick with what's actually used and
set up channels as configured.
2016-03-21 14:24:25 +01:00
@42wim
4984473c1b Merge pull request #14 from fdevibe/enable-users-2
Enable !users command and add a table formatter
2016-03-18 23:48:05 +01:00
Fredrik de Vibe
3fcce2d8a0 Limit number of columns to number of IRC nicks.
Don't add more columns to the IRC nicks table than the total number of
nicks in the IRC channel.
2016-03-18 18:13:17 -04:00
Fredrik de Vibe
a53e699112 Remove unnecessary break in switch block. 2016-03-18 18:09:55 -04:00
Fredrik de Vibe
f29822db02 Add double newline if the message is markup and prefixed.
If the message is prefixed with the sender nick, it will break markup
formatting on the same line. This commit introduces a very rudimentary
markup checker, and if the message is deemed to be markup in those
cases, the space between sender nick and message is replaced by a
double newline.
2016-03-18 18:03:15 -04:00
Fredrik de Vibe
a63433e41b Move new config parameters from IRC to Mattermost.
PrefixMessagesWithNick, NickFormatter and NicksPerRow.
2016-03-18 15:54:14 -04:00
Fredrik de Vibe
e0379ca5af Enable !users command and add a table formatter
* Listen to wildcarded events, currently handle RPL_NAMREPLY.
* Add a formatter to present nick lists from IRC as a table.
2016-03-18 15:19:13 -04:00
@42wim
4759ee6132 Merge pull request #12 from fdevibe/msgprefix
Add config option to prefix messages (IRC->MM) with nick
2016-03-18 12:50:52 +01:00
Fredrik de Vibe
5ec94fdb43 Add config option to prefix messages (IRC->MM) with nick
If username overriding isn't enabled on the Mattermost server, this is
required for Mattermost users to see who sent a message from IRC.
2016-03-18 06:09:29 -04:00
6599 changed files with 7168595 additions and 330 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
Dockerfile
tgs.Dockerfile

3
.fixmie.yml Normal file
View File

@ -0,0 +1,3 @@
go:
comments:
disabled: true

36
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

@ -0,0 +1,36 @@
<!-- This is a bug report template. By following the instructions below and
filling out the sections with your information, you will help the us to get all
the necessary data to fix your issue.
You can also preview your report before submitting it.
Text between <!-- and --> marks will be invisible in the report.
-->
<!-- 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. -->
### Environment
<!-- run `matterbridge -version` -->
<!-- If you're having problems with mattermost also specify the mattermost version. -->
Version:
<!-- What operating system are you using ? (be as specific as possible) -->
Operating system:
<!-- If you compiled matterbridge yourself:
* Specify the output of `go version`
* Specify the output of `git rev-parse HEAD` -->
### 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)) -->

27
.github/ISSUE_TEMPLATE/Bug_report.md vendored Normal file
View File

@ -0,0 +1,27 @@
---
name: Bug report
about: Create a report to help us improve. (Check the FAQ on the wiki first)
labels: bug
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots/debug logs**
If applicable, add screenshots to help explain your problem.
Use logs from running `matterbridge -debug` if possible.
**Environment (please complete the following information):**
- OS: [e.g. linux]
- Matterbridge version: output of `matterbridge -version`
- If self compiled: output of `git rev-parse HEAD`
**Additional context**
Please add your configuration file (be sure to exclude or anonymize private data (tokens/passwords))

View File

@ -0,0 +1,18 @@
---
name: Feature request
about: Suggest an idea for this project
labels: enhancement
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

71
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@ -0,0 +1,71 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
name: "CodeQL"
on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 16 * * 1'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['go']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

58
.github/workflows/development.yml vendored Normal file
View File

@ -0,0 +1,58 @@
name: Development
on: [push, pull_request]
jobs:
lint:
name: golangci-lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 20
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
args: "-v --new-from-rev HEAD~5 --timeout=5m"
test-build-upload:
strategy:
matrix:
go-version: [1.22.x]
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Install Go
uses: actions/setup-go@v3
with:
go-version: ${{ matrix.go-version }}
stable: false
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Test
run: go test ./... -mod=vendor
- name: Build
run: |
mkdir -p output/{win,lin,arm,mac}
VERSION=$(git describe --tags)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/lin/matterbridge-$VERSION-linux-amd64
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/win/matterbridge-$VERSION-windows-amd64.exe
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/mac/matterbridge-$VERSION-darwin-amd64
- name: Upload linux 64-bit
if: startsWith(matrix.go-version,'1.22')
uses: actions/upload-artifact@v3
with:
name: matterbridge-linux-64bit
path: output/lin
- name: Upload windows 64-bit
if: startsWith(matrix.go-version,'1.22')
uses: actions/upload-artifact@v3
with:
name: matterbridge-windows-64bit
path: output/win
- name: Upload darwin 64-bit
if: startsWith(matrix.go-version,'1.22')
uses: actions/upload-artifact@v3
with:
name: matterbridge-darwin-64bit
path: output/mac

68
.github/workflows/docker.yml vendored Normal file
View File

@ -0,0 +1,68 @@
name: docker
on:
push:
branches:
- 'master'
tags:
- 'v*'
pull_request:
branches:
- 'master'
jobs:
docker:
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v2
-
name: Set up QEMU
uses: docker/setup-qemu-action@v1
with:
platforms: amd64,arm64
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
-
name: Docker meta
id: meta
uses: docker/metadata-action@v3
with:
images: 42wim/matterbridge,ghcr.io/42wim/matterbridge
flavor: |
latest=true
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern=stable
type=semver,pattern={{major}}
type=semver,pattern={{major}}.{{minor}}
-
name: Login to DockerHub
uses: docker/login-action@v1
if: github.event_name != 'pull_request'
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
-
name: Log into registry ghcr.io
uses: docker/login-action@v1
if: github.event_name != 'pull_request'
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
-
name: Build and push
id: docker_build
uses: docker/build-push-action@v2
with:
context: .
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}

9
.gitignore vendored Normal file
View File

@ -0,0 +1,9 @@
# Exclude matterbridge binary
/matterbridge
/matterbridge.exe
# Exclude configuration file
matterbridge.toml
# Exclude IDE Files
.vscode

247
.golangci.yaml Normal file
View File

@ -0,0 +1,247 @@
# For full documentation of the configuration options please
# see: https://github.com/golangci/golangci-lint#config-file.
# options for analysis running
run:
# default concurrency is the available CPU number
# concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m
deadline: 5m
# exit code when at least one issue was found, default is 1
issues-exit-code: 1
# include test files or not, default is true
tests: true
# list of build tags, all linters use it. Default is empty list.
build-tags:
# which dirs to skip: they won't be analyzed;
# can use regexp here: generated.*, regexp is applied on full path;
# default value is empty list, but next dirs are always skipped independently
# from this option's value:
# vendor$, third_party$, testdata$, examples$, Godeps$, builtin$
skip-dirs: gateway/bridgemap$
# which files to skip: they will be analyzed, but issues from them
# won't be reported. Default value is empty list, but there is
# no need to include all autogenerated files, we confidently recognize
# autogenerated files. If it's not please let us know.
skip-files:
# output configuration options
output:
# colored-line-number|line-number|json|tab|checkstyle, default is "colored-line-number"
format: colored-line-number
# print lines of code with issue, default is true
print-issued-lines: true
# print linter name in the end of issue text, default is true
print-linter-name: true
# all available settings of specific linters, we can set an option for
# a given linter even if we deactivate that same linter at runtime
linters-settings:
errcheck:
# report about not checking of errors in type assertions: `a := b.(MyStruct)`;
# default is false: such cases aren't reported by default.
check-type-assertions: false
# report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`;
# default is false: such cases aren't reported by default.
check-blank: false
govet:
# report about shadowed variables
check-shadowing: true
golint:
# minimal confidence for issues, default is 0.8
min-confidence: 0.8
gofmt:
# simplify code: gofmt with `-s` option, true by default
simplify: true
goimports:
# put imports beginning with prefix after 3rd-party packages;
# it's a comma-separated list of prefixes
local-prefixes: github.com
gocyclo:
# minimal code complexity to report, 30 by default (but we recommend 10-20)
min-complexity: 15
maligned:
# print struct with more effective memory layout or not, false by default
suggest-new: true
dupl:
# tokens count to trigger issue, 150 by default
threshold: 150
goconst:
# minimal length of string constant, 3 by default
min-len: 3
# minimal occurrences count to trigger, 3 by default
min-occurrences: 3
depguard:
list-type: blacklist
include-go-root: false
packages:
# List of packages that we would want to blacklist for... reasons.
misspell:
# Correct spellings using locale preferences for US or UK.
# Default is to use a neutral variety of English.
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
lll:
# max line length, lines longer will be reported. Default is 120.
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
line-length: 150
# tab width in spaces. Default to 1.
tab-width: 1
unused:
# treat code as a program (not a library) and report unused exported identifiers; default is false.
# XXX: if you enable this setting, unused will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find funcs usages. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
unparam:
# Inspect exported functions, default is false. Set to true if no external program/library imports your code.
# XXX: if you enable this setting, unparam will report a lot of false-positives in text editors:
# if it's called for subdir of a project it can't find external interfaces. All text editor integrations
# with golangci-lint call it on a directory with the changed file.
check-exported: false
nakedret:
# make an issue if func has more lines of code than this setting and it has naked returns; default is 30
max-func-lines: 0 # Warn on all naked returns.
prealloc:
# XXX: we don't recommend using this linter before doing performance profiling.
# For most programs usage of prealloc will be a premature optimization.
# Report preallocation suggestions only on simple loops that have no returns/breaks/continues/gotos in them.
# True by default.
simple: true
range-loops: true # Report preallocation suggestions on range loops, true by default
for-loops: false # Report preallocation suggestions on for loops, false by default
gocritic:
# which checks should be enabled; can't be combined with 'disabled-checks';
# default are: [appendAssign assignOp caseOrder dupArg dupBranchBody dupCase flagDeref
# ifElseChain regexpMust singleCaseSwitch sloppyLen switchTrue typeSwitchVar underef
# unlambda unslice rangeValCopy defaultCaseOrder];
# all checks list: https://github.com/go-critic/checkers
# disabled for now - hugeParam
enabled-checks:
- appendAssign
- assignOp
- boolExprSimplify
- builtinShadow
- captLocal
- caseOrder
- commentedOutImport
- defaultCaseOrder
- dupArg
- dupBranchBody
- dupCase
- dupSubExpr
- elseif
- emptyFallthrough
- ifElseChain
- importShadow
- indexAlloc
- methodExprCall
- nestingReduce
- offBy1
- ptrToRefParam
- regexpMust
- singleCaseSwitch
- sloppyLen
- switchTrue
- typeSwitchVar
- typeUnparen
- underef
- unlambda
- unnecessaryBlock
- unslice
- valSwap
- wrapperFunc
- yodaStyleExpr
# linters that we should / shouldn't run
linters:
enable-all: true
disable:
- gochecknoglobals
- lll
- maligned
- prealloc
- wsl
- gomnd
- godox
- goerr113
- testpackage
- godot
- interfacer
- goheader
- noctx
- gci
- errorlint
- nlreturn
- exhaustivestruct
- forbidigo
- wrapcheck
- varnamelen
- ireturn
- errorlint
- tparallel
- wrapcheck
- paralleltest
- makezero
- thelper
- cyclop
- revive
- importas
- gomoddirectives
- promlinter
- tagliatelle
- errname
- typecheck
- grouper
- decorder
- maintidx
- exhaustruct
- asasalint
- execinquery
- nosnakecase
- exhaustive
- testifylint
- mnd
- depguard
# rules to deal with reported isues
issues:
# List of regexps of issue texts to exclude, empty list by default.
# But independently from this option we use default exclude patterns,
# it can be disabled by `exclude-use-default: false`. To list all
# excluded by default patterns execute `golangci-lint run --help`
exclude:
# Independently from option `exclude` we use default exclude patterns,
# it can be disabled by this option. To list all
# excluded by default patterns execute `golangci-lint run --help`.
# Default value for this option is true.
exclude-use-default: true
# Maximum issues count per one linter. Set to 0 to disable. Default is 50.
max-per-linter: 0
# Maximum count of issues with the same text. Set to 0 to disable. Default is 3.
max-same-issues: 0
# Show only new issues: if there are unstaged changes or untracked files,
# only those changes are analyzed, else only changes in HEAD~ are analyzed.
# It's a super-useful option for integration of golangci-lint into existing
# large codebase. It's not practical to fix all existing issues at the moment
# of integration: much better don't allow issues in new code.
# Default is false.
new: false
# Show only new issues created after git revision `REV`
new-from-rev: "HEAD~1"

41
.goreleaser.yml Normal file
View File

@ -0,0 +1,41 @@
release:
prerelease: auto
name_template: "{{.ProjectName}} v{{.Version}}"
builds:
- env:
- CGO_ENABLED=0
goos:
- freebsd
- windows
- darwin
- linux
- dragonfly
- netbsd
- openbsd
goarch:
- amd64
- arm
- arm64
- 386
goarm:
- 6
- 7
ldflags:
- -s -w -X github.com/42wim/matterbridge/version.GitHash={{.ShortCommit}}
archives:
-
id: matterbridge
builds:
- matterbridge
name_template: "{{ .Binary }}-{{ .Version }}-{{ .Os }}-{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}"
format: binary
files:
- none*
replacements:
386: 32bit
amd64: 64bit
checksum:
name_template: 'checksums.txt'

14
Dockerfile Normal file
View File

@ -0,0 +1,14 @@
FROM alpine AS builder
COPY . /go/src/matterbridge
RUN apk --no-cache add go git \
&& cd /go/src/matterbridge \
&& CGO_ENABLED=0 go build -mod vendor -ldflags "-X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge
FROM alpine
RUN apk --no-cache add ca-certificates mailcap
COPY --from=builder /bin/matterbridge /bin/matterbridge
RUN mkdir /etc/matterbridge \
&& touch /etc/matterbridge/matterbridge.toml \
&& ln -sf /matterbridge.toml /etc/matterbridge/matterbridge.toml
ENTRYPOINT ["/bin/matterbridge", "-conf", "/etc/matterbridge/matterbridge.toml"]

14
Dockerfile_whatsappmulti Normal file
View File

@ -0,0 +1,14 @@
FROM alpine AS builder
COPY . /go/src/matterbridge
RUN apk --no-cache add go git \
&& cd /go/src/matterbridge \
&& CGO_ENABLED=0 go build -tags whatsappmulti -mod vendor -ldflags "-X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge
FROM alpine
RUN apk --no-cache add ca-certificates mailcap
COPY --from=builder /bin/matterbridge /bin/matterbridge
RUN mkdir /etc/matterbridge \
&& touch /etc/matterbridge/matterbridge.toml \
&& ln -sf /matterbridge.toml /etc/matterbridge/matterbridge.toml
ENTRYPOINT ["/bin/matterbridge", "-conf", "/etc/matterbridge/matterbridge.toml"]

488
README.md
View File

@ -1,102 +1,444 @@
<div align="center">
# matterbridge
Simple bridge between mattermost and IRC. Uses the in/outgoing webhooks.
Relays public channel messages between mattermost and IRC.
![Matterbridge Logo](img/matterbridge-notext.gif)<br />
**A simple chat bridge**<br />
Letting people be where they want to be.<br />
<sub>Bridges between a growing number of protocols. Click below to demo or join the development chat.</sub>
Requires mattermost 1.2.0+
<sup>
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.
[Discord][mb-discord] |
[Gitter][mb-gitter] |
[IRC][mb-irc] |
[Keybase][mb-keybase] |
[Matrix][mb-matrix] |
[Mattermost][mb-mattermost] |
[MSTeams][mb-msteams] |
[Rocket.Chat][mb-rocketchat] |
[Slack][mb-slack] |
[Telegram][mb-telegram] |
[Twitch][mb-twitch] |
[WhatsApp][mb-whatsapp] |
[XMPP][mb-xmpp] |
[Zulip][mb-zulip] |
And more...
</sup>
## binaries
Binaries can be found [here] (https://github.com/42wim/matterbridge/releases/tag/v0.3)
---
## building
Make sure you have [Go](https://golang.org/doc/install) properly installed, including setting up your [GOPATH] (https://golang.org/doc/code.html#GOPATH)
[![Download stable](https://img.shields.io/github/release/42wim/matterbridge.svg?label=download%20stable)](https://github.com/42wim/matterbridge/releases/latest)
[![Maintainability](https://api.codeclimate.com/v1/badges/82dff70ef2ba85a6173a/maintainability)](https://codeclimate.com/github/42wim/matterbridge/maintainability)
[![Test Coverage](https://api.codeclimate.com/v1/badges/82dff70ef2ba85a6173a/test_coverage)](https://codeclimate.com/github/42wim/matterbridge/test_coverage)<br />
```
cd $GOPATH
go get github.com/42wim/matterbridge
<hr />
</div>
<div align="right"><sup>
**Note:** Matter<em>most</em> isn't required to run matter<em>bridge</em>.</sup></div>
<p>
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/PoweredByDO/DO_Powered_by_Badge_blue.svg" width="201px">
</a>
</p>
# Table of Contents
- [matterbridge](#matterbridge)
- [Table of Contents](#table-of-contents)
- [Features](#features)
- [Natively supported](#natively-supported)
- [3rd party via matterbridge api](#3rd-party-via-matterbridge-api)
- [API](#api)
- [Chat with us](#chat-with-us)
- [Screenshots](#screenshots)
- [Installing / upgrading](#installing--upgrading)
- [Binaries](#binaries)
- [Packages](#packages)
- [Building](#building)
- [Building with whatsapp (beta) multidevice support](#building-with-whatsapp-beta-multidevice-support)
- [Configuration](#configuration)
- [Basic configuration](#basic-configuration)
- [Settings](#settings)
- [Advanced configuration](#advanced-configuration)
- [Examples](#examples)
- [Bridge mattermost (off-topic) - irc (#testing)](#bridge-mattermost-off-topic---irc-testing)
- [Bridge slack (#general) - discord (general)](#bridge-slack-general---discord-general)
- [Running](#running)
- [Docker](#docker)
- [Systemd](#systemd)
- [Changelog](#changelog)
- [FAQ](#faq)
- [Related projects](#related-projects)
- [Articles / Tutorials](#articles--tutorials)
- [Thanks](#thanks)
## Features
- [Support bridging between any protocols](https://github.com/42wim/matterbridge/wiki/Features#support-bridging-between-any-protocols)
- [Support multiple gateways(bridges) for your protocols](https://github.com/42wim/matterbridge/wiki/Features#support-multiple-gatewaysbridges-for-your-protocols)
- [Message edits and deletes](https://github.com/42wim/matterbridge/wiki/Features#message-edits-and-deletes)
- Preserves threading when possible
- [Attachment / files handling](https://github.com/42wim/matterbridge/wiki/Features#attachment--files-handling)
- [Username and avatar spoofing](https://github.com/42wim/matterbridge/wiki/Features#username-and-avatar-spoofing)
- [Private groups](https://github.com/42wim/matterbridge/wiki/Features#private-groups)
- [API](https://github.com/42wim/matterbridge/wiki/Features#api)
### Natively supported
- [Discord](https://discordapp.com)
- [Gitter](https://gitter.im)
- [Harmony](https://harmonyapp.io)
- [IRC](http://www.mirc.com/servers.html)
- [Keybase](https://keybase.io)
- [Matrix](https://matrix.org)
- [Mattermost](https://github.com/mattermost/mattermost-server/)
- [Microsoft Teams](https://teams.microsoft.com)
- [Mumble](https://www.mumble.info/)
- [Nextcloud Talk](https://nextcloud.com/talk/)
- [Rocket.chat](https://rocket.chat)
- [Slack](https://slack.com)
- [Ssh-chat](https://github.com/shazow/ssh-chat)
- ~~[Steam](https://store.steampowered.com/)~~
- Not supported anymore, see [here](https://github.com/Philipp15b/go-steam/issues/94) for more info.
- [Telegram](https://telegram.org)
- [Twitch](https://twitch.tv)
- [VK](https://vk.com/)
- [WhatsApp](https://www.whatsapp.com/)
- Whatsapp legacy is natively supported
- Whatsapp multidevice beta is natively supported but you need to build yourself, see [here](#building-with-whatsapp-beta-multidevice-support)
- [XMPP](https://xmpp.org)
- [Zulip](https://zulipchat.com)
### 3rd party via matterbridge api
- [Delta Chat](https://github.com/deltachat-bot/matterdelta)
- [Minecraft](https://github.com/raws/mattercraft)
- [Minecraft](https://gitlab.com/Programie/MatterBukkit)
#### Past 3rd party projects
- [Discourse](https://github.com/DeclanHoare/matterbabble)
- [Facebook messenger](https://github.com/powerjungle/fbridge-asyncio)
- [Facebook messenger](https://github.com/VictorNine/fbridge)
- [Minecraft](https://github.com/elytra/MatterLink)
- [Reddit](https://github.com/bonehurtingjuice/mattereddit)
- [MatterAMXX](https://github.com/andrewlindberg/MatterAMXX): [Counter-Strike, half-life and more](https://forums.alliedmods.net/showthread.php?t=319430)
- [Vintage Story](https://github.com/NikkyAI/vs-matterbridge)
- [Ultima Online Emulator](https://github.com/kuoushi/ServUO-Matterbridge)
- [Teamspeak](https://github.com/Archeb/ts-matterbridge)
### API
The API is basic at the moment.
More info and examples on the [wiki](https://github.com/42wim/matterbridge/wiki/Api).
Used by the projects below. Feel free to make a PR to add your project to this list.
- [MatterLink](https://github.com/elytra/MatterLink) (Matterbridge link for Minecraft Forge server chat, archived)
- [MatterCraft](https://github.com/raws/mattercraft) (Matterbridge link for Minecraft Forge server chat)
- [MatterBukkit](https://gitlab.com/Programie/MatterBukkit) (Matterbridge link for Minecraft Bukkit/Spigot server chat)
- [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
- [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support)
- [fbridge-asyncio](https://github.com/powerjungle/fbridge-asyncio) (Facebook messenger support)
- [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
- [matterbabble](https://github.com/DeclanHoare/matterbabble) (Discourse support)
- [MatterAMXX](https://forums.alliedmods.net/showthread.php?t=319430) (Counter-Strike, half-life and more via AMXX mod)
- [Vintage Story](https://github.com/NikkyAI/vs-matterbridge)
- [ServUO-matterbridge](https://github.com/kuoushi/ServUO-Matterbridge) (A matterbridge connector for ServUO servers)
- [ts-matterbridge](https://github.com/Archeb/ts-matterbridge) (Integrate teamspeak chat with matterbridge)
- [beerchat](https://github.com/mt-mods/beerchat) (Matterbridge link for minetest)
## Chat with us
Questions or want to test on your favorite platform? Join below:
- [Discord][mb-discord]
- [Gitter][mb-gitter]
- [IRC][mb-irc]
- [Keybase][mb-keybase]
- [Matrix][mb-matrix]
- [Mattermost][mb-mattermost]
- [Rocket.Chat][mb-rocketchat]
- [Slack][mb-slack]
- [Telegram][mb-telegram]
- [Twitch][mb-twitch]
- [XMPP][mb-xmpp] (matterbridge@conference.jabber.de)
- [Zulip][mb-zulip]
## Screenshots
See <https://github.com/42wim/matterbridge/wiki>
## Installing / upgrading
### Binaries
- Latest stable release [v1.26.0](https://github.com/42wim/matterbridge/releases/latest)
- Development releases (follows master) can be downloaded [here](https://github.com/42wim/matterbridge/actions) selecting the latest green build and then artifacts.
To install or upgrade just download the latest [binary](https://github.com/42wim/matterbridge/releases/latest). On \*nix platforms you may need to make the binary executable - you can do this by running `chmod a+x` on the binary (example: `chmod a+x matterbridge-1.24.1-linux-64bit`). After downloading (and making the binary executable, if necessary), follow the instructions on the [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
### Packages
- [Overview](https://repology.org/metapackage/matterbridge/versions)
- [snap](https://snapcraft.io/matterbridge)
- [scoop](https://github.com/42wim/scoop-bucket)
## Building
Most people just want to use binaries, you can find those [here](https://github.com/42wim/matterbridge/releases/latest)
If you really want to build from source, follow these instructions:
Go 1.18+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed.
Building the binary with **all** the bridges enabled needs about 3GB RAM to compile.
You can reduce this memory requirement to 0,5GB RAM by adding the `nomsteams` tag if you don't need/use the Microsoft Teams bridge.
Matterbridge can be build without gcc/c-compiler: If you're running on windows first run `set CGO_ENABLED=0` on other platforms you prepend `CGO_ENABLED=0` to the `go build` command. (eg `CGO_ENABLED=0 go install github.com/42wim/matterbridge`)
To install the latest stable run:
```bash
go install github.com/42wim/matterbridge
```
You should now have matterbridge binary in the bin directory:
To install the latest dev run:
```bash
go install github.com/42wim/matterbridge@master
```
$ ls bin/
To install the latest stable run without msteams or zulip bridge:
```bash
go install -tags nomsteams,nozulip github.com/42wim/matterbridge
```
You should now have matterbridge binary in the ~/go/bin directory:
```bash
$ ls ~/go/bin/
matterbridge
```
## running
1) Copy the matterbridge.conf.sample to matterbridge.conf in the same directory as the matterbridge binary.
2) Edit matterbridge.conf with the settings for your environment. See below for more config information.
3) Now you can run matterbridge.
## Building with whatsapp (beta) multidevice support
```
Usage of matterbridge:
-conf="matterbridge.conf": config file
Because the library we use for Whatsapp multidevice support includes a GPL3 library we can not provide you binaries.
(as this would require the Matterbridge to change it license to GPL)
Matterbridge can be build without gcc/c-compiler: If you're running on windows first run `set CGO_ENABLED=0` on other platforms you prepend `CGO_ENABLED=0` to the `go build` command. (eg `CGO_ENABLED=0 go install github.com/42wim/matterbridge`)
So this means you have to build it yourself using the instructions below:
```bash
go install -tags whatsappmulti github.com/42wim/matterbridge@master
```
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
If you're low on memory and don't need msteams:
```bash
go install -tags nomsteams,whatsappmulti github.com/42wim/matterbridge@master
```
[IRC]
server="irc.freenode.net"
port=6667
UseTLS=false
SkipTLSVerify=true
nick="matterbot"
channel="#matterbridge"
UseSlackCircumfix=false
You should now have matterbridge binary in the ~/go/bin directory:
```bash
$ ls ~/go/bin/
matterbridge
```
## Configuration
### Basic configuration
See [howto](https://github.com/42wim/matterbridge/wiki/How-to-create-your-config) for a step by step walkthrough for creating your configuration.
### Settings
All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for each bridge.
### 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)
```toml
[irc]
[irc.libera]
Server="irc.libera.chat:6667"
Nick="yourbotname"
[mattermost]
#url is your incoming webhook url (account settings - integrations - incoming webhooks)
url="http://mattermost.yourdomain.com/hooks/incomingwebhookkey"
#port the bridge webserver will listen on
port=9999
#address the webserver will bind to
BindAddress="0.0.0.0"
showjoinpart=true #show irc users joining and parting
#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
[mattermost.work]
Server="yourmattermostserver.tld"
Team="yourteam"
Login="yourlogin"
Password="yourpass"
PrefixMessagesWithNick=true
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#multiple channel config
#token you can find in your outgoing webhook
[Token "outgoingwebhooktoken1"]
IRCChannel="#off-topic"
MMChannel="off-topic"
[[gateway]]
name="mygateway"
enable=true
[[gateway.inout]]
account="irc.libera"
channel="#testing"
[Token "outgoingwebhooktoken2"]
IRCChannel="#testing"
MMChannel="testing"
[general]
#request your API key on https://github.com/giphy/GiphyAPI. This is a public beta key
GiphyApiKey="dc6zaTOxFJmzC"
[[gateway.inout]]
account="mattermost.work"
channel="off-topic"
```
### mattermost
You'll have to configure the incoming en outgoing webhooks.
#### Bridge slack (#general) - discord (general)
* incoming webhooks
Go to "account settings" - integrations - "incoming webhooks".
Choose a channel at "Add a new incoming webhook", this will create a webhook URL right below.
This URL should be set in the matterbridge.conf in the [mattermost] section (see above)
```toml
[slack]
[slack.test]
Token="yourslacktoken"
PrefixMessagesWithNick=true
* outgoing webhooks
Go to "account settings" - integrations - "outgoing webhooks".
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.
[discord]
[discord.test]
Token="yourdiscordtoken"
Server="yourdiscordservername"
e.g. http://192.168.1.1:9999 (9999 is the port specified in [mattermost] section of matterbridge.conf)
[general]
RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> "
[[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.
```bash
Usage of ./matterbridge:
-conf string
config file (default "matterbridge.toml")
-debug
enable debug
-gops
enable gops agent
-version
show version
```
### Docker
Please take a look at the [Docker Wiki page](https://github.com/42wim/matterbridge/wiki/Deploy:-Docker) for more information.
### Systemd
Please take a look at the [Service Files page](https://github.com/42wim/matterbridge/wiki/Service-files) for more information.
## Changelog
See [changelog.md](https://github.com/42wim/matterbridge/blob/master/changelog.md)
## FAQ
See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
## Related projects
- [jwflory/ansible-role-matterbridge](https://galaxy.ansible.com/jwflory/matterbridge) (Ansible role to simplify deploying Matterbridge)
- [matterbridge autoconfig](https://github.com/patcon/matterbridge-autoconfig)
- [matterbridge config viewer](https://github.com/patcon/matterbridge-heroku-viewer)
- [matterbridge-heroku](https://github.com/cadecairos/matterbridge-heroku)
- [mattereddit](https://github.com/bonehurtingjuice/mattereddit)
- [matterlink](https://github.com/elytra/MatterLink)
- [mattermost-plugin](https://github.com/matterbridge/mattermost-plugin) - Run matterbridge as a plugin in mattermost
- [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
- [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
- [isla](https://github.com/alphachung/isla) (Bot for Discord-Telegram groups used alongside matterbridge)
- [matterbabble](https://github.com/DeclanHoare/matterbabble) (Connect Discourse threads to Matterbridge)
- [nextcloud talk](https://github.com/nextcloud/talk_matterbridge) (Integrates matterbridge in Nextcloud Talk)
- [mattercraft](https://github.com/raws/mattercraft) (Minecraft bridge)
- [vs-matterbridge](https://github.com/NikkyAI/vs-matterbridge) (Vintage Story bridge)
- [ServUO-matterbridge](https://github.com/kuoushi/ServUO-Matterbridge) (A matterbridge connector for ServUO servers)
- [ts-matterbridge](https://github.com/Archeb/ts-matterbridge) (Integrate teamspeak chat with matterbridge)
## Articles / Tutorials
- [matterbridge on kubernetes](https://medium.freecodecamp.org/using-kubernetes-to-deploy-a-chat-gateway-or-when-technology-works-like-its-supposed-to-a169a8cd69a3)
- <https://mattermost.com/blog/connect-irc-to-mattermost/>
- <https://blog.valvin.fr/2016/09/17/mattermost-et-un-channel-irc-cest-possible/>
- <https://blog.brightscout.com/top-10-mattermost-integrations/>
- <https://www.algoo.fr/blog/2018/01/19/recouvrez-votre-liberte-en-quittant-slack-pour-un-mattermost-auto-heberge/>
- <https://kopano.com/blog/matterbridge-bridging-mattermost-chat/>
- <https://www.stitcher.com/s/?eid=52382713>
- <https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/>
- <https://userlinux.net/mattermost-and-matterbridge.html>
- <https://nextcloud.com/blog/bridging-chat-services-in-talk/>
- <https://minecraftchest1.wordpress.com/2021/06/05/how-to-install-and-setup-matterbridge/>
- Youtube: [whatsapp - telegram bridging](https://www.youtube.com/watch?v=W-VXISoKtNc)
## Thanks
<p>This project is supported by:</p>
<p>
<a href="https://www.digitalocean.com/">
<img src="https://opensource.nyc3.cdn.digitaloceanspaces.com/attribution/assets/SVG/DO_Logo_horizontal_blue.svg" width="201px">
</a>
</p>
Matterbridge wouldn't exist without these libraries:
- discord - <https://github.com/bwmarrin/discordgo>
- echo - <https://github.com/labstack/echo>
- gops - <https://github.com/google/gops>
- gozulipbot - <https://github.com/ifo/gozulipbot>
- gumble - <https://github.com/layeh/gumble>
- harmony - <https://github.com/harmony-development/shibshib>
- irc - <https://github.com/lrstanley/girc>
- keybase - <https://github.com/keybase/go-keybase-chat-bot>
- matrix - <https://github.com/matrix-org/gomatrix>
- mattermost - <https://github.com/mattermost/mattermost-server>
- msgraph.go - <https://github.com/yaegashi/msgraph.go>
- mumble - <https://github.com/layeh/gumble>
- nctalk - <https://github.com/gary-kim/go-nc-talk>
- rocketchat - <https://github.com/RocketChat/Rocket.Chat.Go.SDK>
- slack - <https://github.com/nlopes/slack>
- sshchat - <https://github.com/shazow/ssh-chat>
- steam - <https://github.com/Philipp15b/go-steam>
- telegram - <https://github.com/go-telegram-bot-api/telegram-bot-api>
- tengo - <https://github.com/d5/tengo>
- vk - <https://github.com/SevereCloud/vksdk>
- whatsapp - <https://github.com/Rhymen/go-whatsapp>
- whatsapp - <https://github.com/tulir/whatsmeow>
- xmpp - <https://github.com/mattn/go-xmpp>
- zulip - <https://github.com/ifo/gozulipbot>
<!-- Links -->
[mb-discord]: https://discord.gg/AkKPtrQ
[mb-gitter]: https://gitter.im/42wim/matterbridge
[mb-irc]: https://web.libera.chat/#matterbridge
[mb-keybase]: https://keybase.io/team/matterbridge
[mb-matrix]: https://riot.im/app/#/room/#matterbridge:matrix.org
[mb-mattermost]: https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e
[mb-msteams]: https://teams.microsoft.com/join/hj92x75gd3y7
[mb-rocketchat]: https://open.rocket.chat/channel/matterbridge
[mb-slack]: https://join.slack.com/t/matterbridgechat/shared_invite/zt-2ourq2h2-7YvyYBq2WFGC~~zEzA68_Q
[mb-telegram]: https://t.me/Matterbridge
[mb-twitch]: https://www.twitch.tv/matterbridge
[mb-whatsapp]: https://www.whatsapp.com/
[mb-xmpp]: https://inverse.chat/
[mb-zulip]: https://matterbridge.zulipchat.com/register/

245
bridge/api/api.go Normal file
View File

@ -0,0 +1,245 @@
package api
import (
"encoding/base64"
"encoding/json"
"net/http"
"strings"
"sync"
"time"
"github.com/olahol/melody"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/mitchellh/mapstructure"
ring "github.com/zfjagann/golang-ring"
)
type API struct {
Messages ring.Ring
sync.RWMutex
*bridge.Config
mrouter *melody.Melody
}
type Message struct {
Text string `json:"text"`
Username string `json:"username"`
UserID string `json:"userid"`
Avatar string `json:"avatar"`
Gateway string `json:"gateway"`
}
func New(cfg *bridge.Config) bridge.Bridger {
b := &API{Config: cfg}
e := echo.New()
e.HideBanner = true
e.HidePort = true
b.mrouter = melody.New()
b.mrouter.HandleMessage(func(s *melody.Session, msg []byte) {
message := config.Message{}
err := json.Unmarshal(msg, &message)
if err != nil {
b.Log.Errorf("failed to decode message from byte[] '%s'", string(msg))
return
}
b.handleWebsocketMessage(message, s)
})
b.mrouter.HandleConnect(func(session *melody.Session) {
greet := b.getGreeting()
data, err := json.Marshal(greet)
if err != nil {
b.Log.Errorf("failed to encode message '%v'", greet)
return
}
err = session.Write(data)
if err != nil {
b.Log.Errorf("failed to write message '%s'", string(data))
return
}
// TODO: send message history buffer from `b.Messages` here
})
b.Messages = ring.Ring{}
if b.GetInt("Buffer") != 0 {
b.Messages.SetCapacity(b.GetInt("Buffer"))
}
if b.GetString("Token") != "" {
e.Use(middleware.KeyAuth(func(key string, c echo.Context) (bool, error) {
return key == b.GetString("Token"), nil
}))
}
// Set RemoteNickFormat to a sane default
if !b.IsKeySet("RemoteNickFormat") {
b.Log.Debugln("RemoteNickFormat is unset, defaulting to \"{NICK}\"")
b.Config.Config.Viper().Set(b.GetConfigKey("RemoteNickFormat"), "{NICK}")
}
e.GET("/api/health", b.handleHealthcheck)
e.GET("/api/messages", b.handleMessages)
e.GET("/api/stream", b.handleStream)
e.GET("/api/websocket", b.handleWebsocket)
e.POST("/api/message", b.handlePostMessage)
go func() {
if b.GetString("BindAddress") == "" {
b.Log.Fatalf("No BindAddress configured.")
}
b.Log.Infof("Listening on %s", b.GetString("BindAddress"))
b.Log.Fatal(e.Start(b.GetString("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) (string, error) {
b.Lock()
defer b.Unlock()
// ignore delete messages
if msg.Event == config.EventMsgDelete {
return "", nil
}
b.Log.Debugf("enqueueing message from %s on ring buffer", msg.Username)
b.Messages.Enqueue(msg)
data, err := json.Marshal(msg)
if err != nil {
b.Log.Errorf("failed to encode message '%s'", msg)
}
_ = b.mrouter.Broadcast(data)
return "", nil
}
func (b *API) handleHealthcheck(c echo.Context) error {
return c.String(http.StatusOK, "OK")
}
func (b *API) handlePostMessage(c echo.Context) error {
message := config.Message{}
if err := c.Bind(&message); err != nil {
return err
}
// these values are fixed
message.Channel = "api"
message.Protocol = "api"
message.Account = b.Account
message.ID = ""
message.Timestamp = time.Now()
var (
fm map[string]interface{}
ds string
ok bool
)
for i, f := range message.Extra["file"] {
fi := config.FileInfo{}
if fm, ok = f.(map[string]interface{}); !ok {
return echo.NewHTTPError(http.StatusInternalServerError, "invalid format for extra")
}
err := mapstructure.Decode(fm, &fi)
if err != nil {
if !strings.Contains(err.Error(), "got string") {
return err
}
}
// mapstructure doesn't decode base64 into []byte, so it must be done manually for fi.Data
if ds, ok = fm["Data"].(string); !ok {
return echo.NewHTTPError(http.StatusInternalServerError, "invalid format for data")
}
data, err := base64.StdEncoding.DecodeString(ds)
if err != nil {
return err
}
fi.Data = &data
message.Extra["file"][i] = fi
}
b.Log.Debugf("Sending message from %s on %s to gateway", message.Username, "api")
b.Remote <- message
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
}
func (b *API) getGreeting() config.Message {
return config.Message{
Event: config.EventAPIConnected,
Timestamp: time.Now(),
}
}
func (b *API) handleStream(c echo.Context) error {
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
c.Response().WriteHeader(http.StatusOK)
greet := b.getGreeting()
if err := json.NewEncoder(c.Response()).Encode(greet); err != nil {
return err
}
c.Response().Flush()
for {
select {
// TODO: this causes issues, messages should be broadcasted to all connected clients
default:
msg := b.Messages.Dequeue()
if msg != nil {
if err := json.NewEncoder(c.Response()).Encode(msg); err != nil {
return err
}
c.Response().Flush()
}
time.Sleep(100 * time.Millisecond)
case <-c.Request().Context().Done():
return nil
}
}
}
func (b *API) handleWebsocketMessage(message config.Message, s *melody.Session) {
message.Channel = "api"
message.Protocol = "api"
message.Account = b.Account
message.ID = ""
message.Timestamp = time.Now()
data, err := json.Marshal(message)
if err != nil {
b.Log.Errorf("failed to encode message for loopback '%v'", message)
return
}
_ = b.mrouter.BroadcastOthers(data, s)
b.Log.Debugf("Sending websocket message from %s on %s to gateway", message.Username, "api")
b.Remote <- message
}
func (b *API) handleWebsocket(c echo.Context) error {
err := b.mrouter.HandleRequest(c.Response(), c.Request())
if err != nil {
b.Log.Errorf("error in websocket handling '%v'", err)
return err
}
return nil
}

135
bridge/bridge.go Normal file
View File

@ -0,0 +1,135 @@
package bridge
import (
"log"
"strings"
"sync"
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/sirupsen/logrus"
)
type Bridger interface {
Send(msg config.Message) (string, error)
Connect() error
JoinChannel(channel config.ChannelInfo) error
Disconnect() error
}
type Bridge struct {
Bridger
*sync.RWMutex
Name string
Account string
Protocol string
Channels map[string]config.ChannelInfo
Joined map[string]bool
ChannelMembers *config.ChannelMembers
Log *logrus.Entry
Config config.Config
General *config.Protocol
}
type Config struct {
*Bridge
Remote chan config.Message
}
// Factory is the factory function to create a bridge
type Factory func(*Config) Bridger
func New(bridge *config.Bridge) *Bridge {
accInfo := strings.Split(bridge.Account, ".")
if len(accInfo) != 2 {
log.Fatalf("config failure, account incorrect: %s", bridge.Account)
}
protocol := accInfo[0]
name := accInfo[1]
return &Bridge{
RWMutex: new(sync.RWMutex),
Channels: make(map[string]config.ChannelInfo),
Name: name,
Protocol: protocol,
Account: bridge.Account,
Joined: make(map[string]bool),
}
}
func (b *Bridge) JoinChannels() error {
return b.joinChannels(b.Channels, b.Joined)
}
// SetChannelMembers sets the newMembers to the bridge ChannelMembers
func (b *Bridge) SetChannelMembers(newMembers *config.ChannelMembers) {
b.Lock()
b.ChannelMembers = newMembers
b.Unlock()
}
func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map[string]bool) error {
for ID, channel := range channels {
if !exists[ID] {
b.Log.Infof("%s: joining %s (ID: %s)", b.Account, channel.Name, ID)
time.Sleep(time.Duration(b.GetInt("JoinDelay")) * time.Millisecond)
err := b.JoinChannel(channel)
if err != nil {
return err
}
exists[ID] = true
}
}
return nil
}
func (b *Bridge) GetConfigKey(key string) string {
return b.Account + "." + key
}
func (b *Bridge) IsKeySet(key string) bool {
return b.Config.IsKeySet(b.GetConfigKey(key)) || b.Config.IsKeySet("general."+key)
}
func (b *Bridge) GetBool(key string) bool {
val, ok := b.Config.GetBool(b.GetConfigKey(key))
if !ok {
val, _ = b.Config.GetBool("general." + key)
}
return val
}
func (b *Bridge) GetInt(key string) int {
val, ok := b.Config.GetInt(b.GetConfigKey(key))
if !ok {
val, _ = b.Config.GetInt("general." + key)
}
return val
}
func (b *Bridge) GetString(key string) string {
val, ok := b.Config.GetString(b.GetConfigKey(key))
if !ok {
val, _ = b.Config.GetString("general." + key)
}
return val
}
func (b *Bridge) GetStringSlice(key string) []string {
val, ok := b.Config.GetStringSlice(b.GetConfigKey(key))
if !ok {
val, _ = b.Config.GetStringSlice("general." + key)
}
return val
}
func (b *Bridge) GetStringSlice2D(key string) [][]string {
val, ok := b.Config.GetStringSlice2D(b.GetConfigKey(key))
if !ok {
val, _ = b.Config.GetStringSlice2D("general." + key)
}
return val
}

442
bridge/config/config.go Normal file
View File

@ -0,0 +1,442 @@
package config
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/fsnotify/fsnotify"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
const (
EventJoinLeave = "join_leave"
EventTopicChange = "topic_change"
EventFailure = "failure"
EventFileFailureSize = "file_failure_size"
EventAvatarDownload = "avatar_download"
EventRejoinChannels = "rejoin_channels"
EventUserAction = "user_action"
EventMsgDelete = "msg_delete"
EventFileDelete = "file_delete"
EventAPIConnected = "api_connected"
EventUserTyping = "user_typing"
EventGetChannelMembers = "get_channel_members"
EventNoticeIRC = "notice_irc"
)
const ParentIDNotFound = "msg-parent-not-found"
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"`
ParentID string `json:"parent_id"`
Timestamp time.Time `json:"timestamp"`
ID string `json:"id"`
Extra map[string][]interface{}
}
func (m Message) ParentNotFound() bool {
return m.ParentID == ParentIDNotFound
}
func (m Message) ParentValid() bool {
return m.ParentID != "" && !m.ParentNotFound()
}
type FileInfo struct {
Name string
Data *[]byte
Comment string
URL string
Size int64
Avatar bool
SHA string
NativeID string
}
type ChannelInfo struct {
Name string
Account string
Direction string
ID string
SameChannel map[string]bool
Options ChannelOptions
}
type ChannelMember struct {
Username string
Nick string
UserID string
ChannelID string
ChannelName string
}
type ChannelMembers []ChannelMember
type Protocol struct {
AllowMention []string // discord
AuthCode string // steam
BindAddress string // mattermost, slack // DEPRECATED
Buffer int // api
Charset string // irc
ClientID string // msteams
ColorNicks bool // only irc for now
Debug bool // general
DebugLevel int // only for irc now
DisableWebPagePreview bool // telegram
EditSuffix string // mattermost, slack, discord, telegram, gitter
EditDisable bool // mattermost, slack, discord, telegram, gitter
HTMLDisable bool // matrix
IconURL string // mattermost, slack
IgnoreFailureOnStart bool // general
IgnoreNicks string // all protocols
IgnoreMessages string // all protocols
Jid string // xmpp
JoinDelay string // all protocols
Label string // all protocols
Login string // mattermost, matrix
LogFile string // general
MediaDownloadBlackList []string
MediaDownloadPath string // Basically MediaServerUpload, but instead of uploading it, just write it to a file on the same server.
MediaDownloadSize int // all protocols
MediaServerDownload string
MediaServerUpload string
MediaConvertTgs string // telegram
MediaConvertWebPToPNG bool // telegram
MessageDelay int // IRC, time in millisecond to wait between messages
MessageFormat string // telegram
MessageLength int // IRC, max length of a message allowed
MessageQueue int // IRC, size of message queue for flood control
MessageSplit bool // IRC, split long messages with newlines on MessageLength instead of clipping
MessageSplitMaxCount int // discord, split long messages into at most this many messages instead of clipping (MessageLength=1950 cannot be configured)
Muc string // xmpp
MxID string // matrix
Name string // all protocols
Nick string // all protocols
NickFormatter string // mattermost, slack
NickServNick string // IRC
NickServUsername string // IRC
NickServPassword string // IRC
NicksPerRow int // mattermost, slack
NoHomeServerSuffix bool // matrix
NoSendJoinPart bool // all protocols
NoTLS bool // mattermost, xmpp
Password string // IRC,mattermost,XMPP,matrix
PrefixMessagesWithNick bool // mattemost, slack
PreserveThreading bool // slack
Protocol string // all protocols
QuoteDisable bool // telegram
QuoteFormat string // telegram
QuoteLengthLimit int // telegram
RealName string // IRC
RejoinDelay int // IRC
ReplaceMessages [][]string // all protocols
ReplaceNicks [][]string // all protocols
RemoteNickFormat string // all protocols
RunCommands []string // IRC
Server string // IRC,mattermost,XMPP,discord,matrix
SessionFile string // msteams,whatsapp
ShowJoinPart bool // all protocols
ShowTopicChange bool // slack
ShowUserTyping bool // slack
ShowEmbeds bool // discord
SkipTLSVerify bool // IRC, mattermost
SkipVersionCheck bool // mattermost
StripNick bool // all protocols
StripMarkdown bool // irc
SyncTopic bool // slack
TengoModifyMessage string // general
Team string // mattermost, keybase
TeamID string // msteams
TenantID string // msteams
Token string // gitter, slack, discord, api, matrix
Topic string // zulip
URL string // mattermost, slack // DEPRECATED
UseAPI bool // mattermost, slack
UseLocalAvatar []string // discord
UseSASL bool // IRC
UseTLS bool // IRC
UseDiscriminator bool // discord
UseFirstName bool // telegram
UseUserName bool // discord, matrix, mattermost
UseInsecureURL bool // telegram
UserName string // IRC
VerboseJoinPart bool // IRC
WebhookBindAddress string // mattermost, slack
WebhookURL string // mattermost, slack
}
type ChannelOptions struct {
Key string // irc, xmpp
WebhookURL string // discord
Topic string // zulip
}
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 Tengo struct {
InMessage string
Message string
RemoteNickFormat string
OutMessage string
}
type SameChannelGateway struct {
Name string
Enable bool
Channels []string
Accounts []string
}
type BridgeValues struct {
API map[string]Protocol
IRC map[string]Protocol
Mattermost map[string]Protocol
Matrix map[string]Protocol
Slack map[string]Protocol
SlackLegacy map[string]Protocol
Steam map[string]Protocol
Gitter map[string]Protocol
XMPP map[string]Protocol
Discord map[string]Protocol
Telegram map[string]Protocol
Rocketchat map[string]Protocol
SSHChat map[string]Protocol
WhatsApp map[string]Protocol // TODO is this struct used? Search for "SlackLegacy" for example didn't return any results
Zulip map[string]Protocol
Keybase map[string]Protocol
Mumble map[string]Protocol
General Protocol
Tengo Tengo
Gateway []Gateway
SameChannelGateway []SameChannelGateway
}
type Config interface {
Viper() *viper.Viper
BridgeValues() *BridgeValues
IsKeySet(key string) bool
GetBool(key string) (bool, bool)
GetInt(key string) (int, bool)
GetString(key string) (string, bool)
GetStringSlice(key string) ([]string, bool)
GetStringSlice2D(key string) ([][]string, bool)
}
type config struct {
sync.RWMutex
logger *logrus.Entry
v *viper.Viper
cv *BridgeValues
}
// NewConfig instantiates a new configuration based on the specified configuration file path.
func NewConfig(rootLogger *logrus.Logger, cfgfile string) Config {
logger := rootLogger.WithFields(logrus.Fields{"prefix": "config"})
viper.SetConfigFile(cfgfile)
input, err := ioutil.ReadFile(cfgfile)
if err != nil {
logger.Fatalf("Failed to read configuration file: %#v", err)
}
cfgtype := detectConfigType(cfgfile)
mycfg := newConfigFromString(logger, input, cfgtype)
if mycfg.cv.General.LogFile != "" {
logfile, err := os.OpenFile(mycfg.cv.General.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
if err == nil {
logger.Info("Opening log file ", mycfg.cv.General.LogFile)
rootLogger.Out = logfile
} else {
logger.Warn("Failed to open ", mycfg.cv.General.LogFile)
}
}
if mycfg.cv.General.MediaDownloadSize == 0 {
mycfg.cv.General.MediaDownloadSize = 1000000
}
viper.WatchConfig()
viper.OnConfigChange(func(e fsnotify.Event) {
logger.Println("Config file changed:", e.Name)
})
return mycfg
}
// detectConfigType detects JSON and YAML formats, defaults to TOML.
func detectConfigType(cfgfile string) string {
fileExt := filepath.Ext(cfgfile)
switch fileExt {
case ".json":
return "json"
case ".yaml", ".yml":
return "yaml"
}
return "toml"
}
// NewConfigFromString instantiates a new configuration based on the specified string.
func NewConfigFromString(rootLogger *logrus.Logger, input []byte) Config {
logger := rootLogger.WithFields(logrus.Fields{"prefix": "config"})
return newConfigFromString(logger, input, "toml")
}
func newConfigFromString(logger *logrus.Entry, input []byte, cfgtype string) *config {
viper.SetConfigType(cfgtype)
viper.SetEnvPrefix("matterbridge")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
viper.AutomaticEnv()
if err := viper.ReadConfig(bytes.NewBuffer(input)); err != nil {
logger.Fatalf("Failed to parse the configuration: %s", err)
}
cfg := &BridgeValues{}
if err := viper.Unmarshal(cfg); err != nil {
logger.Fatalf("Failed to load the configuration: %s", err)
}
return &config{
logger: logger,
v: viper.GetViper(),
cv: cfg,
}
}
func (c *config) BridgeValues() *BridgeValues {
return c.cv
}
func (c *config) Viper() *viper.Viper {
return c.v
}
func (c *config) IsKeySet(key string) bool {
c.RLock()
defer c.RUnlock()
return c.v.IsSet(key)
}
func (c *config) GetBool(key string) (bool, bool) {
c.RLock()
defer c.RUnlock()
return c.v.GetBool(key), c.v.IsSet(key)
}
func (c *config) GetInt(key string) (int, bool) {
c.RLock()
defer c.RUnlock()
return c.v.GetInt(key), c.v.IsSet(key)
}
func (c *config) GetString(key string) (string, bool) {
c.RLock()
defer c.RUnlock()
return c.v.GetString(key), c.v.IsSet(key)
}
func (c *config) GetStringSlice(key string) ([]string, bool) {
c.RLock()
defer c.RUnlock()
return c.v.GetStringSlice(key), c.v.IsSet(key)
}
func (c *config) GetStringSlice2D(key string) ([][]string, bool) {
c.RLock()
defer c.RUnlock()
res, ok := c.v.Get(key).([]interface{})
if !ok {
return nil, false
}
var result [][]string
for _, entry := range res {
result2 := []string{}
for _, entry2 := range entry.([]interface{}) {
result2 = append(result2, entry2.(string))
}
result = append(result, result2)
}
return result, true
}
func GetIconURL(msg *Message, iconURL string) string {
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
}
type TestConfig struct {
Config
Overrides map[string]interface{}
}
func (c *TestConfig) IsKeySet(key string) bool {
_, ok := c.Overrides[key]
return ok || c.Config.IsKeySet(key)
}
func (c *TestConfig) GetBool(key string) (bool, bool) {
val, ok := c.Overrides[key]
if ok {
return val.(bool), true
}
return c.Config.GetBool(key)
}
func (c *TestConfig) GetInt(key string) (int, bool) {
if val, ok := c.Overrides[key]; ok {
return val.(int), true
}
return c.Config.GetInt(key)
}
func (c *TestConfig) GetString(key string) (string, bool) {
if val, ok := c.Overrides[key]; ok {
return val.(string), true
}
return c.Config.GetString(key)
}
func (c *TestConfig) GetStringSlice(key string) ([]string, bool) {
if val, ok := c.Overrides[key]; ok {
return val.([]string), true
}
return c.Config.GetStringSlice(key)
}
func (c *TestConfig) GetStringSlice2D(key string) ([][]string, bool) {
if val, ok := c.Overrides[key]; ok {
return val.([][]string), true
}
return c.Config.GetStringSlice2D(key)
}

405
bridge/discord/discord.go Normal file
View File

@ -0,0 +1,405 @@
package bdiscord
import (
"bytes"
"fmt"
"strings"
"sync"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/discord/transmitter"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/bwmarrin/discordgo"
lru "github.com/hashicorp/golang-lru"
)
const (
MessageLength = 1950
cFileUpload = "file_upload"
)
type Bdiscord struct {
*bridge.Config
c *discordgo.Session
nick string
userID string
guildID string
channelsMutex sync.RWMutex
channels []*discordgo.Channel
channelInfoMap map[string]*config.ChannelInfo
membersMutex sync.RWMutex
userMemberMap map[string]*discordgo.Member
nickMemberMap map[string]*discordgo.Member
// Webhook specific logic
useAutoWebhooks bool
transmitter *transmitter.Transmitter
cache *lru.Cache
}
func New(cfg *bridge.Config) bridge.Bridger {
newCache, err := lru.New(5000)
if err != nil {
cfg.Log.Fatalf("Could not create LRU cache: %v", err)
}
b := &Bdiscord{
Config: cfg,
cache: newCache,
}
b.userMemberMap = make(map[string]*discordgo.Member)
b.nickMemberMap = make(map[string]*discordgo.Member)
b.channelInfoMap = make(map[string]*config.ChannelInfo)
b.useAutoWebhooks = b.GetBool("AutoWebhooks")
if b.useAutoWebhooks {
b.Log.Debug("Using automatic webhooks")
}
return b
}
func (b *Bdiscord) Connect() error {
var err error
token := b.GetString("Token")
b.Log.Info("Connecting")
if !strings.HasPrefix(b.GetString("Token"), "Bot ") {
token = "Bot " + b.GetString("Token")
}
// if we have a User token, remove the `Bot` prefix
if strings.HasPrefix(b.GetString("Token"), "User ") {
token = strings.Replace(b.GetString("Token"), "User ", "", -1)
}
b.c, err = discordgo.New(token)
if err != nil {
return err
}
b.Log.Info("Connection succeeded")
// Add privileged intent for guild member tracking. This is needed to track nicks
// for display names and @mention translation
b.c.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged |
discordgo.IntentsGuildMembers)
err = b.c.Open()
if err != nil {
return err
}
guilds, err := b.c.UserGuilds(100, "", "", false)
if err != nil {
return err
}
userinfo, err := b.c.User("@me")
if err != nil {
return err
}
serverName := strings.Replace(b.GetString("Server"), "ID:", "", -1)
b.nick = userinfo.Username
b.userID = userinfo.ID
// Try and find this account's guild, and populate channels
b.channelsMutex.Lock()
for _, guild := range guilds {
// Skip, if the server name does not match the visible name or the ID
if guild.Name != serverName && guild.ID != serverName {
continue
}
// Complain about an ambiguous Server setting. Two Discord servers could have the same title!
// For IDs, practically this will never happen. It would only trigger if some server's name is also an ID.
if b.guildID != "" {
return fmt.Errorf("found multiple Discord servers with the same name %#v, expected to see only one", serverName)
}
// Getting this guild's channel could result in a permission error
b.channels, err = b.c.GuildChannels(guild.ID)
if err != nil {
return fmt.Errorf("could not get %#v's channels: %w", b.GetString("Server"), err)
}
b.guildID = guild.ID
}
b.channelsMutex.Unlock()
// If we couldn't find a guild, we print extra debug information and return a nice error
if b.guildID == "" {
err = fmt.Errorf("could not find Discord server %#v", b.GetString("Server"))
b.Log.Error(err.Error())
// Print all of the possible server values
b.Log.Info("Possible server values:")
for _, guild := range guilds {
b.Log.Infof("\t- Server=%#v # by name", guild.Name)
b.Log.Infof("\t- Server=%#v # by ID", guild.ID)
}
// If there are no results, we should say that
if len(guilds) == 0 {
b.Log.Info("\t- (none found)")
}
return err
}
// Legacy note: WebhookURL used to have an actual webhook URL that we would edit,
// but we stopped doing that due to Discord making rate limits more aggressive.
//
// Even older: the same WebhookURL used to be used by every channel, which is usually unexpected.
// This is no longer possible.
if b.GetString("WebhookURL") != "" {
message := "The global WebhookURL setting has been removed. "
message += "You can get similar \"webhook editing\" behaviour by replacing this line with `AutoWebhooks=true`. "
message += "If you rely on the old-OLD (non-editing) behaviour, can move the WebhookURL to specific channel sections."
b.Log.Errorln(message)
return fmt.Errorf("use of removed WebhookURL setting")
}
if b.GetInt("debuglevel") == 2 {
b.Log.Debug("enabling even more discord debug")
b.c.Debug = true
}
// Initialise webhook management
b.transmitter = transmitter.New(b.c, b.guildID, "matterbridge", b.useAutoWebhooks)
b.transmitter.Log = b.Log
var webhookChannelIDs []string
for _, channel := range b.Channels {
channelID := b.getChannelID(channel.Name) // note(qaisjp): this readlocks channelsMutex
// If a WebhookURL was not explicitly provided for this channel,
// there are two options: just a regular bot message (ugly) or this is should be webhook sent
if channel.Options.WebhookURL == "" {
// If it should be webhook sent, we should enforce this via the transmitter
if b.useAutoWebhooks {
webhookChannelIDs = append(webhookChannelIDs, channelID)
}
continue
}
whID, whToken, ok := b.splitURL(channel.Options.WebhookURL)
if !ok {
return fmt.Errorf("failed to parse WebhookURL %#v for channel %#v", channel.Options.WebhookURL, channel.ID)
}
b.transmitter.AddWebhook(channelID, &discordgo.Webhook{
ID: whID,
Token: whToken,
GuildID: b.guildID,
ChannelID: channelID,
})
}
if b.useAutoWebhooks {
err = b.transmitter.RefreshGuildWebhooks(webhookChannelIDs)
if err != nil {
b.Log.WithError(err).Println("transmitter could not refresh guild webhooks")
return err
}
}
// Obtaining guild members and initializing nickname mapping.
b.membersMutex.Lock()
defer b.membersMutex.Unlock()
members, err := b.c.GuildMembers(b.guildID, "", 1000)
if err != nil {
b.Log.Error("Error obtaining server members: ", err)
return err
}
for _, member := range members {
if member == nil {
b.Log.Warnf("Skipping missing information for a user.")
continue
}
b.userMemberMap[member.User.ID] = member
b.nickMemberMap[member.User.Username] = member
if member.Nick != "" {
b.nickMemberMap[member.Nick] = member
}
}
b.c.AddHandler(b.messageCreate)
b.c.AddHandler(b.messageTyping)
b.c.AddHandler(b.messageUpdate)
b.c.AddHandler(b.messageDelete)
b.c.AddHandler(b.messageDeleteBulk)
b.c.AddHandler(b.memberAdd)
b.c.AddHandler(b.memberRemove)
b.c.AddHandler(b.memberUpdate)
if b.GetInt("debuglevel") == 1 {
b.c.AddHandler(b.messageEvent)
}
return nil
}
func (b *Bdiscord) Disconnect() error {
return b.c.Close()
}
func (b *Bdiscord) JoinChannel(channel config.ChannelInfo) error {
b.channelsMutex.Lock()
defer b.channelsMutex.Unlock()
b.channelInfoMap[channel.ID] = &channel
return nil
}
func (b *Bdiscord) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg)
channelID := b.getChannelID(msg.Channel)
if channelID == "" {
return "", fmt.Errorf("Could not find channelID for %v", msg.Channel)
}
if msg.Event == config.EventUserTyping {
if b.GetBool("ShowUserTyping") {
err := b.c.ChannelTyping(channelID)
return "", err
}
return "", nil
}
// Make a action /me of the message
if msg.Event == config.EventUserAction {
msg.Text = "_" + msg.Text + "_"
}
// Handle prefix hint for unthreaded messages.
if msg.ParentNotFound() {
msg.ParentID = ""
}
// Use webhook to send the message
useWebhooks := b.shouldMessageUseWebhooks(&msg)
if useWebhooks && msg.Event != config.EventMsgDelete && msg.ParentID == "" {
return b.handleEventWebhook(&msg, channelID)
}
return b.handleEventBotUser(&msg, channelID)
}
// handleEventDirect handles events via the bot user
func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (string, error) {
b.Log.Debugf("Broadcasting using token (API)")
// Delete message
if msg.Event == config.EventMsgDelete {
if msg.ID == "" {
return "", nil
}
err := b.c.ChannelMessageDelete(channelID, msg.ID)
return "", err
}
// Delete a file
if msg.Event == config.EventFileDelete {
if msg.ID == "" {
return "", nil
}
if fi, ok := b.cache.Get(cFileUpload + msg.ID); ok {
err := b.c.ChannelMessageDelete(channelID, fi.(string)) // nolint:forcetypeassert
b.cache.Remove(cFileUpload + msg.ID)
return "", err
}
return "", fmt.Errorf("file %s not found", msg.ID)
}
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(msg, b.General) {
// TODO: Use ClipOrSplitMessage
rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength, b.GetString("MessageClipped"))
if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil {
b.Log.Errorf("Could not send message %#v: %s", rmsg, err)
}
}
// check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 {
return b.handleUploadFile(msg, channelID)
}
}
// Edit message
if msg.ID != "" {
// Exploit that a discord message ID is actually just a large number, and we encode a list of IDs by separating them with ";".
msgIds := strings.Split(msg.ID, ";")
msgParts := helper.ClipOrSplitMessage(b.replaceUserMentions(msg.Text), MessageLength, b.GetString("MessageClipped"), len(msgIds))
for len(msgParts) < len(msgIds) {
msgParts = append(msgParts, "((obsoleted by edit))")
}
for i := range msgParts {
// In case of split-messages where some parts remain the same (i.e. only a typo-fix in a huge message), this causes some noop-updates.
// TODO: Optimize away noop-updates of un-edited messages
// TODO: Use RemoteNickFormat instead of this broken concatenation
_, err := b.c.ChannelMessageEdit(channelID, msgIds[i], msg.Username+msgParts[i])
if err != nil {
return "", err
}
}
return msg.ID, nil
}
msgParts := helper.ClipOrSplitMessage(b.replaceUserMentions(msg.Text), MessageLength, b.GetString("MessageClipped"), b.GetInt("MessageSplitMaxCount"))
msgIds := []string{}
for _, msgPart := range msgParts {
m := discordgo.MessageSend{
Content: msg.Username + msgPart,
AllowedMentions: b.getAllowedMentions(),
}
if msg.ParentValid() {
m.Reference = &discordgo.MessageReference{
MessageID: msg.ParentID,
ChannelID: channelID,
GuildID: b.guildID,
}
}
// Post normal message
res, err := b.c.ChannelMessageSendComplex(channelID, &m)
if err != nil {
return "", err
}
msgIds = append(msgIds, res.ID)
}
// Exploit that a discord message ID is actually just a large number, so we encode a list of IDs by separating them with ";".
return strings.Join(msgIds, ";"), nil
}
// handleUploadFile handles native upload of files
func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (string, error) {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
file := discordgo.File{
Name: fi.Name,
ContentType: "",
Reader: bytes.NewReader(*fi.Data),
}
m := discordgo.MessageSend{
Content: msg.Username + fi.Comment,
Files: []*discordgo.File{&file},
AllowedMentions: b.getAllowedMentions(),
}
res, err := b.c.ChannelMessageSendComplex(channelID, &m)
if err != nil {
return "", fmt.Errorf("file upload failed: %s", err)
}
// link file_upload_nativeID (file ID from the original bridge) to our upload id
// so that we can remove this later when it eg needs to be deleted
b.cache.Add(cFileUpload+fi.NativeID, res.ID)
}
return "", nil
}

281
bridge/discord/handlers.go Normal file
View File

@ -0,0 +1,281 @@
package bdiscord
import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/bwmarrin/discordgo"
"github.com/davecgh/go-spew/spew"
)
func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) { //nolint:unparam
if m.GuildID != b.guildID {
b.Log.Debugf("Ignoring messageDelete because it originates from a different guild")
return
}
rmsg := config.Message{Account: b.Account, ID: m.ID, Event: config.EventMsgDelete, Text: config.EventMsgDelete}
rmsg.Channel = b.getChannelName(m.ChannelID)
b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
// TODO(qaisjp): if other bridges support bulk deletions, it could be fanned out centrally
func (b *Bdiscord) messageDeleteBulk(s *discordgo.Session, m *discordgo.MessageDeleteBulk) { //nolint:unparam
if m.GuildID != b.guildID {
b.Log.Debugf("Ignoring messageDeleteBulk because it originates from a different guild")
return
}
for _, msgID := range m.Messages {
rmsg := config.Message{
Account: b.Account,
ID: msgID,
Event: config.EventMsgDelete,
Text: config.EventMsgDelete,
Channel: b.getChannelName(m.ChannelID),
}
b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
}
func (b *Bdiscord) messageEvent(s *discordgo.Session, m *discordgo.Event) {
b.Log.Debug(spew.Sdump(m.Struct))
}
func (b *Bdiscord) messageTyping(s *discordgo.Session, m *discordgo.TypingStart) {
if m.GuildID != b.guildID {
b.Log.Debugf("Ignoring messageTyping because it originates from a different guild")
return
}
if !b.GetBool("ShowUserTyping") {
return
}
// Ignore our own typing messages
if m.UserID == b.userID {
return
}
rmsg := config.Message{Account: b.Account, Event: config.EventUserTyping}
rmsg.Channel = b.getChannelName(m.ChannelID)
b.Remote <- rmsg
}
func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) { //nolint:unparam
if m.GuildID != b.guildID {
b.Log.Debugf("Ignoring messageUpdate because it originates from a different guild")
return
}
if b.GetBool("EditDisable") {
return
}
// only when message is actually edited
if m.Message.EditedTimestamp != nil {
b.Log.Debugf("Sending edit message")
m.Content += b.GetString("EditSuffix")
msg := &discordgo.MessageCreate{
Message: m.Message,
}
b.messageCreate(s, msg)
}
}
func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { //nolint:unparam
if m.GuildID != b.guildID {
b.Log.Debugf("Ignoring messageCreate because it originates from a different guild")
return
}
var err error
// not relay our own messages
if m.Author.Username == b.nick {
return
}
// if using webhooks, do not relay if it's ours
if m.Author.Bot && b.transmitter.HasWebhook(m.Author.ID) {
return
}
// add the url of the attachments to content
if len(m.Attachments) > 0 {
for _, attach := range m.Attachments {
m.Content = m.Content + "\n" + attach.URL
}
}
rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg", UserID: m.Author.ID, ID: m.ID}
b.Log.Debugf("== Receiving event %#v", m.Message)
if m.Content != "" {
m.Message.Content = b.replaceChannelMentions(m.Message.Content)
rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c)
if err != nil {
b.Log.Errorf("ContentWithMoreMentionsReplaced failed: %s", err)
rmsg.Text = m.ContentWithMentionsReplaced()
}
}
// set channel name
rmsg.Channel = b.getChannelName(m.ChannelID)
fromWebhook := m.WebhookID != ""
if !fromWebhook && !b.GetBool("UseUserName") {
rmsg.Username = b.getNick(m.Author, m.GuildID)
} else {
rmsg.Username = m.Author.Username
if !fromWebhook && b.GetBool("UseDiscriminator") {
rmsg.Username += "#" + m.Author.Discriminator
}
}
// if we have embedded content add it to text
if b.GetBool("ShowEmbeds") && m.Message.Embeds != nil {
for _, embed := range m.Message.Embeds {
rmsg.Text += handleEmbed(embed)
}
}
// no empty messages
if rmsg.Text == "" {
return
}
// do we have a /me action
var ok bool
rmsg.Text, ok = b.replaceAction(rmsg.Text)
if ok {
rmsg.Event = config.EventUserAction
}
// Replace emotes
rmsg.Text = replaceEmotes(rmsg.Text)
// Add our parent id if it exists, and if it's not referring to a message in another channel
if ref := m.MessageReference; ref != nil && ref.ChannelID == m.ChannelID {
rmsg.ParentID = ref.MessageID
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) {
if m.GuildID != b.guildID {
b.Log.Debugf("Ignoring memberUpdate because it originates from a different guild")
return
}
if m.Member == nil {
b.Log.Warnf("Received member update with no member information: %#v", m)
}
b.membersMutex.Lock()
defer b.membersMutex.Unlock()
if currMember, ok := b.userMemberMap[m.Member.User.ID]; ok {
b.Log.Debugf(
"%s: memberupdate: user %s (nick %s) changes nick to %s",
b.Account,
m.Member.User.Username,
b.userMemberMap[m.Member.User.ID].Nick,
m.Member.Nick,
)
delete(b.nickMemberMap, currMember.User.Username)
delete(b.nickMemberMap, currMember.Nick)
delete(b.userMemberMap, m.Member.User.ID)
}
b.userMemberMap[m.Member.User.ID] = m.Member
b.nickMemberMap[m.Member.User.Username] = m.Member
if m.Member.Nick != "" {
b.nickMemberMap[m.Member.Nick] = m.Member
}
}
func (b *Bdiscord) memberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd) {
if m.GuildID != b.guildID {
b.Log.Debugf("Ignoring memberAdd because it originates from a different guild")
return
}
if b.GetBool("nosendjoinpart") {
return
}
if m.Member == nil {
b.Log.Warnf("Received member update with no member information: %#v", m)
return
}
username := m.Member.User.Username
if m.Member.Nick != "" {
username = m.Member.Nick
}
rmsg := config.Message{
Account: b.Account,
Event: config.EventJoinLeave,
Username: "system",
Text: username + " joins",
}
b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
func (b *Bdiscord) memberRemove(s *discordgo.Session, m *discordgo.GuildMemberRemove) {
if m.GuildID != b.guildID {
b.Log.Debugf("Ignoring memberRemove because it originates from a different guild")
return
}
if b.GetBool("nosendjoinpart") {
return
}
if m.Member == nil {
b.Log.Warnf("Received member update with no member information: %#v", m)
return
}
username := m.Member.User.Username
if m.Member.Nick != "" {
username = m.Member.Nick
}
rmsg := config.Message{
Account: b.Account,
Event: config.EventJoinLeave,
Username: "system",
Text: username + " leaves",
}
b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
func handleEmbed(embed *discordgo.MessageEmbed) string {
var t []string
var result string
t = append(t, embed.Title)
t = append(t, embed.Description)
t = append(t, embed.URL)
i := 0
for _, e := range t {
if e == "" {
continue
}
i++
if i == 1 {
result += " embed: " + e
continue
}
result += " - " + e
}
if result != "" {
result += "\n"
}
return result
}

View File

@ -0,0 +1,58 @@
package bdiscord
import (
"testing"
"github.com/bwmarrin/discordgo"
"github.com/stretchr/testify/assert"
)
func TestHandleEmbed(t *testing.T) {
testcases := map[string]struct {
embed *discordgo.MessageEmbed
result string
}{
"allempty": {
embed: &discordgo.MessageEmbed{},
result: "",
},
"one": {
embed: &discordgo.MessageEmbed{
Title: "blah",
},
result: " embed: blah\n",
},
"two": {
embed: &discordgo.MessageEmbed{
Title: "blah",
Description: "blah2",
},
result: " embed: blah - blah2\n",
},
"three": {
embed: &discordgo.MessageEmbed{
Title: "blah",
Description: "blah2",
URL: "blah3",
},
result: " embed: blah - blah2 - blah3\n",
},
"twob": {
embed: &discordgo.MessageEmbed{
Description: "blah2",
URL: "blah3",
},
result: " embed: blah2 - blah3\n",
},
"oneb": {
embed: &discordgo.MessageEmbed{
URL: "blah3",
},
result: " embed: blah3\n",
},
}
for name, tc := range testcases {
assert.Equalf(t, tc.result, handleEmbed(tc.embed), "Testcases %s", name)
}
}

269
bridge/discord/helpers.go Normal file
View File

@ -0,0 +1,269 @@
package bdiscord
import (
"errors"
"regexp"
"strings"
"unicode"
"github.com/bwmarrin/discordgo"
)
func (b *Bdiscord) getAllowedMentions() *discordgo.MessageAllowedMentions {
// If AllowMention is not specified, then allow all mentions (default Discord behavior)
if !b.IsKeySet("AllowMention") {
return nil
}
// Otherwise, allow only the mentions that are specified
allowedMentionTypes := make([]discordgo.AllowedMentionType, 0, 3)
for _, m := range b.GetStringSlice("AllowMention") {
switch m {
case "everyone":
allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeEveryone)
case "roles":
allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeRoles)
case "users":
allowedMentionTypes = append(allowedMentionTypes, discordgo.AllowedMentionTypeUsers)
}
}
return &discordgo.MessageAllowedMentions{
Parse: allowedMentionTypes,
}
}
func (b *Bdiscord) getNick(user *discordgo.User, guildID string) string {
b.membersMutex.RLock()
defer b.membersMutex.RUnlock()
if member, ok := b.userMemberMap[user.ID]; ok {
if member.Nick != "" {
// Only return if nick is set.
return member.Nick
}
// Otherwise return username.
return user.Username
}
// If we didn't find nick, search for it.
member, err := b.c.GuildMember(guildID, user.ID)
if err != nil {
b.Log.Warnf("Failed to fetch information for member %#v on guild %#v: %s", user, guildID, err)
return user.Username
} else if member == nil {
b.Log.Warnf("Got no information for member %#v", user)
return user.Username
}
b.userMemberMap[user.ID] = member
b.nickMemberMap[member.User.Username] = member
if member.Nick != "" {
b.nickMemberMap[member.Nick] = member
return member.Nick
}
return user.Username
}
func (b *Bdiscord) getGuildMemberByNick(nick string) (*discordgo.Member, error) {
b.membersMutex.RLock()
defer b.membersMutex.RUnlock()
if member, ok := b.nickMemberMap[strings.TrimSpace(nick)]; ok {
return member, nil
}
return nil, errors.New("Couldn't find guild member with nick " + nick) // This will most likely get ignored by the caller
}
func (b *Bdiscord) getChannelID(name string) string {
if strings.Contains(name, "/") {
return b.getCategoryChannelID(name)
}
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
idcheck := strings.Split(name, "ID:")
if len(idcheck) > 1 {
return idcheck[1]
}
for _, channel := range b.channels {
if channel.Name == name && channel.Type == discordgo.ChannelTypeGuildText {
return channel.ID
}
}
return ""
}
func (b *Bdiscord) getCategoryChannelID(name string) string {
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
res := strings.Split(name, "/")
// shouldn't happen because function should be only called from getChannelID
if len(res) != 2 {
return ""
}
catName, chanName := res[0], res[1]
for _, channel := range b.channels {
// if we have a parentID, lookup the name of that parent (category)
// and if it matches return it
if channel.Name == chanName && channel.ParentID != "" {
for _, cat := range b.channels {
if cat.ID == channel.ParentID && cat.Name == catName {
return channel.ID
}
}
}
}
return ""
}
func (b *Bdiscord) getChannelName(id string) string {
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
for _, c := range b.channelInfoMap {
if c.Name == "ID:"+id {
// if we have ID: specified in our gateway configuration return this
return c.Name
}
}
for _, channel := range b.channels {
if channel.ID == id {
return b.getCategoryChannelName(channel.Name, channel.ParentID)
}
}
return ""
}
func (b *Bdiscord) getCategoryChannelName(name, parentID string) string {
var usesCat bool
// do we have a category configuration in the channel config
for _, c := range b.channelInfoMap {
if strings.Contains(c.Name, "/") {
usesCat = true
break
}
}
// configuration without category, return the normal channel name
if !usesCat {
return name
}
// create a category/channel response
for _, c := range b.channels {
if c.ID == parentID {
name = c.Name + "/" + name
}
}
return name
}
var (
// See https://discordapp.com/developers/docs/reference#message-formatting.
channelMentionRE = regexp.MustCompile("<#[0-9]+>")
userMentionRE = regexp.MustCompile("@[^@\n]{1,32}")
emoteRE = regexp.MustCompile(`<a?(:\w+:)\d+>`)
)
func (b *Bdiscord) replaceChannelMentions(text string) string {
replaceChannelMentionFunc := func(match string) string {
channelID := match[2 : len(match)-1]
channelName := b.getChannelName(channelID)
// If we don't have the channel refresh our list.
if channelName == "" {
var err error
b.channels, err = b.c.GuildChannels(b.guildID)
if err != nil {
return "#unknownchannel"
}
channelName = b.getChannelName(channelID)
}
return "#" + channelName
}
return channelMentionRE.ReplaceAllStringFunc(text, replaceChannelMentionFunc)
}
func (b *Bdiscord) replaceUserMentions(text string) string {
replaceUserMentionFunc := func(match string) string {
var (
err error
member *discordgo.Member
username string
)
usernames := enumerateUsernames(match[1:])
for _, username = range usernames {
b.Log.Debugf("Testing mention: '%s'", username)
member, err = b.getGuildMemberByNick(username)
if err == nil {
break
}
}
if member == nil {
return match
}
return strings.Replace(match, "@"+username, member.User.Mention(), 1)
}
return userMentionRE.ReplaceAllStringFunc(text, replaceUserMentionFunc)
}
func replaceEmotes(text string) string {
return emoteRE.ReplaceAllString(text, "$1")
}
func (b *Bdiscord) replaceAction(text string) (string, bool) {
length := len(text)
if length > 1 && text[0] == '_' && text[length-1] == '_' {
return text[1 : length-1], true
}
return text, false
}
// splitURL splits a webhookURL and returns the ID and token.
func (b *Bdiscord) splitURL(url string) (string, string, bool) {
const (
expectedWebhookSplitCount = 7
webhookIdxID = 5
webhookIdxToken = 6
)
webhookURLSplit := strings.Split(url, "/")
if len(webhookURLSplit) != expectedWebhookSplitCount {
return "", "", false
}
return webhookURLSplit[webhookIdxID], webhookURLSplit[webhookIdxToken], true
}
func enumerateUsernames(s string) []string {
onlySpace := true
for _, r := range s {
if !unicode.IsSpace(r) {
onlySpace = false
break
}
}
if onlySpace {
return nil
}
var username, endSpace string
var usernames []string
skippingSpace := true
for _, r := range s {
if unicode.IsSpace(r) {
if !skippingSpace {
usernames = append(usernames, username)
skippingSpace = true
}
endSpace += string(r)
username += string(r)
} else {
endSpace = ""
username += string(r)
skippingSpace = false
}
}
if endSpace == "" {
usernames = append(usernames, username)
}
return usernames
}

View File

@ -0,0 +1,46 @@
package bdiscord
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEnumerateUsernames(t *testing.T) {
testcases := map[string]struct {
match string
expectedUsernames []string
}{
"only space": {
match: " \t\n \t",
expectedUsernames: nil,
},
"single word": {
match: "veni",
expectedUsernames: []string{"veni"},
},
"single word with preceeding space": {
match: " vidi",
expectedUsernames: []string{" vidi"},
},
"single word with suffixed space": {
match: "vici ",
expectedUsernames: []string{"vici"},
},
"multi-word with varying whitespace": {
match: "just me and\tmy friends \t",
expectedUsernames: []string{
"just",
"just me",
"just me and",
"just me and\tmy",
"just me and\tmy friends",
},
},
}
for testname, testcase := range testcases {
foundUsernames := enumerateUsernames(testcase.match)
assert.Equalf(t, testcase.expectedUsernames, foundUsernames, "Should have found the expected usernames for testcase %s", testname)
}
}

View File

@ -0,0 +1,257 @@
// Package transmitter provides functionality for transmitting
// arbitrary webhook messages to Discord.
//
// The package provides the following functionality:
//
// - Creating new webhooks, whenever necessary
// - Loading webhooks that we have previously created
// - Sending new messages
// - Editing messages, via message ID
// - Deleting messages, via message ID
//
// The package has been designed for matterbridge, but with other
// Go bots in mind. The public API should be matterbridge-agnostic.
package transmitter
import (
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/bwmarrin/discordgo"
log "github.com/sirupsen/logrus"
)
// A Transmitter represents a message manager for a single guild.
type Transmitter struct {
session *discordgo.Session
guild string
title string
autoCreate bool
// channelWebhooks maps from a channel ID to a webhook instance
channelWebhooks map[string]*discordgo.Webhook
mutex sync.RWMutex
Log *log.Entry
}
// ErrWebhookNotFound is returned when a valid webhook for this channel/message combination does not exist
var ErrWebhookNotFound = errors.New("webhook for this channel and message does not exist")
// ErrPermissionDenied is returned if the bot does not have permission to manage webhooks.
//
// Bots can be granted a guild-wide permission and channel-specific permissions to manage webhooks.
// Despite potentially having guild-wide permission, channel specific overrides could deny a bot's permission to manage webhooks.
var ErrPermissionDenied = errors.New("missing 'Manage Webhooks' permission")
// New returns a new Transmitter given a Discord session, guild ID, and title.
func New(session *discordgo.Session, guild string, title string, autoCreate bool) *Transmitter {
return &Transmitter{
session: session,
guild: guild,
title: title,
autoCreate: autoCreate,
channelWebhooks: make(map[string]*discordgo.Webhook),
Log: log.NewEntry(log.StandardLogger()),
}
}
// Send transmits a message to the given channel with the provided webhook data, and waits until Discord responds with message data.
func (t *Transmitter) Send(channelID string, params *discordgo.WebhookParams) (*discordgo.Message, error) {
wh, err := t.getOrCreateWebhook(channelID)
if err != nil {
return nil, err
}
msg, err := t.session.WebhookExecute(wh.ID, wh.Token, true, params)
if err != nil {
return nil, fmt.Errorf("execute failed: %w", err)
}
return msg, nil
}
// Edit will edit a message in a channel, if possible.
func (t *Transmitter) Edit(channelID string, messageID string, params *discordgo.WebhookParams) error {
wh := t.getWebhook(channelID)
if wh == nil {
return ErrWebhookNotFound
}
uri := discordgo.EndpointWebhookToken(wh.ID, wh.Token) + "/messages/" + messageID
_, err := t.session.RequestWithBucketID("PATCH", uri, params, discordgo.EndpointWebhookToken("", ""))
if err != nil {
return err
}
return nil
}
// HasWebhook checks whether the transmitter is using a particular webhook.
func (t *Transmitter) HasWebhook(id string) bool {
t.mutex.RLock()
defer t.mutex.RUnlock()
for _, wh := range t.channelWebhooks {
if wh.ID == id {
return true
}
}
return false
}
// AddWebhook allows you to register a channel's webhook with the transmitter.
func (t *Transmitter) AddWebhook(channelID string, webhook *discordgo.Webhook) bool {
t.Log.Debugf("Manually added webhook %#v to channel %#v", webhook.ID, channelID)
t.mutex.Lock()
defer t.mutex.Unlock()
_, replaced := t.channelWebhooks[channelID]
t.channelWebhooks[channelID] = webhook
return replaced
}
// RefreshGuildWebhooks loads "relevant" webhooks into the transmitter, with careful permission handling.
//
// Notes:
//
// - A webhook is "relevant" if it was created by this bot -- the ApplicationID should match the bot's ID.
// - The term "having permission" means having the "Manage Webhooks" permission. See ErrPermissionDenied for more information.
// - This function is additive and will not unload previously loaded webhooks.
// - A nil channelIDs slice is treated the same as an empty one.
//
// If the bot has guild-wide permission:
//
// 1. it will load any "relevant" webhooks from the entire guild
// 2. the given slice is ignored
//
// If the bot does not have guild-wide permission:
//
// 1. it will load any "relevant" webhooks in each channel
// 2. a single error will be returned if any error occurs (incl. if there is no permission for any of these channels)
//
// If any channel has more than one "relevant" webhook, it will randomly pick one.
func (t *Transmitter) RefreshGuildWebhooks(channelIDs []string) error {
t.Log.Debugln("Refreshing guild webhooks")
botID, err := getDiscordUserID(t.session)
if err != nil {
return fmt.Errorf("could not get current user: %w", err)
}
// Get all existing webhooks
hooks, err := t.session.GuildWebhooks(t.guild)
if err != nil {
switch {
case isDiscordPermissionError(err):
// We fallback on manually fetching hooks from individual channels
// if we don't have the "Manage Webhooks" permission globally.
// We can only do this if we were provided channelIDs, though.
if len(channelIDs) == 0 {
return ErrPermissionDenied
}
t.Log.Debugln("Missing global 'Manage Webhooks' permission, falling back on per-channel permission")
return t.fetchChannelsHooks(channelIDs, botID)
default:
return fmt.Errorf("could not get webhooks: %w", err)
}
}
t.Log.Debugln("Refreshing guild webhooks using global permission")
t.assignHooksByAppID(hooks, botID, false)
return nil
}
// createWebhook creates a webhook for a specific channel.
func (t *Transmitter) createWebhook(channel string) (*discordgo.Webhook, error) {
t.mutex.Lock()
defer t.mutex.Unlock()
wh, err := t.session.WebhookCreate(channel, t.title+time.Now().Format(" 3:04:05PM"), "")
if err != nil {
return nil, err
}
t.channelWebhooks[channel] = wh
return wh, nil
}
func (t *Transmitter) getWebhook(channel string) *discordgo.Webhook {
t.mutex.RLock()
defer t.mutex.RUnlock()
return t.channelWebhooks[channel]
}
func (t *Transmitter) getOrCreateWebhook(channelID string) (*discordgo.Webhook, error) {
// If we have a webhook for this channel, immediately return it
wh := t.getWebhook(channelID)
if wh != nil {
return wh, nil
}
// Early exit if we don't want to automatically create one
if !t.autoCreate {
return nil, ErrWebhookNotFound
}
t.Log.Infof("Creating a webhook for %s\n", channelID)
wh, err := t.createWebhook(channelID)
if err != nil {
return nil, fmt.Errorf("could not create webhook: %w", err)
}
return wh, nil
}
// fetchChannelsHooks fetches hooks for the given channelIDs and calls assignHooksByAppID for each channel's hooks
func (t *Transmitter) fetchChannelsHooks(channelIDs []string, botID string) error {
// For each channel, search for relevant hooks
var failedHooks []string
for _, channelID := range channelIDs {
hooks, err := t.session.ChannelWebhooks(channelID)
if err != nil {
failedHooks = append(failedHooks, "\n- "+channelID+": "+err.Error())
continue
}
t.assignHooksByAppID(hooks, botID, true)
}
// Compose an error if any hooks failed
if len(failedHooks) > 0 {
return errors.New("failed to fetch hooks:" + strings.Join(failedHooks, ""))
}
return nil
}
func (t *Transmitter) assignHooksByAppID(hooks []*discordgo.Webhook, appID string, channelTargeted bool) {
logLine := "Picking up webhook"
if channelTargeted {
logLine += " (channel targeted)"
}
t.mutex.Lock()
defer t.mutex.Unlock()
for _, wh := range hooks {
if wh.ApplicationID != appID {
continue
}
t.channelWebhooks[wh.ChannelID] = wh
t.Log.WithFields(log.Fields{
"id": wh.ID,
"name": wh.Name,
"channel": wh.ChannelID,
}).Println(logLine)
}
}

View File

@ -0,0 +1,32 @@
package transmitter
import (
"github.com/bwmarrin/discordgo"
)
// isDiscordPermissionError returns false for nil, and true if a Discord RESTError with code discordgo.ErrorCodeMissionPermissions
func isDiscordPermissionError(err error) bool {
if err == nil {
return false
}
restErr, ok := err.(*discordgo.RESTError)
if !ok {
return false
}
return restErr.Message != nil && restErr.Message.Code == discordgo.ErrCodeMissingPermissions
}
// getDiscordUserID gets own user ID from state, and fallback on API request
func getDiscordUserID(session *discordgo.Session) (string, error) {
if user := session.State.User; user != nil {
return user.ID, nil
}
user, err := session.User("@me")
if err != nil {
return "", err
}
return user.ID, nil
}

179
bridge/discord/webhook.go Normal file
View File

@ -0,0 +1,179 @@
package bdiscord
import (
"bytes"
"strings"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/bwmarrin/discordgo"
)
// shouldMessageUseWebhooks checks if have a channel specific webhook, if we're not using auto webhooks
func (b *Bdiscord) shouldMessageUseWebhooks(msg *config.Message) bool {
if b.useAutoWebhooks {
return true
}
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok {
if ci.Options.WebhookURL != "" {
return true
}
}
return false
}
// maybeGetLocalAvatar checks if UseLocalAvatar contains the message's
// account or protocol, and if so, returns the Discord avatar (if exists)
func (b *Bdiscord) maybeGetLocalAvatar(msg *config.Message) string {
for _, val := range b.GetStringSlice("UseLocalAvatar") {
if msg.Protocol != val && msg.Account != val {
continue
}
member, err := b.getGuildMemberByNick(msg.Username)
if err != nil {
return ""
}
return member.User.AvatarURL("")
}
return ""
}
func (b *Bdiscord) webhookSendTextOnly(msg *config.Message, channelID string) (string, error) {
msgParts := helper.ClipOrSplitMessage(msg.Text, MessageLength, b.GetString("MessageClipped"), b.GetInt("MessageSplitMaxCount"))
msgIds := []string{}
for _, msgPart := range msgParts {
res, err := b.transmitter.Send(
channelID,
&discordgo.WebhookParams{
Content: msgPart,
Username: msg.Username,
AvatarURL: msg.Avatar,
AllowedMentions: b.getAllowedMentions(),
},
)
if err != nil {
return "", err
} else {
msgIds = append(msgIds, res.ID)
}
}
// Exploit that a discord message ID is actually just a large number, so we encode a list of IDs by separating them with ";".
return strings.Join(msgIds, ";"), nil
}
func (b *Bdiscord) webhookSendFilesOnly(msg *config.Message, channelID string) error {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo) //nolint:forcetypeassert
file := discordgo.File{
Name: fi.Name,
ContentType: "",
Reader: bytes.NewReader(*fi.Data),
}
content := fi.Comment
// Cannot use the resulting ID for any edits anyway, so throw it away.
// This has to be re-enabled when we implement message deletion.
_, err := b.transmitter.Send(
channelID,
&discordgo.WebhookParams{
Username: msg.Username,
AvatarURL: msg.Avatar,
Files: []*discordgo.File{&file},
Content: content,
AllowedMentions: b.getAllowedMentions(),
},
)
if err != nil {
b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, err)
return err
}
}
return nil
}
// webhookSend send one or more message via webhook, taking care of file
// uploads (from slack, telegram or mattermost).
// Returns messageID and error.
func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (string, error) {
var (
res string
err error
)
// If avatar is unset, mutate the message to include the local avatar (but only if settings say we should do this)
if msg.Avatar == "" {
msg.Avatar = b.maybeGetLocalAvatar(msg)
}
// WebhookParams can have either `Content` or `File`.
// We can't send empty messages.
if msg.Text != "" {
res, err = b.webhookSendTextOnly(msg, channelID)
}
if err == nil && msg.Extra != nil {
err = b.webhookSendFilesOnly(msg, channelID)
}
return res, err
}
func (b *Bdiscord) handleEventWebhook(msg *config.Message, channelID string) (string, error) {
// skip events
if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange {
return "", nil
}
// skip empty messages
if msg.Text == "" && (msg.Extra == nil || len(msg.Extra["file"]) == 0) {
b.Log.Debugf("Skipping empty message %#v", msg)
return "", nil
}
// discord username must be [0..32] max
if len(msg.Username) > 32 {
msg.Username = msg.Username[0:32]
}
if msg.ID != "" {
// Exploit that a discord message ID is actually just a large number, and we encode a list of IDs by separating them with ";".
msgIds := strings.Split(msg.ID, ";")
msgParts := helper.ClipOrSplitMessage(b.replaceUserMentions(msg.Text), MessageLength, b.GetString("MessageClipped"), len(msgIds))
for len(msgParts) < len(msgIds) {
msgParts = append(msgParts, "((obsoleted by edit))")
}
b.Log.Debugf("Editing webhook message")
var editErr error = nil
for i := range msgParts {
// In case of split-messages where some parts remain the same (i.e. only a typo-fix in a huge message), this causes some noop-updates.
// TODO: Optimize away noop-updates of un-edited messages
editErr = b.transmitter.Edit(channelID, msgIds[i], &discordgo.WebhookParams{
Content: msgParts[i],
Username: msg.Username,
AllowedMentions: b.getAllowedMentions(),
})
if editErr != nil {
break
}
}
if editErr == nil {
return msg.ID, nil
}
b.Log.Errorf("Could not edit webhook message(s): %s; sending as new message(s) instead", editErr)
}
b.Log.Debugf("Processing webhook sending for message %#v", msg)
msg.Text = b.replaceUserMentions(msg.Text)
msgID, err := b.webhookSend(msg, channelID)
if err != nil {
b.Log.Errorf("Could not broadcast via webhook for message %#v: %s", msgID, err)
return "", err
}
return msgID, nil
}

252
bridge/harmony/harmony.go Normal file
View File

@ -0,0 +1,252 @@
package harmony
import (
"fmt"
"log"
"strconv"
"strings"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/harmony-development/shibshib"
chatv1 "github.com/harmony-development/shibshib/gen/chat/v1"
typesv1 "github.com/harmony-development/shibshib/gen/harmonytypes/v1"
profilev1 "github.com/harmony-development/shibshib/gen/profile/v1"
)
type cachedProfile struct {
data *profilev1.GetProfileResponse
lastUpdated time.Time
}
type Bharmony struct {
*bridge.Config
c *shibshib.Client
profileCache map[uint64]cachedProfile
}
func uToStr(in uint64) string {
return strconv.FormatUint(in, 10)
}
func strToU(in string) (uint64, error) {
return strconv.ParseUint(in, 10, 64)
}
func New(cfg *bridge.Config) bridge.Bridger {
b := &Bharmony{
Config: cfg,
profileCache: map[uint64]cachedProfile{},
}
return b
}
func (b *Bharmony) getProfile(u uint64) (*profilev1.GetProfileResponse, error) {
if v, ok := b.profileCache[u]; ok && time.Since(v.lastUpdated) < time.Minute*10 {
return v.data, nil
}
resp, err := b.c.ProfileKit.GetProfile(&profilev1.GetProfileRequest{
UserId: u,
})
if err != nil {
if v, ok := b.profileCache[u]; ok {
return v.data, nil
}
return nil, err
}
b.profileCache[u] = cachedProfile{
data: resp,
lastUpdated: time.Now(),
}
return resp, nil
}
func (b *Bharmony) avatarFor(m *chatv1.Message) string {
if m.Overrides != nil {
return m.Overrides.GetAvatar()
}
profi, err := b.getProfile(m.AuthorId)
if err != nil {
return ""
}
return b.c.TransformHMCURL(profi.Profile.GetUserAvatar())
}
func (b *Bharmony) usernameFor(m *chatv1.Message) string {
if m.Overrides != nil {
return m.Overrides.GetUsername()
}
profi, err := b.getProfile(m.AuthorId)
if err != nil {
return ""
}
return profi.Profile.UserName
}
func (b *Bharmony) toMessage(msg *shibshib.LocatedMessage) config.Message {
message := config.Message{}
message.Account = b.Account
message.UserID = uToStr(msg.Message.AuthorId)
message.Avatar = b.avatarFor(msg.Message)
message.Username = b.usernameFor(msg.Message)
message.Channel = uToStr(msg.ChannelID)
message.ID = uToStr(msg.MessageId)
switch content := msg.Message.Content.Content.(type) {
case *chatv1.Content_EmbedMessage:
message.Text = "Embed"
case *chatv1.Content_AttachmentMessage:
var s strings.Builder
for idx, attach := range content.AttachmentMessage.Files {
s.WriteString(b.c.TransformHMCURL(attach.Id))
if idx < len(content.AttachmentMessage.Files)-1 {
s.WriteString(", ")
}
}
message.Text = s.String()
case *chatv1.Content_PhotoMessage:
var s strings.Builder
for idx, attach := range content.PhotoMessage.GetPhotos() {
s.WriteString(attach.GetCaption().GetText())
s.WriteString("\n")
s.WriteString(b.c.TransformHMCURL(attach.GetHmc()))
if idx < len(content.PhotoMessage.GetPhotos())-1 {
s.WriteString("\n\n")
}
}
message.Text = s.String()
case *chatv1.Content_TextMessage:
message.Text = content.TextMessage.Content.Text
}
return message
}
func (b *Bharmony) outputMessages() {
for {
msg := <-b.c.EventsStream()
if msg.Message.AuthorId == b.c.UserID {
continue
}
b.Remote <- b.toMessage(msg)
}
}
func (b *Bharmony) GetUint64(conf string) uint64 {
num, err := strToU(b.GetString(conf))
if err != nil {
log.Fatal(err)
}
return num
}
func (b *Bharmony) Connect() (err error) {
b.c, err = shibshib.NewClient(b.GetString("Homeserver"), b.GetString("Token"), b.GetUint64("UserID"))
if err != nil {
return
}
b.c.SubscribeToGuild(b.GetUint64("Community"))
go b.outputMessages()
return nil
}
func (b *Bharmony) send(msg config.Message) (id string, err error) {
msgChan, err := strToU(msg.Channel)
if err != nil {
return
}
retID, err := b.c.ChatKit.SendMessage(&chatv1.SendMessageRequest{
GuildId: b.GetUint64("Community"),
ChannelId: msgChan,
Content: &chatv1.Content{
Content: &chatv1.Content_TextMessage{
TextMessage: &chatv1.Content_TextContent{
Content: &chatv1.FormattedText{
Text: msg.Text,
},
},
},
},
Overrides: &chatv1.Overrides{
Username: &msg.Username,
Avatar: &msg.Avatar,
Reason: &chatv1.Overrides_Bridge{Bridge: &typesv1.Empty{}},
},
InReplyTo: nil,
EchoId: nil,
Metadata: nil,
})
if err != nil {
err = fmt.Errorf("send: error sending message: %w", err)
log.Println(err.Error())
}
return uToStr(retID.MessageId), err
}
func (b *Bharmony) delete(msg config.Message) (id string, err error) {
msgChan, err := strToU(msg.Channel)
if err != nil {
return "", err
}
msgID, err := strToU(msg.ID)
if err != nil {
return "", err
}
_, err = b.c.ChatKit.DeleteMessage(&chatv1.DeleteMessageRequest{
GuildId: b.GetUint64("Community"),
ChannelId: msgChan,
MessageId: msgID,
})
return "", err
}
func (b *Bharmony) typing(msg config.Message) (id string, err error) {
msgChan, err := strToU(msg.Channel)
if err != nil {
return "", err
}
_, err = b.c.ChatKit.Typing(&chatv1.TypingRequest{
GuildId: b.GetUint64("Community"),
ChannelId: msgChan,
})
return "", err
}
func (b *Bharmony) Send(msg config.Message) (id string, err error) {
switch msg.Event {
case "":
return b.send(msg)
case config.EventMsgDelete:
return b.delete(msg)
case config.EventUserTyping:
return b.typing(msg)
default:
return "", nil
}
}
func (b *Bharmony) JoinChannel(channel config.ChannelInfo) error {
return nil
}
func (b *Bharmony) Disconnect() error {
return nil
}

287
bridge/helper/helper.go Normal file
View File

@ -0,0 +1,287 @@
package helper
import (
"bytes"
"fmt"
"image/png"
"io"
"net/http"
"regexp"
"strings"
"time"
"unicode/utf8"
"golang.org/x/image/webp"
"github.com/42wim/matterbridge/bridge/config"
"github.com/gomarkdown/markdown"
"github.com/gomarkdown/markdown/html"
"github.com/gomarkdown/markdown/parser"
"github.com/sirupsen/logrus"
)
// DownloadFile downloads the given non-authenticated URL.
func DownloadFile(url string) (*[]byte, error) {
return DownloadFileAuth(url, "")
}
// DownloadFileAuth downloads the given URL using the specified authentication token.
func DownloadFileAuth(url string, auth string) (*[]byte, error) {
var buf bytes.Buffer
client := &http.Client{
Timeout: time.Second * 5,
}
req, err := http.NewRequest("GET", url, nil)
if auth != "" {
req.Header.Add("Authorization", auth)
}
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
io.Copy(&buf, resp.Body)
data := buf.Bytes()
return &data, nil
}
// DownloadFileAuthRocket downloads the given URL using the specified Rocket user ID and authentication token.
func DownloadFileAuthRocket(url, token, userID string) (*[]byte, error) {
var buf bytes.Buffer
client := &http.Client{
Timeout: time.Second * 5,
}
req, err := http.NewRequest("GET", url, nil)
req.Header.Add("X-Auth-Token", token)
req.Header.Add("X-User-Id", userID)
if err != nil {
return nil, err
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
_, err = io.Copy(&buf, resp.Body)
data := buf.Bytes()
return &data, err
}
// GetSubLines splits messages in newline-delimited lines. If maxLineLength is
// specified as non-zero GetSubLines will also clip long lines to the maximum
// length and insert a warning marker that the line was clipped.
//
// TODO: The current implementation has the inconvenient that it disregards
// word boundaries when splitting but this is hard to solve without potentially
// breaking formatting and other stylistic effects.
func GetSubLines(message string, maxLineLength int, clippingMessage string) []string {
if clippingMessage == "" {
clippingMessage = " <clipped message>"
}
var lines []string
for _, line := range strings.Split(strings.TrimSpace(message), "\n") {
if line == "" {
// Prevent sending empty messages, so we'll skip this line
// if it has no content.
continue
}
if maxLineLength == 0 || len([]byte(line)) <= maxLineLength {
lines = append(lines, line)
continue
}
// !!! WARNING !!!
// Before touching the splitting logic below please ensure that you PROPERLY
// understand how strings, runes and range loops over strings work in Go.
// A good place to start is to read https://blog.golang.org/strings. :-)
var splitStart int
var startOfPreviousRune int
for i := range line {
if i-splitStart > maxLineLength-len([]byte(clippingMessage)) {
lines = append(lines, line[splitStart:startOfPreviousRune]+clippingMessage)
splitStart = startOfPreviousRune
}
startOfPreviousRune = i
}
// This last append is safe to do without looking at the remaining byte-length
// as we assume that the byte-length of the last rune will never exceed that of
// the byte-length of the clipping message.
lines = append(lines, line[splitStart:])
}
return lines
}
// HandleExtra manages the supplementary details stored inside a message's 'Extra' field map.
func HandleExtra(msg *config.Message, general *config.Protocol) []config.Message {
extra := msg.Extra
rmsg := []config.Message{}
for _, f := range extra[config.EventFileFailureSize] {
fi := f.(config.FileInfo)
text := fmt.Sprintf("file %s too big to download (%#v > allowed size: %#v)", fi.Name, fi.Size, general.MediaDownloadSize)
rmsg = append(rmsg, config.Message{
Text: text,
Username: "<system> ",
Channel: msg.Channel,
Account: msg.Account,
})
}
return rmsg
}
// GetAvatar constructs a URL for a given user-avatar if it is available in the cache.
func GetAvatar(av map[string]string, userid string, general *config.Protocol) string {
if sha, ok := av[userid]; ok {
return general.MediaServerDownload + "/" + sha + "/" + userid + ".png"
}
return ""
}
// HandleDownloadSize checks a specified filename against the configured download blacklist
// and checks a specified file-size against the configure limit.
func HandleDownloadSize(logger *logrus.Entry, msg *config.Message, name string, size int64, general *config.Protocol) error {
// check blacklist here
for _, entry := range general.MediaDownloadBlackList {
if entry != "" {
re, err := regexp.Compile(entry)
if err != nil {
logger.Errorf("incorrect regexp %s for %s", entry, msg.Account)
continue
}
if re.MatchString(name) {
return fmt.Errorf("Matching blacklist %s. Not downloading %s", entry, name)
}
}
}
logger.Debugf("Trying to download %#v with size %#v", name, size)
if int(size) > general.MediaDownloadSize {
msg.Event = config.EventFileFailureSize
msg.Extra[msg.Event] = append(msg.Extra[msg.Event], config.FileInfo{
Name: name,
Comment: msg.Text,
Size: size,
})
return fmt.Errorf("File %#v to large to download (%#v). MediaDownloadSize is %#v", name, size, general.MediaDownloadSize)
}
return nil
}
// HandleDownloadData adds the data for a remote file into a Matterbridge gateway message.
func HandleDownloadData(logger *logrus.Entry, msg *config.Message, name, comment, url string, data *[]byte, general *config.Protocol) {
HandleDownloadData2(logger, msg, name, "", comment, url, data, general)
}
// HandleDownloadData adds the data for a remote file into a Matterbridge gateway message.
func HandleDownloadData2(logger *logrus.Entry, msg *config.Message, name, id, comment, url string, data *[]byte, general *config.Protocol) {
var avatar bool
logger.Debugf("Download OK %#v %#v", name, len(*data))
if msg.Event == config.EventAvatarDownload {
avatar = true
}
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{
Name: name,
Data: data,
URL: url,
Comment: comment,
Avatar: avatar,
NativeID: id,
})
}
var emptyLineMatcher = regexp.MustCompile("\n+")
// RemoveEmptyNewLines collapses consecutive newline characters into a single one and
// trims any preceding or trailing newline characters as well.
func RemoveEmptyNewLines(msg string) string {
return emptyLineMatcher.ReplaceAllString(strings.Trim(msg, "\n"), "\n")
}
// ClipMessage trims a message to the specified length if it exceeds it and adds a warning
// to the message in case it does so.
func ClipMessage(text string, length int, clippingMessage string) string {
if clippingMessage == "" {
clippingMessage = " <clipped message>"
}
if len(text) > length {
text = text[:length-len(clippingMessage)]
for len(text) > 0 {
if r, _ := utf8.DecodeLastRuneInString(text); r == utf8.RuneError {
text = text[:len(text)-1]
// Note: DecodeLastRuneInString only returns the constant value "1" in
// case of an error. We do not yet know whether the last rune is now
// actually valid. Example: "€" is 0xE2 0x82 0xAC. If we happen to split
// the string just before 0xAC, and go back only one byte, that would
// leave us with a string that ends in the byte 0xE2, which is not a valid
// rune, so we need to try again.
} else {
break
}
}
text += clippingMessage
}
return text
}
func ClipOrSplitMessage(text string, length int, clippingMessage string, splitMax int) []string {
var msgParts []string
remainingText := text
// Invariant of this splitting loop: No text is lost (msgParts+remainingText is the original text),
// and all parts is guaranteed to satisfy the length requirement.
for len(msgParts) < splitMax-1 && len(remainingText) > length {
// Decision: The text needs to be split (again).
var chunk string
wasted := 0
// The longest UTF-8 encoding of a valid rune is 4 bytes (0xF4 0x8F 0xBF 0xBF, encoding U+10FFFF),
// so we should never need to waste 4 or more bytes at a time.
for wasted < 4 && wasted < length {
chunk = remainingText[:length-wasted]
if r, _ := utf8.DecodeLastRuneInString(chunk); r == utf8.RuneError {
wasted += 1
} else {
break
}
}
// Note: At this point, "chunk" might still be invalid, if "text" is very broken.
msgParts = append(msgParts, chunk)
remainingText = remainingText[len(chunk):]
}
msgParts = append(msgParts, ClipMessage(remainingText, length, clippingMessage))
return msgParts
}
// ParseMarkdown takes in an input string as markdown and parses it to html
func ParseMarkdown(input string) string {
extensions := parser.HardLineBreak | parser.NoIntraEmphasis | parser.FencedCode
markdownParser := parser.NewWithExtensions(extensions)
renderer := html.NewRenderer(html.RendererOptions{
Flags: 0,
})
parsedMarkdown := markdown.ToHTML([]byte(input), markdownParser, renderer)
res := string(parsedMarkdown)
res = strings.TrimPrefix(res, "<p>")
res = strings.TrimSuffix(res, "</p>\n")
return res
}
// ConvertWebPToPNG converts input data (which should be WebP format) to PNG format
func ConvertWebPToPNG(data *[]byte) error {
r := bytes.NewReader(*data)
m, err := webp.Decode(r)
if err != nil {
return err
}
var output []byte
w := bytes.NewBuffer(output)
if err := png.Encode(w, m); err != nil {
return err
}
*data = w.Bytes()
return nil
}

View File

@ -0,0 +1,238 @@
package helper
import (
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
const testLineLength = 64
var lineSplittingTestCases = map[string]struct {
input string
splitOutput []string
nonSplitOutput []string
}{
"Short single-line message": {
input: "short",
splitOutput: []string{"short"},
nonSplitOutput: []string{"short"},
},
"Long single-line message": {
input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
splitOutput: []string{
"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>",
"cing elit, sed do eiusmod tempor incididunt ut <clipped message>",
" labore et dolore magna aliqua.",
},
nonSplitOutput: []string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."},
},
"Short multi-line message": {
input: "I\ncan't\nget\nno\nsatisfaction!",
splitOutput: []string{
"I",
"can't",
"get",
"no",
"satisfaction!",
},
nonSplitOutput: []string{
"I",
"can't",
"get",
"no",
"satisfaction!",
},
},
"Long multi-line message": {
input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n" +
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" +
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n" +
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
splitOutput: []string{
"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>",
"cing elit, sed do eiusmod tempor incididunt ut <clipped message>",
" labore et dolore magna aliqua.",
"Ut enim ad minim veniam, quis nostrud exercita <clipped message>",
"tion ullamco laboris nisi ut aliquip ex ea com <clipped message>",
"modo consequat.",
"Duis aute irure dolor in reprehenderit in volu <clipped message>",
"ptate velit esse cillum dolore eu fugiat nulla <clipped message>",
" pariatur.",
"Excepteur sint occaecat cupidatat non proident <clipped message>",
", sunt in culpa qui officia deserunt mollit an <clipped message>",
"im id est laborum.",
},
nonSplitOutput: []string{
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
},
},
"Message ending with new-line.": {
input: "Newline ending\n",
splitOutput: []string{"Newline ending"},
nonSplitOutput: []string{"Newline ending"},
},
"Long message containing UTF-8 multi-byte runes": {
input: "不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說",
splitOutput: []string{
"不布人個我此而及單石業喜資富下 <clipped message>",
"我河下日沒一我臺空達的常景便物 <clipped message>",
"沒為……子大我別名解成?生賣的 <clipped message>",
"全直黑,我自我結毛分洲了世當, <clipped message>",
"是政福那是東;斯說",
},
nonSplitOutput: []string{"不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說"},
},
"Long message, clip three-byte rune after two bytes": {
input: "x 人人生而自由,在尊嚴和權利上一律平等。 他們都具有理性和良知,應該以兄弟情誼的精神對待彼此。",
splitOutput: []string{
"x 人人生而自由,在尊嚴和權利上 <clipped message>",
"一律平等。 他們都具有理性和良知 <clipped message>",
",應該以兄弟情誼的精神對待彼此。",
},
nonSplitOutput: []string{"x 人人生而自由,在尊嚴和權利上一律平等。 他們都具有理性和良知,應該以兄弟情誼的精神對待彼此。"},
},
}
func TestGetSubLines(t *testing.T) {
for testname, testcase := range lineSplittingTestCases {
splitLines := GetSubLines(testcase.input, testLineLength, "")
assert.Equalf(t, testcase.splitOutput, splitLines, "'%s' testcase should give expected lines with splitting.", testname)
for _, splitLine := range splitLines {
byteLength := len([]byte(splitLine))
assert.True(t, byteLength <= testLineLength, "Splitted line '%s' of testcase '%s' should not exceed the maximum byte-length (%d vs. %d).", splitLine, testcase, byteLength, testLineLength)
}
nonSplitLines := GetSubLines(testcase.input, 0, "")
assert.Equalf(t, testcase.nonSplitOutput, nonSplitLines, "'%s' testcase should give expected lines without splitting.", testname)
}
}
func TestConvertWebPToPNG(t *testing.T) {
if os.Getenv("LOCAL_TEST") == "" {
t.Skip()
}
input, err := ioutil.ReadFile("test.webp")
if err != nil {
t.Fail()
}
d := &input
err = ConvertWebPToPNG(d)
if err != nil {
t.Fail()
}
err = ioutil.WriteFile("test.png", *d, 0o644) // nolint:gosec
if err != nil {
t.Fail()
}
}
var clippingOrSplittingTestCases = map[string]struct {
inputText string
clipSplitLength int
clippingMessage string
splitMax int
expectedOutput []string
}{
"Short single-line message, split 3": {
inputText: "short",
clipSplitLength: 20,
clippingMessage: "?!?!",
splitMax: 3,
expectedOutput: []string{"short"},
},
"Short single-line message, split 1": {
inputText: "short",
clipSplitLength: 20,
clippingMessage: "?!?!",
splitMax: 1,
expectedOutput: []string{"short"},
},
"Short single-line message, split 0": {
// Mainly check that we don't crash.
inputText: "short",
clipSplitLength: 20,
clippingMessage: "?!?!",
splitMax: 0,
expectedOutput: []string{"short"},
},
"Long single-line message, noclip": {
inputText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
clipSplitLength: 50,
clippingMessage: "?!?!",
splitMax: 10,
expectedOutput: []string{
"Lorem ipsum dolor sit amet, consectetur adipiscing",
" elit, sed do eiusmod tempor incididunt ut labore ",
"et dolore magna aliqua.",
},
},
"Long single-line message, noclip tight": {
inputText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
clipSplitLength: 50,
clippingMessage: "?!?!",
splitMax: 3,
expectedOutput: []string{
"Lorem ipsum dolor sit amet, consectetur adipiscing",
" elit, sed do eiusmod tempor incididunt ut labore ",
"et dolore magna aliqua.",
},
},
"Long single-line message, clip custom": {
inputText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
clipSplitLength: 50,
clippingMessage: "?!?!",
splitMax: 2,
expectedOutput: []string{
"Lorem ipsum dolor sit amet, consectetur adipiscing",
" elit, sed do eiusmod tempor incididunt ut lab?!?!",
},
},
"Long single-line message, clip built-in": {
inputText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
clipSplitLength: 50,
clippingMessage: "",
splitMax: 2,
expectedOutput: []string{
"Lorem ipsum dolor sit amet, consectetur adipiscing",
" elit, sed do eiusmod tempor inc <clipped message>",
},
},
"Short multi-line message": {
inputText: "I\ncan't\nget\nno\nsatisfaction!",
clipSplitLength: 50,
clippingMessage: "",
splitMax: 2,
expectedOutput: []string{"I\ncan't\nget\nno\nsatisfaction!"},
},
"Long message containing UTF-8 multi-byte runes": {
inputText: "人人生而自由,在尊嚴和權利上一律平等。 他們都具有理性和良知,應該以兄弟情誼的精神對待彼此。",
clipSplitLength: 50,
clippingMessage: "",
splitMax: 10,
expectedOutput: []string{
"人人生而自由,在尊嚴和權利上一律", // Note: only 48 bytes!
"平等。 他們都具有理性和良知,應該", // Note: only 49 bytes!
"以兄弟情誼的精神對待彼此。",
},
},
}
func TestClipOrSplitMessage(t *testing.T) {
for testname, testcase := range clippingOrSplittingTestCases {
actualOutput := ClipOrSplitMessage(testcase.inputText, testcase.clipSplitLength, testcase.clippingMessage, testcase.splitMax)
assert.Equalf(t, testcase.expectedOutput, actualOutput, "'%s' testcase should give expected lines with clipping+splitting.", testname)
for _, splitLine := range testcase.expectedOutput {
byteLength := len([]byte(splitLine))
assert.True(t, byteLength <= testcase.clipSplitLength, "Splitted line '%s' of testcase '%s' should not exceed the maximum byte-length (%d vs. %d).", splitLine, testname, testcase.clipSplitLength, byteLength)
}
}
}

View File

@ -0,0 +1,35 @@
//go:build cgolottie
package helper
import (
"fmt"
"github.com/Benau/tgsconverter/libtgsconverter"
"github.com/sirupsen/logrus"
)
func CanConvertTgsToX() error {
return nil
}
// ConvertTgsToX convert input data (which should be tgs format) to any format supported by libtgsconverter
func ConvertTgsToX(data *[]byte, outputFormat string, logger *logrus.Entry) error {
options := libtgsconverter.NewConverterOptions()
options.SetExtension(outputFormat)
blob, err := libtgsconverter.ImportFromData(*data, options)
if err != nil {
return fmt.Errorf("failed to run libtgsconverter.ImportFromData: %s", err.Error())
}
*data = blob
return nil
}
func SupportsFormat(format string) bool {
return libtgsconverter.SupportsExtension(format)
}
func LottieBackend() string {
return "libtgsconverter"
}

View File

@ -0,0 +1,90 @@
//go:build !cgolottie
package helper
import (
"io/ioutil"
"os"
"os/exec"
"github.com/sirupsen/logrus"
)
// CanConvertTgsToX Checks whether the external command necessary for ConvertTgsToX works.
func CanConvertTgsToX() error {
// We depend on the fact that `lottie_convert.py --help` has exit status 0.
// Hyrum's Law predicted this, and Murphy's Law predicts that this will break eventually.
// However, there is no alternative like `lottie_convert.py --is-properly-installed`
cmd := exec.Command("lottie_convert.py", "--help")
return cmd.Run()
}
// ConvertTgsToWebP convert input data (which should be tgs format) to WebP format
// This relies on an external command, which is ugly, but works.
func ConvertTgsToX(data *[]byte, outputFormat string, logger *logrus.Entry) error {
// lottie can't handle input from a pipe, so write to a temporary file:
tmpInFile, err := ioutil.TempFile(os.TempDir(), "matterbridge-lottie-input-*.tgs")
if err != nil {
return err
}
tmpInFileName := tmpInFile.Name()
defer func() {
if removeErr := os.Remove(tmpInFileName); removeErr != nil {
logger.Errorf("Could not delete temporary (input) file %s: %v", tmpInFileName, removeErr)
}
}()
// lottie can handle writing to a pipe, but there is no way to do that platform-independently.
// "/dev/stdout" won't work on Windows, and "-" upsets Cairo for some reason. So we need another file:
tmpOutFile, err := ioutil.TempFile(os.TempDir(), "matterbridge-lottie-output-*.data")
if err != nil {
return err
}
tmpOutFileName := tmpOutFile.Name()
defer func() {
if removeErr := os.Remove(tmpOutFileName); removeErr != nil {
logger.Errorf("Could not delete temporary (output) file %s: %v", tmpOutFileName, removeErr)
}
}()
if _, writeErr := tmpInFile.Write(*data); writeErr != nil {
return writeErr
}
// Must close before calling lottie to avoid data races:
if closeErr := tmpInFile.Close(); closeErr != nil {
return closeErr
}
// Call lottie to transform:
cmd := exec.Command("lottie_convert.py", "--input-format", "lottie", "--output-format", outputFormat, tmpInFileName, tmpOutFileName)
cmd.Stdout = nil
cmd.Stderr = nil
// NB: lottie writes progress into to stderr in all cases.
_, stderr := cmd.Output()
if stderr != nil {
// 'stderr' already contains some parts of Stderr, because it was set to 'nil'.
return stderr
}
dataContents, err := ioutil.ReadFile(tmpOutFileName)
if err != nil {
return err
}
*data = dataContents
return nil
}
func SupportsFormat(format string) bool {
switch format {
case "png":
fallthrough
case "webp":
return true
default:
return false
}
return false
}
func LottieBackend() string {
return "lottie_convert.py"
}

32
bridge/irc/charset.go Normal file
View File

@ -0,0 +1,32 @@
package birc
import (
"golang.org/x/text/encoding"
"golang.org/x/text/encoding/japanese"
"golang.org/x/text/encoding/korean"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/encoding/traditionalchinese"
"golang.org/x/text/encoding/unicode"
)
var encoders = map[string]encoding.Encoding{
"utf-8": unicode.UTF8,
"iso-2022-jp": japanese.ISO2022JP,
"big5": traditionalchinese.Big5,
"gbk": simplifiedchinese.GBK,
"euc-kr": korean.EUCKR,
"gb2312": simplifiedchinese.HZGB2312,
"shift-jis": japanese.ShiftJIS,
"euc-jp": japanese.EUCJP,
"gb18030": simplifiedchinese.GB18030,
}
func toUTF8(from string, input string) string {
enc, ok := encoders[from]
if !ok {
return input
}
res, _ := enc.NewDecoder().String(input)
return res
}

279
bridge/irc/handlers.go Normal file
View File

@ -0,0 +1,279 @@
package birc
import (
"bytes"
"fmt"
"io/ioutil"
"strconv"
"strings"
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/lrstanley/girc"
"github.com/paulrosania/go-charset/charset"
"github.com/saintfish/chardet"
// We need to import the 'data' package as an implicit dependency.
// See: https://godoc.org/github.com/paulrosania/go-charset/charset
_ "github.com/paulrosania/go-charset/data"
)
func (b *Birc) handleCharset(msg *config.Message) error {
if b.GetString("Charset") != "" {
switch b.GetString("Charset") {
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
msg.Text = toUTF8(b.GetString("Charset"), msg.Text)
default:
buf := new(bytes.Buffer)
w, err := charset.NewWriter(b.GetString("Charset"), buf)
if err != nil {
b.Log.Errorf("charset to utf-8 conversion failed: %s", err)
return err
}
fmt.Fprint(w, msg.Text)
w.Close()
msg.Text = buf.String()
}
}
return nil
}
// handleFiles returns true if we have handled the files, otherwise return false
func (b *Birc) handleFiles(msg *config.Message) bool {
if msg.Extra == nil {
return false
}
for _, rmsg := range helper.HandleExtra(msg, b.General) {
b.Local <- rmsg
}
if len(msg.Extra["file"]) == 0 {
return false
}
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.Comment != "" {
msg.Text += fi.Comment + " : "
}
if fi.URL != "" {
msg.Text = fi.URL
if fi.Comment != "" {
msg.Text = fi.Comment + " : " + fi.URL
}
}
b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event}
}
return true
}
func (b *Birc) handleInvite(client *girc.Client, event girc.Event) {
if len(event.Params) != 2 {
return
}
channel := event.Params[1]
b.Log.Debugf("got invite for %s", channel)
if _, ok := b.channels[channel]; ok {
b.i.Cmd.Join(channel)
}
}
func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
if len(event.Params) == 0 {
b.Log.Debugf("handleJoinPart: empty Params? %#v", event)
return
}
channel := strings.ToLower(event.Params[0])
if event.Command == "KICK" && event.Params[1] == b.Nick {
b.Log.Infof("Got kicked from %s by %s", channel, event.Source.Name)
time.Sleep(time.Duration(b.GetInt("RejoinDelay")) * time.Second)
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: channel, Account: b.Account, Event: config.EventRejoinChannels}
return
}
if event.Command == "QUIT" {
if event.Source.Name == b.Nick && strings.Contains(event.Last(), "Ping timeout") {
b.Log.Infof("%s reconnecting ..", b.Account)
b.Remote <- config.Message{Username: "system", Text: "reconnect", Channel: channel, Account: b.Account, Event: config.EventFailure}
return
}
}
if event.Source.Name != b.Nick {
if b.GetBool("nosendjoinpart") {
return
}
msg := config.Message{Username: "system", Text: event.Source.Name + " " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave}
if b.GetBool("verbosejoinpart") {
b.Log.Debugf("<= Sending verbose JOIN_LEAVE event from %s to gateway", b.Account)
msg = config.Message{Username: "system", Text: event.Source.Name + " (" + event.Source.Ident + "@" + event.Source.Host + ") " + strings.ToLower(event.Command) + "s", Channel: channel, Account: b.Account, Event: config.EventJoinLeave}
} else {
b.Log.Debugf("<= Sending JOIN_LEAVE event from %s to gateway", b.Account)
}
b.Log.Debugf("<= Message is %#v", msg)
b.Remote <- msg
return
}
b.Log.Debugf("handle %#v", event)
}
func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) {
b.Log.Debug("Registering callbacks")
i := b.i
b.Nick = event.Params[0]
b.Log.Debug("Clearing handlers before adding in case of BNC reconnect")
i.Handlers.Clear("PRIVMSG")
i.Handlers.Clear("CTCP_ACTION")
i.Handlers.Clear(girc.RPL_TOPICWHOTIME)
i.Handlers.Clear(girc.NOTICE)
i.Handlers.Clear("JOIN")
i.Handlers.Clear("PART")
i.Handlers.Clear("QUIT")
i.Handlers.Clear("KICK")
i.Handlers.Clear("INVITE")
i.Handlers.AddBg("PRIVMSG", b.handlePrivMsg)
i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
i.Handlers.AddBg(girc.NOTICE, b.handleNotice)
i.Handlers.AddBg("JOIN", b.handleJoinPart)
i.Handlers.AddBg("PART", b.handleJoinPart)
i.Handlers.AddBg("QUIT", b.handleJoinPart)
i.Handlers.AddBg("KICK", b.handleJoinPart)
i.Handlers.Add("INVITE", b.handleInvite)
}
func (b *Birc) handleNickServ() {
if !b.GetBool("UseSASL") && b.GetString("NickServNick") != "" && b.GetString("NickServPassword") != "" {
b.Log.Debugf("Sending identify to nickserv %s", b.GetString("NickServNick"))
b.i.Cmd.Message(b.GetString("NickServNick"), "IDENTIFY "+b.GetString("NickServPassword"))
}
if strings.EqualFold(b.GetString("NickServNick"), "Q@CServe.quakenet.org") {
b.Log.Debugf("Authenticating %s against %s", b.GetString("NickServUsername"), b.GetString("NickServNick"))
b.i.Cmd.Message(b.GetString("NickServNick"), "AUTH "+b.GetString("NickServUsername")+" "+b.GetString("NickServPassword"))
}
// give nickserv some slack
time.Sleep(time.Second * 5)
b.authDone = true
}
func (b *Birc) handleNotice(client *girc.Client, event girc.Event) {
if strings.Contains(event.String(), "This nickname is registered") && event.Source.Name == b.GetString("NickServNick") {
b.handleNickServ()
} else {
b.handlePrivMsg(client, event)
}
}
func (b *Birc) handleOther(client *girc.Client, event girc.Event) {
if b.GetInt("DebugLevel") == 1 {
if event.Command != "CLIENT_STATE_UPDATED" &&
event.Command != "CLIENT_GENERAL_UPDATED" {
b.Log.Debugf("%#v", event.String())
}
return
}
switch event.Command {
case "372", "375", "376", "250", "251", "252", "253", "254", "255", "265", "266", "002", "003", "004", "005":
return
}
b.Log.Debugf("%#v", event.String())
}
func (b *Birc) handleOtherAuth(client *girc.Client, event girc.Event) {
b.handleNickServ()
b.handleRunCommands()
// we are now fully connected
// only send on first connection
if b.FirstConnection {
b.connected <- nil
}
}
func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
if b.skipPrivMsg(event) {
return
}
rmsg := config.Message{
Username: event.Source.Name,
Channel: strings.ToLower(event.Params[0]),
Account: b.Account,
UserID: event.Source.Ident + "@" + event.Source.Host,
}
b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Last(), event)
// set action event
if ok, ctcp := event.IsCTCP(); ok {
if ctcp.Command != girc.CTCP_ACTION {
b.Log.Debugf("dropping user ctcp, command: %s", ctcp.Command)
return
}
rmsg.Event = config.EventUserAction
}
// set NOTICE event
if event.Command == "NOTICE" {
rmsg.Event = config.EventNoticeIRC
}
// strip action, we made an event if it was an action
rmsg.Text += event.StripAction()
// start detecting the charset
mycharset := b.GetString("Charset")
if mycharset == "" {
// detect what were sending so that we convert it to utf-8
detector := chardet.NewTextDetector()
result, err := detector.DetectBest([]byte(rmsg.Text))
if err != nil {
b.Log.Infof("detection failed for rmsg.Text: %#v", rmsg.Text)
return
}
b.Log.Debugf("detected %s confidence %#v", result.Charset, result.Confidence)
mycharset = result.Charset
// if we're not sure, just pick ISO-8859-1
if result.Confidence < 80 {
mycharset = "ISO-8859-1"
}
}
switch mycharset {
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
rmsg.Text = toUTF8(b.GetString("Charset"), rmsg.Text)
default:
r, err := charset.NewReader(mycharset, strings.NewReader(rmsg.Text))
if err != nil {
b.Log.Errorf("charset to utf-8 conversion failed: %s", err)
return
}
output, _ := ioutil.ReadAll(r)
rmsg.Text = string(output)
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", event.Params[0], b.Account)
b.Remote <- rmsg
}
func (b *Birc) handleRunCommands() {
for _, cmd := range b.GetStringSlice("RunCommands") {
cmd = strings.ReplaceAll(cmd, "{BOTNICK}", b.Nick)
if err := b.i.Cmd.SendRaw(cmd); err != nil {
b.Log.Errorf("RunCommands %s failed: %s", cmd, err)
}
time.Sleep(time.Second)
}
}
func (b *Birc) handleTopicWhoTime(client *girc.Client, event girc.Event) {
parts := strings.Split(event.Params[2], "!")
t, err := strconv.ParseInt(event.Params[3], 10, 64)
if err != nil {
b.Log.Errorf("Invalid time stamp: %s", event.Params[3])
}
user := parts[0]
if len(parts) > 1 {
user += " [" + parts[1] + "]"
}
b.Log.Debugf("%s: Topic set by %s [%s]", event.Command, user, time.Unix(t, 0))
}

415
bridge/irc/irc.go Normal file
View File

@ -0,0 +1,415 @@
package birc
import (
"crypto/tls"
"errors"
"fmt"
"hash/crc32"
"io/ioutil"
"net"
"sort"
"strconv"
"strings"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/lrstanley/girc"
stripmd "github.com/writeas/go-strip-markdown"
// We need to import the 'data' package as an implicit dependency.
// See: https://godoc.org/github.com/paulrosania/go-charset/charset
_ "github.com/paulrosania/go-charset/data"
)
type Birc struct {
i *girc.Client
Nick string
names map[string][]string
connected chan error
Local chan config.Message // local queue for flood control
FirstConnection, authDone bool
MessageDelay, MessageQueue, MessageLength int
channels map[string]bool
*bridge.Config
}
func New(cfg *bridge.Config) bridge.Bridger {
b := &Birc{}
b.Config = cfg
b.Nick = b.GetString("Nick")
b.names = make(map[string][]string)
b.connected = make(chan error)
b.channels = make(map[string]bool)
if b.GetInt("MessageDelay") == 0 {
b.MessageDelay = 1300
} else {
b.MessageDelay = b.GetInt("MessageDelay")
}
if b.GetInt("MessageQueue") == 0 {
b.MessageQueue = 30
} else {
b.MessageQueue = b.GetInt("MessageQueue")
}
if b.GetInt("MessageLength") == 0 {
b.MessageLength = 400
} else {
b.MessageLength = b.GetInt("MessageLength")
}
b.FirstConnection = true
return b
}
func (b *Birc) Command(msg *config.Message) string {
if msg.Text == "!users" {
b.i.Handlers.Add(girc.RPL_NAMREPLY, b.storeNames)
b.i.Handlers.Add(girc.RPL_ENDOFNAMES, b.endNames)
b.i.Cmd.SendRaw("NAMES " + msg.Channel) //nolint:errcheck
}
return ""
}
func (b *Birc) Connect() error {
if b.GetBool("UseSASL") && b.GetString("TLSClientCertificate") != "" {
return errors.New("you can't enable SASL and TLSClientCertificate at the same time")
}
b.Local = make(chan config.Message, b.MessageQueue+10)
b.Log.Infof("Connecting %s", b.GetString("Server"))
i, err := b.getClient()
if err != nil {
return err
}
if b.GetBool("UseSASL") {
i.Config.SASL = &girc.SASLPlain{
User: b.GetString("NickServNick"),
Pass: b.GetString("NickServPassword"),
}
}
i.Handlers.Add(girc.RPL_WELCOME, b.handleNewConnection)
i.Handlers.Add(girc.RPL_ENDOFMOTD, b.handleOtherAuth)
i.Handlers.Add(girc.ERR_NOMOTD, b.handleOtherAuth)
i.Handlers.Add(girc.ALL_EVENTS, b.handleOther)
b.i = i
go b.doConnect()
err = <-b.connected
if err != nil {
return fmt.Errorf("connection failed %s", err)
}
b.Log.Info("Connection succeeded")
b.FirstConnection = false
if b.GetInt("DebugLevel") == 0 {
i.Handlers.Clear(girc.ALL_EVENTS)
}
go b.doSend()
return nil
}
func (b *Birc) Disconnect() error {
b.i.Close()
close(b.Local)
return nil
}
func (b *Birc) JoinChannel(channel config.ChannelInfo) error {
b.channels[channel.Name] = true
// need to check if we have nickserv auth done before joining channels
for {
if b.authDone {
break
}
time.Sleep(time.Second)
}
if channel.Options.Key != "" {
b.Log.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name)
b.i.Cmd.JoinKey(channel.Name, channel.Options.Key)
} else {
b.i.Cmd.Join(channel.Name)
}
return nil
}
func (b *Birc) Send(msg config.Message) (string, error) {
// ignore delete messages
if msg.Event == config.EventMsgDelete {
return "", nil
}
b.Log.Debugf("=> Receiving %#v", msg)
// we can be in between reconnects #385
if !b.i.IsConnected() {
b.Log.Error("Not connected to server, dropping message")
return "", nil
}
// Execute a command
if strings.HasPrefix(msg.Text, "!") {
b.Command(&msg)
}
// convert to specified charset
if err := b.handleCharset(&msg); err != nil {
return "", err
}
// handle files, return if we're done here
if ok := b.handleFiles(&msg); ok {
return "", nil
}
var msgLines []string
if b.GetBool("StripMarkdown") {
msg.Text = stripmd.Strip(msg.Text)
}
if b.GetBool("MessageSplit") {
msgLines = helper.GetSubLines(msg.Text, b.MessageLength, b.GetString("MessageClipped"))
} else {
msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
}
for i := range msgLines {
if len(b.Local) >= b.MessageQueue {
b.Log.Debugf("flooding, dropping message (queue at %d)", len(b.Local))
return "", nil
}
msg.Text = msgLines[i]
b.Local <- msg
}
return "", nil
}
func (b *Birc) doConnect() {
for {
if err := b.i.Connect(); err != nil {
b.Log.Errorf("disconnect: error: %s", err)
if b.FirstConnection {
b.connected <- err
return
}
} else {
b.Log.Info("disconnect: client requested quit")
}
b.Log.Info("reconnecting in 30 seconds...")
time.Sleep(30 * time.Second)
b.i.Handlers.Clear(girc.RPL_WELCOME)
b.i.Handlers.Add(girc.RPL_WELCOME, func(client *girc.Client, event girc.Event) {
b.Remote <- config.Message{Username: "system", Text: "rejoin", Channel: "", Account: b.Account, Event: config.EventRejoinChannels}
// set our correct nick on reconnect if necessary
b.Nick = event.Source.Name
})
}
}
// Sanitize nicks for RELAYMSG: replace IRC characters with special meanings with "-"
func sanitizeNick(nick string) string {
sanitize := func(r rune) rune {
if strings.ContainsRune("!+%@&#$:'\"?*,. ", r) {
return '-'
}
return r
}
return strings.Map(sanitize, nick)
}
func (b *Birc) doSend() {
rate := time.Millisecond * time.Duration(b.MessageDelay)
throttle := time.NewTicker(rate)
for msg := range b.Local {
<-throttle.C
username := msg.Username
// Optional support for the proposed RELAYMSG extension, described at
// https://github.com/jlu5/ircv3-specifications/blob/master/extensions/relaymsg.md
// nolint:nestif
if (b.i.HasCapability("overdrivenetworks.com/relaymsg") || b.i.HasCapability("draft/relaymsg")) &&
b.GetBool("UseRelayMsg") {
username = sanitizeNick(username)
text := msg.Text
// Work around girc chomping leading commas on single word messages?
if strings.HasPrefix(text, ":") && !strings.ContainsRune(text, ' ') {
text = ":" + text
}
if msg.Event == config.EventUserAction {
b.i.Cmd.SendRawf("RELAYMSG %s %s :\x01ACTION %s\x01", msg.Channel, username, text) //nolint:errcheck
} else {
b.Log.Debugf("Sending RELAYMSG to channel %s: nick=%s", msg.Channel, username)
b.i.Cmd.SendRawf("RELAYMSG %s %s :%s", msg.Channel, username, text) //nolint:errcheck
}
} else {
if b.GetBool("Colornicks") {
checksum := crc32.ChecksumIEEE([]byte(msg.Username))
colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes
username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username)
}
switch msg.Event {
case config.EventUserAction:
b.i.Cmd.Action(msg.Channel, username+msg.Text)
case config.EventNoticeIRC:
b.Log.Debugf("Sending notice to channel %s", msg.Channel)
b.i.Cmd.Notice(msg.Channel, username+msg.Text)
default:
b.Log.Debugf("Sending to channel %s", msg.Channel)
b.i.Cmd.Message(msg.Channel, username+msg.Text)
}
}
}
}
// validateInput validates the server/port/nick configuration. Returns a *girc.Client if successful
func (b *Birc) getClient() (*girc.Client, error) {
server, portstr, err := net.SplitHostPort(b.GetString("Server"))
if err != nil {
return nil, err
}
port, err := strconv.Atoi(portstr)
if err != nil {
return nil, err
}
user := b.GetString("UserName")
if user == "" {
user = b.GetString("Nick")
}
// fix strict user handling of girc
for !girc.IsValidUser(user) {
if len(user) == 1 || len(user) == 0 {
user = "matterbridge"
break
}
user = user[1:]
}
realName := b.GetString("RealName")
if realName == "" {
realName = b.GetString("Nick")
}
debug := ioutil.Discard
if b.GetInt("DebugLevel") == 2 {
debug = b.Log.Writer()
}
pingDelay, err := time.ParseDuration(b.GetString("pingdelay"))
if err != nil || pingDelay == 0 {
pingDelay = time.Minute
}
b.Log.Debugf("setting pingdelay to %s", pingDelay)
tlsConfig, err := b.getTLSConfig()
if err != nil {
return nil, err
}
i := girc.New(girc.Config{
Server: server,
ServerPass: b.GetString("Password"),
Port: port,
Nick: b.GetString("Nick"),
User: user,
Name: realName,
SSL: b.GetBool("UseTLS"),
Bind: b.GetString("Bind"),
TLSConfig: tlsConfig,
PingDelay: pingDelay,
// skip gIRC internal rate limiting, since we have our own throttling
AllowFlood: true,
Debug: debug,
SupportedCaps: map[string][]string{"overdrivenetworks.com/relaymsg": nil, "draft/relaymsg": nil},
})
return i, nil
}
func (b *Birc) endNames(client *girc.Client, event girc.Event) {
channel := event.Params[1]
sort.Strings(b.names[channel])
maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow()
for len(b.names[channel]) > maxNamesPerPost {
b.Remote <- config.Message{
Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost]),
Channel: channel, Account: b.Account,
}
b.names[channel] = b.names[channel][maxNamesPerPost:]
}
b.Remote <- config.Message{
Username: b.Nick, Text: b.formatnicks(b.names[channel]),
Channel: channel, Account: b.Account,
}
b.names[channel] = nil
b.i.Handlers.Clear(girc.RPL_NAMREPLY)
b.i.Handlers.Clear(girc.RPL_ENDOFNAMES)
}
func (b *Birc) skipPrivMsg(event girc.Event) bool {
// Our nick can be changed
b.Nick = b.i.GetNick()
// freenode doesn't send 001 as first reply
if event.Command == "NOTICE" && len(event.Params) != 2 {
return true
}
// don't forward queries to the bot
if event.Params[0] == b.Nick {
return true
}
// don't forward message from ourself
if event.Source != nil {
if event.Source.Name == b.Nick {
return true
}
}
// don't forward messages we sent via RELAYMSG
if relayedNick, ok := event.Tags.Get("draft/relaymsg"); ok && relayedNick == b.Nick {
return true
}
// This is the old name of the cap sent in spoofed messages; I've kept this in
// for compatibility reasons
if relayedNick, ok := event.Tags.Get("relaymsg"); ok && relayedNick == b.Nick {
return true
}
return false
}
func (b *Birc) nicksPerRow() int {
return 4
}
func (b *Birc) storeNames(client *girc.Client, event girc.Event) {
channel := event.Params[2]
b.names[channel] = append(
b.names[channel],
strings.Split(strings.TrimSpace(event.Last()), " ")...)
}
func (b *Birc) formatnicks(nicks []string) string {
return strings.Join(nicks, ", ") + " currently on IRC"
}
func (b *Birc) getTLSConfig() (*tls.Config, error) {
server, _, _ := net.SplitHostPort(b.GetString("server"))
tlsConfig := &tls.Config{
InsecureSkipVerify: b.GetBool("skiptlsverify"), //nolint:gosec
ServerName: server,
}
if filename := b.GetString("TLSClientCertificate"); filename != "" {
cert, err := tls.LoadX509KeyPair(filename, filename)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
}
return tlsConfig, nil
}

View File

@ -0,0 +1,59 @@
package bkeybase
import (
"strconv"
"github.com/42wim/matterbridge/bridge/config"
"github.com/keybase/go-keybase-chat-bot/kbchat/types/chat1"
)
func (b *Bkeybase) handleKeybase() {
sub, err := b.kbc.ListenForNewTextMessages()
if err != nil {
b.Log.Errorf("Error listening: %s", err.Error())
}
go func() {
for {
msg, err := sub.Read()
if err != nil {
b.Log.Errorf("failed to read message: %s", err.Error())
}
if msg.Message.Content.TypeName != "text" {
continue
}
if msg.Message.Sender.Username == b.kbc.GetUsername() {
continue
}
b.handleMessage(msg.Message)
}
}()
}
func (b *Bkeybase) handleMessage(msg chat1.MsgSummary) {
b.Log.Debugf("== Receiving event: %#v", msg)
if msg.Channel.TopicName != b.channel || msg.Channel.Name != b.team {
return
}
if msg.Sender.Username != b.kbc.GetUsername() {
// TODO download avatar
// Create our message
rmsg := config.Message{Username: msg.Sender.Username, Text: msg.Content.Text.Body, UserID: string(msg.Sender.Uid), Channel: msg.Channel.TopicName, ID: strconv.Itoa(int(msg.Id)), Account: b.Account}
// Text must be a string
if msg.Content.TypeName != "text" {
b.Log.Errorf("message is not text")
return
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", msg.Sender.Username, msg.Channel.Name)
b.Remote <- rmsg
}
}

106
bridge/keybase/keybase.go Normal file
View File

@ -0,0 +1,106 @@
package bkeybase
import (
"io/ioutil"
"os"
"path/filepath"
"strconv"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/keybase/go-keybase-chat-bot/kbchat"
)
// Bkeybase bridge structure
type Bkeybase struct {
kbc *kbchat.API
user string
channel string
team string
*bridge.Config
}
// New initializes Bkeybase object and sets team
func New(cfg *bridge.Config) bridge.Bridger {
b := &Bkeybase{Config: cfg}
b.team = b.Config.GetString("Team")
return b
}
// Connect starts keybase API and listener loop
func (b *Bkeybase) Connect() error {
var err error
b.Log.Infof("Connecting %s", b.GetString("Team"))
// use default keybase location (`keybase`)
b.kbc, err = kbchat.Start(kbchat.RunOptions{})
if err != nil {
return err
}
b.user = b.kbc.GetUsername()
b.Log.Info("Connection succeeded")
go b.handleKeybase()
return nil
}
// Disconnect doesn't do anything for now
func (b *Bkeybase) Disconnect() error {
return nil
}
// JoinChannel sets channel name in struct
func (b *Bkeybase) JoinChannel(channel config.ChannelInfo) error {
if _, err := b.kbc.JoinChannel(b.team, channel.Name); err != nil {
return err
}
b.channel = channel.Name
return nil
}
// Send receives bridge messages and sends them to Keybase chat room
func (b *Bkeybase) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg)
// Handle /me events
if msg.Event == config.EventUserAction {
msg.Text = "_" + msg.Text + "_"
}
// Delete message if we have an ID
// Delete message not supported by keybase go library yet
// Edit message if we have an ID
// kbchat lib does not support message editing yet
if len(msg.Extra["file"]) > 0 {
// Upload a file
dir, err := ioutil.TempDir("", "matterbridge")
if err != nil {
return "", err
}
defer os.RemoveAll(dir)
for _, f := range msg.Extra["file"] {
fname := f.(config.FileInfo).Name
fdata := *f.(config.FileInfo).Data
fcaption := f.(config.FileInfo).Comment
fpath := filepath.Join(dir, fname)
if err = ioutil.WriteFile(fpath, fdata, 0600); err != nil {
return "", err
}
_, _ = b.kbc.SendAttachmentByTeam(b.team, &b.channel, fpath, fcaption)
}
return "", nil
}
// Send regular message
text := msg.Username + msg.Text
resp, err := b.kbc.SendMessageByTeamName(b.team, &b.channel, text)
if err != nil {
return "", err
}
return strconv.Itoa(int(*resp.Result.MessageID)), err
}

215
bridge/matrix/helpers.go Normal file
View File

@ -0,0 +1,215 @@
package bmatrix
import (
"encoding/json"
"errors"
"fmt"
"html"
"strings"
"time"
matrix "github.com/matterbridge/gomatrix"
)
func newMatrixUsername(username string) *matrixUsername {
mUsername := new(matrixUsername)
// check if we have a </tag>. if we have, we don't escape HTML. #696
if htmlTag.MatchString(username) {
mUsername.formatted = username
// remove the HTML formatting for beautiful push messages #1188
mUsername.plain = htmlReplacementTag.ReplaceAllString(username, "")
} else {
mUsername.formatted = html.EscapeString(username)
mUsername.plain = username
}
return mUsername
}
// getRoomID retrieves a matching room ID from the channel name.
func (b *Bmatrix) getRoomID(channel string) string {
b.RLock()
defer b.RUnlock()
for ID, name := range b.RoomMap {
if name == channel {
return ID
}
}
return ""
}
// interface2Struct marshals and immediately unmarshals an interface.
// Useful for converting map[string]interface{} to a struct.
func interface2Struct(in interface{}, out interface{}) error {
jsonObj, err := json.Marshal(in)
if err != nil {
return err //nolint:wrapcheck
}
return json.Unmarshal(jsonObj, out)
}
// getDisplayName retrieves the displayName for mxid, querying the homeserver if the mxid is not in the cache.
func (b *Bmatrix) getDisplayName(mxid string) string {
if b.GetBool("UseUserName") {
return mxid[1:]
}
b.RLock()
if val, present := b.NicknameMap[mxid]; present {
b.RUnlock()
return val.displayName
}
b.RUnlock()
displayName, err := b.mc.GetDisplayName(mxid)
var httpError *matrix.HTTPError
if errors.As(err, &httpError) {
b.Log.Warnf("Couldn't retrieve the display name for %s", mxid)
}
if err != nil {
return b.cacheDisplayName(mxid, mxid[1:])
}
return b.cacheDisplayName(mxid, displayName.DisplayName)
}
// cacheDisplayName stores the mapping between a mxid and a display name, to be reused later without performing a query to the homserver.
// Note that old entries are cleaned when this function is called.
func (b *Bmatrix) cacheDisplayName(mxid string, displayName string) string {
now := time.Now()
// scan to delete old entries, to stop memory usage from becoming too high with old entries.
// In addition, we also detect if another user have the same username, and if so, we append their mxids to their usernames to differentiate them.
toDelete := []string{}
conflict := false
b.Lock()
for mxid, v := range b.NicknameMap {
// to prevent username reuse across matrix servers - or even on the same server, append
// the mxid to the username when there is a conflict
if v.displayName == displayName {
conflict = true
// TODO: it would be nice to be able to rename previous messages from this user.
// The current behavior is that only users with clashing usernames and *that have spoken since the bridge last started* will get their mxids shown, and I don't know if that's the expected behavior.
v.displayName = fmt.Sprintf("%s (%s)", displayName, mxid)
b.NicknameMap[mxid] = v
}
if now.Sub(v.lastUpdated) > 10*time.Minute {
toDelete = append(toDelete, mxid)
}
}
if conflict {
displayName = fmt.Sprintf("%s (%s)", displayName, mxid)
}
for _, v := range toDelete {
delete(b.NicknameMap, v)
}
b.NicknameMap[mxid] = NicknameCacheEntry{
displayName: displayName,
lastUpdated: now,
}
b.Unlock()
return displayName
}
// handleError converts errors into httpError.
//nolint:exhaustivestruct
func handleError(err error) *httpError {
var mErr matrix.HTTPError
if !errors.As(err, &mErr) {
return &httpError{
Err: "not a HTTPError",
}
}
var httpErr httpError
if err := json.Unmarshal(mErr.Contents, &httpErr); err != nil {
return &httpError{
Err: "unmarshal failed",
}
}
return &httpErr
}
func (b *Bmatrix) containsAttachment(content map[string]interface{}) bool {
// Skip empty messages
if content["msgtype"] == nil {
return false
}
// Only allow image,video or file msgtypes
if !(content["msgtype"].(string) == "m.image" ||
content["msgtype"].(string) == "m.video" ||
content["msgtype"].(string) == "m.file") {
return false
}
return true
}
// getAvatarURL returns the avatar URL of the specified sender.
func (b *Bmatrix) getAvatarURL(sender string) string {
urlPath := b.mc.BuildURL("profile", sender, "avatar_url")
s := struct {
AvatarURL string `json:"avatar_url"`
}{}
err := b.mc.MakeRequest("GET", urlPath, nil, &s)
if err != nil {
b.Log.Errorf("getAvatarURL failed: %s", err)
return ""
}
url := strings.ReplaceAll(s.AvatarURL, "mxc://", b.GetString("Server")+"/_matrix/media/r0/thumbnail/")
if url != "" {
url += "?width=37&height=37&method=crop"
}
return url
}
// handleRatelimit handles the ratelimit errors and return if we're ratelimited and the amount of time to sleep
func (b *Bmatrix) handleRatelimit(err error) (time.Duration, bool) {
httpErr := handleError(err)
if httpErr.Errcode != "M_LIMIT_EXCEEDED" {
return 0, false
}
b.Log.Debugf("ratelimited: %s", httpErr.Err)
b.Log.Infof("getting ratelimited by matrix, sleeping approx %d seconds before retrying", httpErr.RetryAfterMs/1000)
return time.Duration(httpErr.RetryAfterMs) * time.Millisecond, true
}
// retry function will check if we're ratelimited and retries again when backoff time expired
// returns original error if not 429 ratelimit
func (b *Bmatrix) retry(f func() error) error {
b.rateMutex.Lock()
defer b.rateMutex.Unlock()
for {
if err := f(); err != nil {
if backoff, ok := b.handleRatelimit(err); ok {
time.Sleep(backoff)
} else {
return err
}
} else {
return nil
}
}
}

718
bridge/matrix/matrix.go Normal file
View File

@ -0,0 +1,718 @@
package bmatrix
import (
"bytes"
"fmt"
"mime"
"regexp"
"strings"
"sync"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
matrix "github.com/matterbridge/gomatrix"
)
var (
htmlTag = regexp.MustCompile("</.*?>")
htmlReplacementTag = regexp.MustCompile("<[^>]*>")
)
type NicknameCacheEntry struct {
displayName string
lastUpdated time.Time
}
type Bmatrix struct {
mc *matrix.Client
UserID string
NicknameMap map[string]NicknameCacheEntry
RoomMap map[string]string
rateMutex sync.RWMutex
sync.RWMutex
*bridge.Config
}
type httpError struct {
Errcode string `json:"errcode"`
Err string `json:"error"`
RetryAfterMs int `json:"retry_after_ms"`
}
type matrixUsername struct {
plain string
formatted string
}
// SubTextMessage represents the new content of the message in edit messages.
type SubTextMessage struct {
MsgType string `json:"msgtype"`
Body string `json:"body"`
FormattedBody string `json:"formatted_body,omitempty"`
Format string `json:"format,omitempty"`
}
// MessageRelation explains how the current message relates to a previous message.
// Notably used for message edits.
type MessageRelation struct {
EventID string `json:"event_id"`
Type string `json:"rel_type"`
}
type EditedMessage struct {
NewContent SubTextMessage `json:"m.new_content"`
RelatedTo MessageRelation `json:"m.relates_to"`
matrix.TextMessage
}
type InReplyToRelationContent struct {
EventID string `json:"event_id"`
}
type InReplyToRelation struct {
InReplyTo InReplyToRelationContent `json:"m.in_reply_to"`
}
type ReplyMessage struct {
RelatedTo InReplyToRelation `json:"m.relates_to"`
matrix.TextMessage
}
func New(cfg *bridge.Config) bridge.Bridger {
b := &Bmatrix{Config: cfg}
b.RoomMap = make(map[string]string)
b.NicknameMap = make(map[string]NicknameCacheEntry)
return b
}
func (b *Bmatrix) Connect() error {
var err error
b.Log.Infof("Connecting %s", b.GetString("Server"))
if b.GetString("MxID") != "" && b.GetString("Token") != "" {
b.mc, err = matrix.NewClient(
b.GetString("Server"), b.GetString("MxID"), b.GetString("Token"),
)
if err != nil {
return err
}
b.UserID = b.GetString("MxID")
b.Log.Info("Using existing Matrix credentials")
} else {
b.mc, err = matrix.NewClient(b.GetString("Server"), "", "")
if err != nil {
return err
}
resp, err := b.mc.Login(&matrix.ReqLogin{
Type: "m.login.password",
User: b.GetString("Login"),
Password: b.GetString("Password"),
Identifier: matrix.NewUserIdentifier(b.GetString("Login")),
})
if err != nil {
return err
}
b.mc.SetCredentials(resp.UserID, resp.AccessToken)
b.UserID = resp.UserID
b.Log.Info("Connection succeeded")
}
go b.handlematrix()
return nil
}
func (b *Bmatrix) Disconnect() error {
return nil
}
func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error {
return b.retry(func() 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 nil
})
}
func (b *Bmatrix) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg)
channel := b.getRoomID(msg.Channel)
b.Log.Debugf("Channel %s maps to channel id %s", msg.Channel, channel)
username := newMatrixUsername(msg.Username)
body := username.plain + msg.Text
formattedBody := username.formatted + helper.ParseMarkdown(msg.Text)
if b.GetBool("SpoofUsername") {
// https://spec.matrix.org/v1.3/client-server-api/#mroommember
type stateMember struct {
AvatarURL string `json:"avatar_url,omitempty"`
DisplayName string `json:"displayname"`
Membership string `json:"membership"`
}
// TODO: reset username afterwards with DisplayName: null ?
m := stateMember{
AvatarURL: "",
DisplayName: username.plain,
Membership: "join",
}
_, err := b.mc.SendStateEvent(channel, "m.room.member", b.UserID, m)
if err == nil {
body = msg.Text
formattedBody = helper.ParseMarkdown(msg.Text)
}
}
// Make a action /me of the message
if msg.Event == config.EventUserAction {
m := matrix.TextMessage{
MsgType: "m.emote",
Body: body,
FormattedBody: formattedBody,
Format: "org.matrix.custom.html",
}
if b.GetBool("HTMLDisable") {
m.Format = ""
m.FormattedBody = ""
}
msgID := ""
err := b.retry(func() error {
resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m)
if err != nil {
return err
}
msgID = resp.EventID
return err
})
return msgID, err
}
// Delete message
if msg.Event == config.EventMsgDelete {
if msg.ID == "" {
return "", nil
}
msgID := ""
err := b.retry(func() error {
resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{})
if err != nil {
return err
}
msgID = resp.EventID
return err
})
return msgID, err
}
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
rmsg := rmsg
err := b.retry(func() error {
_, err := b.mc.SendText(channel, rmsg.Username+rmsg.Text)
return err
})
if err != nil {
b.Log.Errorf("sendText failed: %s", err)
}
}
// check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 {
return b.handleUploadFiles(&msg, channel)
}
}
// Edit message if we have an ID
if msg.ID != "" {
rmsg := EditedMessage{
TextMessage: matrix.TextMessage{
Body: body,
MsgType: "m.text",
Format: "org.matrix.custom.html",
FormattedBody: formattedBody,
},
}
rmsg.NewContent = SubTextMessage{
Body: rmsg.TextMessage.Body,
FormattedBody: rmsg.TextMessage.FormattedBody,
Format: rmsg.TextMessage.Format,
MsgType: "m.text",
}
if b.GetBool("HTMLDisable") {
rmsg.TextMessage.Format = ""
rmsg.TextMessage.FormattedBody = ""
rmsg.NewContent.Format = ""
rmsg.NewContent.FormattedBody = ""
}
rmsg.RelatedTo = MessageRelation{
EventID: msg.ID,
Type: "m.replace",
}
err := b.retry(func() error {
_, err := b.mc.SendMessageEvent(channel, "m.room.message", rmsg)
return err
})
if err != nil {
return "", err
}
return msg.ID, nil
}
// Use notices to send join/leave events
if msg.Event == config.EventJoinLeave {
m := matrix.TextMessage{
MsgType: "m.notice",
Body: body,
FormattedBody: formattedBody,
Format: "org.matrix.custom.html",
}
if b.GetBool("HTMLDisable") {
m.Format = ""
m.FormattedBody = ""
}
var (
resp *matrix.RespSendEvent
err error
)
err = b.retry(func() error {
resp, err = b.mc.SendMessageEvent(channel, "m.room.message", m)
return err
})
if err != nil {
return "", err
}
return resp.EventID, err
}
if msg.ParentValid() {
m := ReplyMessage{
TextMessage: matrix.TextMessage{
MsgType: "m.text",
Body: body,
FormattedBody: formattedBody,
Format: "org.matrix.custom.html",
},
}
if b.GetBool("HTMLDisable") {
m.TextMessage.Format = ""
m.TextMessage.FormattedBody = ""
}
m.RelatedTo = InReplyToRelation{
InReplyTo: InReplyToRelationContent{
EventID: msg.ParentID,
},
}
var (
resp *matrix.RespSendEvent
err error
)
err = b.retry(func() error {
resp, err = b.mc.SendMessageEvent(channel, "m.room.message", m)
return err
})
if err != nil {
return "", err
}
return resp.EventID, err
}
if b.GetBool("HTMLDisable") {
var (
resp *matrix.RespSendEvent
err error
)
err = b.retry(func() error {
resp, err = b.mc.SendText(channel, body)
return err
})
if err != nil {
return "", err
}
return resp.EventID, err
}
// Post normal message with HTML support (eg riot.im)
var (
resp *matrix.RespSendEvent
err error
)
err = b.retry(func() error {
resp, err = b.mc.SendFormattedText(channel, body, formattedBody)
return err
})
if err != nil {
return "", err
}
return resp.EventID, err
}
func (b *Bmatrix) handlematrix() {
syncer := b.mc.Syncer.(*matrix.DefaultSyncer)
syncer.OnEventType("m.room.redaction", b.handleEvent)
syncer.OnEventType("m.room.message", b.handleEvent)
syncer.OnEventType("m.room.member", b.handleMemberChange)
go func() {
for {
if b == nil {
return
}
if err := b.mc.Sync(); err != nil {
b.Log.Println("Sync() returned ", err)
}
}
}()
}
func (b *Bmatrix) handleEdit(ev *matrix.Event, rmsg config.Message) bool {
relationInterface, present := ev.Content["m.relates_to"]
newContentInterface, present2 := ev.Content["m.new_content"]
if !(present && present2) {
return false
}
var relation MessageRelation
if err := interface2Struct(relationInterface, &relation); err != nil {
b.Log.Warnf("Couldn't parse 'm.relates_to' object with value %#v", relationInterface)
return false
}
var newContent SubTextMessage
if err := interface2Struct(newContentInterface, &newContent); err != nil {
b.Log.Warnf("Couldn't parse 'm.new_content' object with value %#v", newContentInterface)
return false
}
if relation.Type != "m.replace" {
return false
}
rmsg.ID = relation.EventID
rmsg.Text = newContent.Body
b.Remote <- rmsg
return true
}
func (b *Bmatrix) handleReply(ev *matrix.Event, rmsg config.Message) bool {
relationInterface, present := ev.Content["m.relates_to"]
if !present {
return false
}
var relation InReplyToRelation
if err := interface2Struct(relationInterface, &relation); err != nil {
// probably fine
return false
}
body := rmsg.Text
if !b.GetBool("keepquotedreply") {
for strings.HasPrefix(body, "> ") {
lineIdx := strings.IndexRune(body, '\n')
if lineIdx == -1 {
body = ""
} else {
body = body[(lineIdx + 1):]
}
}
}
rmsg.Text = body
rmsg.ParentID = relation.InReplyTo.EventID
b.Remote <- rmsg
return true
}
func (b *Bmatrix) handleMemberChange(ev *matrix.Event) {
// Update the displayname on join messages, according to https://matrix.org/docs/spec/client_server/r0.6.1#events-on-change-of-profile-information
if ev.Content["membership"] == "join" {
if dn, ok := ev.Content["displayname"].(string); ok {
b.cacheDisplayName(ev.Sender, dn)
}
}
}
func (b *Bmatrix) handleEvent(ev *matrix.Event) {
b.Log.Debugf("== Receiving event: %#v", ev)
if ev.Sender != b.UserID {
b.RLock()
channel, ok := b.RoomMap[ev.RoomID]
b.RUnlock()
if !ok {
b.Log.Debugf("Unknown room %s", ev.RoomID)
return
}
// Create our message
rmsg := config.Message{
Username: b.getDisplayName(ev.Sender),
Channel: channel,
Account: b.Account,
UserID: ev.Sender,
ID: ev.ID,
Avatar: b.getAvatarURL(ev.Sender),
}
// Remove homeserver suffix if configured
if b.GetBool("NoHomeServerSuffix") {
re := regexp.MustCompile("(.*?):.*")
rmsg.Username = re.ReplaceAllString(rmsg.Username, `$1`)
}
// Delete event
if ev.Type == "m.room.redaction" {
rmsg.Event = config.EventMsgDelete
rmsg.ID = ev.Redacts
rmsg.Text = config.EventMsgDelete
b.Remote <- rmsg
return
}
// Text must be a string
if rmsg.Text, ok = ev.Content["body"].(string); !ok {
b.Log.Errorf("Content[body] is not a string: %T\n%#v",
ev.Content["body"], ev.Content)
return
}
// Do we have a /me action
if ev.Content["msgtype"].(string) == "m.emote" {
rmsg.Event = config.EventUserAction
}
// Is it an edit?
if b.handleEdit(ev, rmsg) {
return
}
// Is it a reply?
if b.handleReply(ev, rmsg) {
return
}
// Do we have attachments
if b.containsAttachment(ev.Content) {
err := b.handleDownloadFile(&rmsg, ev.Content)
if err != nil {
b.Log.Errorf("download failed: %#v", err)
}
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Sender, b.Account)
b.Remote <- rmsg
// not crucial, so no ratelimit check here
if err := b.mc.MarkRead(ev.RoomID, ev.ID); err != nil {
b.Log.Errorf("couldn't mark message as read %s", err.Error())
}
}
}
// handleDownloadFile handles file download
func (b *Bmatrix) handleDownloadFile(rmsg *config.Message, content map[string]interface{}) error {
var (
ok bool
url, name, msgtype, mtype string
info map[string]interface{}
size float64
)
rmsg.Extra = make(map[string][]interface{})
if url, ok = content["url"].(string); !ok {
return fmt.Errorf("url isn't a %T", url)
}
url = strings.Replace(url, "mxc://", b.GetString("Server")+"/_matrix/media/v1/download/", -1)
if info, ok = content["info"].(map[string]interface{}); !ok {
return fmt.Errorf("info isn't a %T", info)
}
if size, ok = info["size"].(float64); !ok {
return fmt.Errorf("size isn't a %T", size)
}
if name, ok = content["body"].(string); !ok {
return fmt.Errorf("name isn't a %T", name)
}
if msgtype, ok = content["msgtype"].(string); !ok {
return fmt.Errorf("msgtype isn't a %T", msgtype)
}
if mtype, ok = info["mimetype"].(string); !ok {
return fmt.Errorf("mtype isn't a %T", mtype)
}
// check if we have an image uploaded without extension
if !strings.Contains(name, ".") {
if msgtype == "m.image" {
mext, _ := mime.ExtensionsByType(mtype)
if len(mext) > 0 {
name += mext[0]
}
} else {
// just a default .png extension if we don't have mime info
name += ".png"
}
}
// check if the size is ok
err := helper.HandleDownloadSize(b.Log, rmsg, name, int64(size), b.General)
if err != nil {
return err
}
// actually download the file
data, err := helper.DownloadFile(url)
if err != nil {
return fmt.Errorf("download %s failed %#v", url, err)
}
// add the downloaded data to the message
helper.HandleDownloadData(b.Log, rmsg, name, "", url, data, b.General)
return nil
}
// handleUploadFiles handles native upload of files.
func (b *Bmatrix) handleUploadFiles(msg *config.Message, channel string) (string, error) {
for _, f := range msg.Extra["file"] {
if fi, ok := f.(config.FileInfo); ok {
b.handleUploadFile(msg, channel, &fi)
}
}
return "", nil
}
// handleUploadFile handles native upload of a file.
func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *config.FileInfo) {
username := newMatrixUsername(msg.Username)
content := bytes.NewReader(*fi.Data)
sp := strings.Split(fi.Name, ".")
mtype := mime.TypeByExtension("." + sp[len(sp)-1])
// image and video uploads send no username, we have to do this ourself here #715
err := b.retry(func() error {
_, err := b.mc.SendFormattedText(channel, username.plain+fi.Comment, username.formatted+fi.Comment)
return err
})
if err != nil {
b.Log.Errorf("file comment failed: %#v", err)
}
b.Log.Debugf("uploading file: %s %s", fi.Name, mtype)
var res *matrix.RespMediaUpload
err = b.retry(func() error {
res, err = b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
return err
})
if err != nil {
b.Log.Errorf("file upload failed: %#v", err)
return
}
switch {
case strings.Contains(mtype, "video"):
b.Log.Debugf("sendVideo %s", res.ContentURI)
err = b.retry(func() error {
_, err = b.mc.SendVideo(channel, fi.Name, res.ContentURI)
return err
})
if err != nil {
b.Log.Errorf("sendVideo failed: %#v", err)
}
case strings.Contains(mtype, "image"):
b.Log.Debugf("sendImage %s", res.ContentURI)
err = b.retry(func() error {
_, err = b.mc.SendImage(channel, fi.Name, res.ContentURI)
return err
})
if err != nil {
b.Log.Errorf("sendImage failed: %#v", err)
}
case strings.Contains(mtype, "audio"):
b.Log.Debugf("sendAudio %s", res.ContentURI)
err = b.retry(func() error {
_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.AudioMessage{
MsgType: "m.audio",
Body: fi.Name,
URL: res.ContentURI,
Info: matrix.AudioInfo{
Mimetype: mtype,
Size: uint(len(*fi.Data)),
},
})
return err
})
if err != nil {
b.Log.Errorf("sendAudio failed: %#v", err)
}
default:
b.Log.Debugf("sendFile %s", res.ContentURI)
err = b.retry(func() error {
_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.FileMessage{
MsgType: "m.file",
Body: fi.Name,
URL: res.ContentURI,
Info: matrix.FileInfo{
Mimetype: mtype,
Size: uint(len(*fi.Data)),
},
})
return err
})
if err != nil {
b.Log.Errorf("sendFile failed: %#v", err)
}
}
b.Log.Debugf("result: %#v", res)
}

View File

@ -0,0 +1,28 @@
package bmatrix
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestPlainUsername(t *testing.T) {
uut := newMatrixUsername("MyUser")
assert.Equal(t, "MyUser", uut.formatted)
assert.Equal(t, "MyUser", uut.plain)
}
func TestHTMLUsername(t *testing.T) {
uut := newMatrixUsername("<b>MyUser</b>")
assert.Equal(t, "<b>MyUser</b>", uut.formatted)
assert.Equal(t, "MyUser", uut.plain)
}
func TestFancyUsername(t *testing.T) {
uut := newMatrixUsername("<MyUser>")
assert.Equal(t, "&lt;MyUser&gt;", uut.formatted)
assert.Equal(t, "<MyUser>", uut.plain)
}

View File

@ -0,0 +1,217 @@
package bmattermost
import (
"context"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/matterbridge/matterclient"
"github.com/mattermost/mattermost/server/public/model"
)
// handleDownloadAvatar downloads the avatar of userid from channel
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
// logs an error message if it fails
func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
rmsg := config.Message{
Username: "system",
Text: "avatar",
Channel: channel,
Account: b.Account,
UserID: userid,
Event: config.EventAvatarDownload,
Extra: make(map[string][]interface{}),
}
if _, ok := b.avatarMap[userid]; !ok {
var (
data []byte
err error
)
data, _, err = b.mc.Client.GetProfileImage(context.TODO(), userid, "")
if err != nil {
b.Log.Errorf("ProfileImage download failed for %#v %s", userid, err)
return
}
err = helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General)
if err != nil {
b.Log.Error(err)
return
}
helper.HandleDownloadData(b.Log, &rmsg, userid+".png", rmsg.Text, "", &data, b.General)
b.Remote <- rmsg
}
}
//nolint:wrapcheck
func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error {
url, _, _ := b.mc.Client.GetFileLink(context.TODO(), id)
finfo, _, err := b.mc.Client.GetFileInfo(context.TODO(), id)
if err != nil {
return err
}
err = helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General)
if err != nil {
return err
}
data, _, err := b.mc.Client.DownloadFile(context.TODO(), id, true)
if err != nil {
return err
}
helper.HandleDownloadData(b.Log, rmsg, finfo.Name, rmsg.Text, url, &data, b.General)
return nil
}
func (b *Bmattermost) handleMatter() {
messages := make(chan *config.Message)
if b.GetString("WebhookBindAddress") != "" {
b.Log.Debugf("Choosing webhooks based receiving")
go b.handleMatterHook(messages)
} else {
if b.GetString("Token") != "" {
b.Log.Debugf("Choosing token based receiving")
} else {
b.Log.Debugf("Choosing login/password based receiving")
}
// if for some reason we only want to sent stuff to mattermost but not receive, return
if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") != "" && b.GetString("Token") == "" && b.GetString("Login") == "" {
b.Log.Debugf("No WebhookBindAddress specified, only WebhookURL. You will not receive messages from mattermost, only sending is possible.")
}
go b.handleMatterClient(messages)
}
var ok bool
for message := range messages {
message.Avatar = helper.GetAvatar(b.avatarMap, message.UserID, b.General)
message.Account = b.Account
message.Text, ok = b.replaceAction(message.Text)
if ok {
message.Event = config.EventUserAction
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
b.Log.Debugf("<= Message is %#v", message)
b.Remote <- *message
}
}
//nolint:cyclop
func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
for message := range b.mc.MessageChan {
b.Log.Debugf("%#v %#v", message.Raw.GetData(), message.Raw.EventType())
if b.skipMessage(message) {
b.Log.Debugf("Skipped message: %#v", message)
continue
}
channelName := b.getChannelName(message.Post.ChannelId)
if channelName == "" {
channelName = message.Channel
}
// only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" {
b.handleDownloadAvatar(message.UserID, channelName)
}
b.Log.Debugf("== Receiving event %#v", message)
rmsg := &config.Message{
Username: message.Username,
UserID: message.UserID,
Channel: channelName,
Text: message.Text,
ID: message.Post.Id,
ParentID: message.Post.RootId, // ParentID is obsolete with mattermost
Extra: make(map[string][]interface{}),
}
// handle mattermost post properties (override username and attachments)
b.handleProps(rmsg, message)
// create a text for bridges that don't support native editing
if message.Raw.EventType() == model.WebsocketEventPostEdited && !b.GetBool("EditDisable") {
rmsg.Text = message.Text + b.GetString("EditSuffix")
}
if message.Raw.EventType() == model.WebsocketEventPostDeleted {
rmsg.Event = config.EventMsgDelete
}
for _, id := range message.Post.FileIds {
err := b.handleDownloadFile(rmsg, id)
if err != nil {
b.Log.Errorf("download failed: %s", err)
}
}
// Use nickname instead of username if defined
if !b.GetBool("useusername") {
if nick := b.mc.GetNickName(rmsg.UserID); nick != "" {
rmsg.Username = nick
}
}
messages <- rmsg
}
}
func (b *Bmattermost) handleMatterHook(messages chan *config.Message) {
for {
message := b.mh.Receive()
b.Log.Debugf("Receiving from matterhook %#v", message)
messages <- &config.Message{
UserID: message.UserID,
Username: message.UserName,
Text: message.Text,
Channel: message.ChannelName,
}
}
}
func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
var err error
var res, id string
channelID := b.getChannelID(msg.Channel)
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
id, err = b.mc.UploadFile(*fi.Data, channelID, fi.Name)
if err != nil {
return "", err
}
msg.Text = fi.Comment
if b.GetBool("PrefixMessagesWithNick") {
msg.Text = msg.Username + msg.Text
}
res, err = b.mc.PostMessageWithFiles(channelID, msg.Text, msg.ParentID, []string{id})
}
return res, err
}
//nolint:forcetypeassert
func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Message) {
props := message.Post.Props
if props == nil {
return
}
if _, ok := props["override_username"].(string); ok {
rmsg.Username = props["override_username"].(string)
}
if _, ok := props["attachments"].([]interface{}); ok {
rmsg.Extra["attachments"] = props["attachments"].([]interface{})
if rmsg.Text != "" {
return
}
for _, attachment := range rmsg.Extra["attachments"] {
attach := attachment.(map[string]interface{})
if attach["text"].(string) != "" {
rmsg.Text += attach["text"].(string)
continue
}
if attach["fallback"].(string) != "" {
rmsg.Text += attach["fallback"].(string)
}
}
}
}

View File

@ -0,0 +1,294 @@
package bmattermost
import (
"net/http"
"strings"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterhook"
"github.com/matterbridge/matterclient"
"github.com/mattermost/mattermost/server/public/model"
)
func (b *Bmattermost) doConnectWebhookBind() error {
switch {
case b.GetString("WebhookURL") != "":
b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{
InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
BindAddress: b.GetString("WebhookBindAddress"),
})
case b.GetString("Token") != "":
b.Log.Info("Connecting using token (sending)")
err := b.apiLogin()
if err != nil {
return err
}
case b.GetString("Login") != "":
b.Log.Info("Connecting using login/password (sending)")
err := b.apiLogin()
if err != nil {
return err
}
default:
b.Log.Info("Connecting using webhookbindaddress (receiving)")
b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{
InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
BindAddress: b.GetString("WebhookBindAddress"),
})
}
return nil
}
func (b *Bmattermost) doConnectWebhookURL() error {
b.Log.Info("Connecting using webhookurl (sending)")
b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{
InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
DisableServer: true,
})
if b.GetString("Token") != "" {
b.Log.Info("Connecting using token (receiving)")
err := b.apiLogin()
if err != nil {
return err
}
} else if b.GetString("Login") != "" {
b.Log.Info("Connecting using login/password (receiving)")
err := b.apiLogin()
if err != nil {
return err
}
}
return nil
}
//nolint:wrapcheck
func (b *Bmattermost) apiLogin() error {
password := b.GetString("Password")
if b.GetString("Token") != "" {
password = "token=" + b.GetString("Token")
}
b.mc = matterclient.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server"), "")
if b.GetBool("debug") {
b.mc.SetLogLevel("debug")
}
b.mc.SkipTLSVerify = b.GetBool("SkipTLSVerify")
b.mc.SkipVersionCheck = b.GetBool("SkipVersionCheck")
b.mc.NoTLS = b.GetBool("NoTLS")
b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server"))
if err := b.mc.Login(); err != nil {
return err
}
b.Log.Info("Connection succeeded")
b.TeamID = b.mc.GetTeamID()
return nil
}
// replaceAction replace the message with the correct action (/me) code
func (b *Bmattermost) replaceAction(text string) (string, bool) {
if strings.HasPrefix(text, "*") && strings.HasSuffix(text, "*") {
return strings.Replace(text, "*", "", -1), true
}
return text, false
}
func (b *Bmattermost) cacheAvatar(msg *config.Message) (string, error) {
fi := msg.Extra["file"][0].(config.FileInfo)
/* if we have a sha we have successfully uploaded the file to the media server,
so we can now cache the sha */
if fi.SHA != "" {
b.Log.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID)
b.avatarMap[msg.UserID] = fi.SHA
}
return "", nil
}
// sendWebhook uses the configured WebhookURL to send the message
func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
// skip events
if msg.Event != "" {
return "", nil
}
if b.GetBool("PrefixMessagesWithNick") {
msg.Text = msg.Username + msg.Text
}
if msg.Extra != nil {
// this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
rmsg := rmsg // scopelint
iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl"))
matterMessage := matterhook.OMessage{
IconURL: iconURL,
Channel: rmsg.Channel,
UserName: rmsg.Username,
Text: rmsg.Text,
Props: make(map[string]interface{}),
}
matterMessage.Props["matterbridge_"+b.uuid] = true
if err := b.mh.Send(matterMessage); err != nil {
b.Log.Errorf("sendWebhook failed: %s ", err)
}
}
// webhook doesn't support file uploads, so we add the url manually
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
msg.Text += " " + fi.URL
}
}
}
}
iconURL := config.GetIconURL(&msg, b.GetString("iconurl"))
matterMessage := matterhook.OMessage{
IconURL: iconURL,
Channel: msg.Channel,
UserName: msg.Username,
Text: msg.Text,
Props: make(map[string]interface{}),
}
if msg.Avatar != "" {
matterMessage.IconURL = msg.Avatar
}
matterMessage.Props["matterbridge_"+b.uuid] = true
err := b.mh.Send(matterMessage)
if err != nil {
b.Log.Info(err)
return "", err
}
return "", nil
}
// skipMessages returns true if this message should not be handled
//
//nolint:gocyclo,cyclop
func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
// Handle join/leave
skipJoinMessageTypes := map[string]struct{}{
"system_join_leave": {}, // deprecated for system_add_to_channel
"system_leave_channel": {}, // deprecated for system_remove_from_channel
"system_join_channel": {},
"system_add_to_channel": {},
"system_remove_from_channel": {},
"system_add_to_team": {},
"system_remove_from_team": {},
}
// dirty hack to efficiently check if this element is in the map without writing a contains func
// can be replaced with native slice.contains with go 1.21
if _, ok := skipJoinMessageTypes[message.Type]; ok {
if b.GetBool("nosendjoinpart") {
return true
}
channelName := b.getChannelName(message.Post.ChannelId)
if channelName == "" {
channelName = message.Channel
}
b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
b.Remote <- config.Message{
Username: "system",
Text: message.Text,
Channel: channelName,
Account: b.Account,
Event: config.EventJoinLeave,
}
return true
}
// Handle edited messages
if (message.Raw.EventType() == model.WebsocketEventPostEdited) && b.GetBool("EditDisable") {
return true
}
// Ignore non-post messages
if message.Post == nil {
b.Log.Debugf("ignoring nil message.Post: %#v", message)
return true
}
// Ignore messages sent from matterbridge
if message.Post.Props != nil {
if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok {
b.Log.Debug("sent by matterbridge, ignoring")
return true
}
}
// Ignore messages sent from a user logged in as the bot
if b.mc.User.Username == message.Username {
b.Log.Debug("message from same user as bot, ignoring")
return true
}
// if the message has reactions don't repost it (for now, until we can correlate reaction with message)
if message.Post.HasReactions {
return true
}
// ignore messages from other teams than ours
if message.Raw.GetData()["team_id"].(string) != b.TeamID {
b.Log.Debug("message from other team, ignoring")
return true
}
// only handle posted, edited or deleted events
if !(message.Raw.EventType() == "posted" || message.Raw.EventType() == model.WebsocketEventPostEdited ||
message.Raw.EventType() == model.WebsocketEventPostDeleted) {
return true
}
return false
}
func (b *Bmattermost) getVersion() string {
proto := "https"
if b.GetBool("notls") {
proto = "http"
}
resp, err := http.Get(proto + "://" + b.GetString("server"))
if err != nil {
b.Log.Error("failed getting version")
return ""
}
defer resp.Body.Close()
return resp.Header.Get("X-Version-Id")
}
func (b *Bmattermost) getChannelID(name string) string {
idcheck := strings.Split(name, "ID:")
if len(idcheck) > 1 {
return idcheck[1]
}
return b.mc.GetChannelID(name, b.TeamID)
}
func (b *Bmattermost) getChannelName(id string) string {
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
for _, c := range b.channelInfoMap {
if c.Name == "ID:"+id {
// if we have ID: specified in our gateway configuration return this
return c.Name
}
}
return ""
}

View File

@ -0,0 +1,194 @@
package bmattermost
import (
"context"
"errors"
"fmt"
"strings"
"sync"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterhook"
"github.com/matterbridge/matterclient"
"github.com/rs/xid"
)
type Bmattermost struct {
mh *matterhook.Client
mc *matterclient.Client
v6 bool
uuid string
TeamID string
*bridge.Config
avatarMap map[string]string
channelsMutex sync.RWMutex
channelInfoMap map[string]*config.ChannelInfo
}
const mattermostPlugin = "mattermost.plugin"
func New(cfg *bridge.Config) bridge.Bridger {
b := &Bmattermost{
Config: cfg,
avatarMap: make(map[string]string),
channelInfoMap: make(map[string]*config.ChannelInfo),
}
b.v6 = b.GetBool("v6")
b.uuid = xid.New().String()
return b
}
func (b *Bmattermost) Command(cmd string) string {
return ""
}
func (b *Bmattermost) Connect() error {
if b.Account == mattermostPlugin {
return nil
}
if strings.HasPrefix(b.getVersion(), "6.") || strings.HasPrefix(b.getVersion(), "7.") {
if !b.v6 {
b.v6 = true
}
}
if b.GetString("WebhookBindAddress") != "" {
if err := b.doConnectWebhookBind(); err != nil {
return err
}
go b.handleMatter()
return nil
}
switch {
case b.GetString("WebhookURL") != "":
if err := b.doConnectWebhookURL(); err != nil {
return err
}
go b.handleMatter()
return nil
case b.GetString("Token") != "":
b.Log.Info("Connecting using token (sending and receiving)")
err := b.apiLogin()
if err != nil {
return err
}
go b.handleMatter()
case b.GetString("Login") != "":
b.Log.Info("Connecting using login/password (sending and receiving)")
b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
err := b.apiLogin()
if err != nil {
return err
}
go b.handleMatter()
}
if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") == "" &&
b.GetString("Login") == "" && b.GetString("Token") == "" {
return errors.New("no connection method found. See that you have WebhookBindAddress, WebhookURL or Token/Login/Password/Server/Team configured")
}
return nil
}
func (b *Bmattermost) Disconnect() error {
return nil
}
func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error {
if b.Account == mattermostPlugin {
return nil
}
b.channelsMutex.Lock()
b.channelInfoMap[channel.ID] = &channel
b.channelsMutex.Unlock()
// we can only join channels using the API
if b.GetString("WebhookURL") == "" && b.GetString("WebhookBindAddress") == "" {
id := b.getChannelID(channel.Name)
if id == "" {
return fmt.Errorf("Could not find channel ID for channel %s", channel.Name)
}
return b.mc.JoinChannel(id)
}
return nil
}
func (b *Bmattermost) Send(msg config.Message) (string, error) {
if b.Account == mattermostPlugin {
return "", nil
}
b.Log.Debugf("=> Receiving %#v", msg)
// Make a action /me of the message
if msg.Event == config.EventUserAction {
msg.Text = "*" + msg.Text + "*"
}
// map the file SHA to our user (caches the avatar)
if msg.Event == config.EventAvatarDownload {
return b.cacheAvatar(&msg)
}
// Use webhook to send the message
if b.GetString("WebhookURL") != "" {
return b.sendWebhook(msg)
}
// Delete message
if msg.Event == config.EventMsgDelete {
if msg.ID == "" {
return "", nil
}
return msg.ID, b.mc.DeleteMessage(msg.ID)
}
// Handle prefix hint for unthreaded messages.
if msg.ParentNotFound() {
msg.ParentID = ""
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
}
// we only can reply to the root of the thread, not to a specific ID (like discord for example does)
if msg.ParentID != "" {
post, _, err := b.mc.Client.GetPost(context.TODO(), msg.ParentID, "")
if err != nil {
b.Log.Errorf("getting post %s failed: %s", msg.ParentID, err)
}
if post != nil && post.RootId != "" {
msg.ParentID = post.RootId
}
}
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
if _, err := b.mc.PostMessage(b.getChannelID(rmsg.Channel), rmsg.Username+rmsg.Text, msg.ParentID); err != nil {
b.Log.Errorf("PostMessage failed: %s", err)
}
}
if len(msg.Extra["file"]) > 0 {
return b.handleUploadFile(&msg)
}
}
// Prepend nick if configured
if b.GetBool("PrefixMessagesWithNick") {
msg.Text = msg.Username + msg.Text
}
// Edit message if we have an ID
if msg.ID != "" {
return b.mc.EditMessage(msg.ID, msg.Text)
}
// Post normal message
return b.mc.PostMessage(b.getChannelID(msg.Channel), msg.Text, msg.ParentID)
}

101
bridge/msteams/handler.go Normal file
View File

@ -0,0 +1,101 @@
package bmsteams
import (
"encoding/json"
"fmt"
"io/ioutil"
"strings"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
msgraph "github.com/yaegashi/msgraph.go/beta"
)
func (b *Bmsteams) findFile(weburl string) (string, error) {
itemRB, err := b.gc.GetDriveItemByURL(b.ctx, weburl)
if err != nil {
return "", err
}
itemRB.Workbook().Worksheets()
b.gc.Workbooks()
item, err := itemRB.Request().Get(b.ctx)
if err != nil {
return "", err
}
if url, ok := item.GetAdditionalData("@microsoft.graph.downloadUrl"); ok {
return url.(string), nil
}
return "", nil
}
// handleDownloadFile handles file download
func (b *Bmsteams) handleDownloadFile(rmsg *config.Message, filename, weburl string) error {
realURL, err := b.findFile(weburl)
if err != nil {
return err
}
// Actually download the file.
data, err := helper.DownloadFile(realURL)
if err != nil {
return fmt.Errorf("download %s failed %#v", weburl, err)
}
// If a comment is attached to the file(s) it is in the 'Text' field of the teams messge event
// and should be added as comment to only one of the files. We reset the 'Text' field to ensure
// that the comment is not duplicated.
comment := rmsg.Text
rmsg.Text = ""
helper.HandleDownloadData(b.Log, rmsg, filename, comment, weburl, data, b.General)
return nil
}
func (b *Bmsteams) handleAttachments(rmsg *config.Message, msg msgraph.ChatMessage) {
for _, a := range msg.Attachments {
//remove the attachment tags from the text
rmsg.Text = attachRE.ReplaceAllString(rmsg.Text, "")
//handle a code snippet (code block)
if *a.ContentType == "application/vnd.microsoft.card.codesnippet" {
b.handleCodeSnippet(rmsg, a)
continue
}
//handle the download
err := b.handleDownloadFile(rmsg, *a.Name, *a.ContentURL)
if err != nil {
b.Log.Errorf("download of %s failed: %s", *a.Name, err)
}
}
}
type AttachContent struct {
Language string `json:"language"`
CodeSnippetURL string `json:"codeSnippetUrl"`
}
func (b *Bmsteams) handleCodeSnippet(rmsg *config.Message, attach msgraph.ChatMessageAttachment) {
var content AttachContent
err := json.Unmarshal([]byte(*attach.Content), &content)
if err != nil {
b.Log.Errorf("unmarshal codesnippet failed: %s", err)
return
}
s := strings.Split(content.CodeSnippetURL, "/")
if len(s) != 13 {
b.Log.Errorf("codesnippetUrl has unexpected size: %s", content.CodeSnippetURL)
return
}
resp, err := b.gc.Teams().Request().Client().Get(content.CodeSnippetURL)
if err != nil {
b.Log.Errorf("retrieving snippet content failed:%s", err)
return
}
defer resp.Body.Close()
res, err := ioutil.ReadAll(resp.Body)
if err != nil {
b.Log.Errorf("reading snippet data failed: %s", err)
return
}
rmsg.Text = rmsg.Text + "\n```" + content.Language + "\n" + string(res) + "\n```\n"
}

229
bridge/msteams/msteams.go Normal file
View File

@ -0,0 +1,229 @@
package bmsteams
import (
"context"
"fmt"
"os"
"regexp"
"strings"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/davecgh/go-spew/spew"
"github.com/mattn/godown"
msgraph "github.com/yaegashi/msgraph.go/beta"
"github.com/yaegashi/msgraph.go/msauth"
"golang.org/x/oauth2"
)
var (
defaultScopes = []string{"openid", "profile", "offline_access", "Group.Read.All", "Group.ReadWrite.All"}
attachRE = regexp.MustCompile(`<attachment id=.*?attachment>`)
)
type Bmsteams struct {
gc *msgraph.GraphServiceRequestBuilder
ctx context.Context
botID string
*bridge.Config
}
func New(cfg *bridge.Config) bridge.Bridger {
return &Bmsteams{Config: cfg}
}
func (b *Bmsteams) Connect() error {
tokenCachePath := b.GetString("sessionFile")
if tokenCachePath == "" {
tokenCachePath = "msteams_session.json"
}
ctx := context.Background()
m := msauth.NewManager()
m.LoadFile(tokenCachePath) //nolint:errcheck
ts, err := m.DeviceAuthorizationGrant(ctx, b.GetString("TenantID"), b.GetString("ClientID"), defaultScopes, nil)
if err != nil {
return err
}
err = m.SaveFile(tokenCachePath)
if err != nil {
b.Log.Errorf("Couldn't save sessionfile in %s: %s", tokenCachePath, err)
}
// make file readable only for matterbridge user
err = os.Chmod(tokenCachePath, 0o600)
if err != nil {
b.Log.Errorf("Couldn't change permissions for %s: %s", tokenCachePath, err)
}
httpClient := oauth2.NewClient(ctx, ts)
graphClient := msgraph.NewClient(httpClient)
b.gc = graphClient
b.ctx = ctx
err = b.setBotID()
if err != nil {
return err
}
b.Log.Info("Connection succeeded")
return nil
}
func (b *Bmsteams) Disconnect() error {
return nil
}
func (b *Bmsteams) JoinChannel(channel config.ChannelInfo) error {
go func(name string) {
for {
err := b.poll(name)
if err != nil {
b.Log.Errorf("polling failed for %s: %s. retrying in 5 seconds", name, err)
}
time.Sleep(time.Second * 5)
}
}(channel.Name)
return nil
}
func (b *Bmsteams) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg)
if msg.ParentValid() {
return b.sendReply(msg)
}
// Handle prefix hint for unthreaded messages.
if msg.ParentNotFound() {
msg.ParentID = ""
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
}
ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(msg.Channel).Messages().Request()
text := msg.Username + msg.Text
content := &msgraph.ItemBody{Content: &text}
rmsg := &msgraph.ChatMessage{Body: content}
res, err := ct.Add(b.ctx, rmsg)
if err != nil {
return "", err
}
return *res.ID, nil
}
func (b *Bmsteams) sendReply(msg config.Message) (string, error) {
ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(msg.Channel).Messages().ID(msg.ParentID).Replies().Request()
// Handle prefix hint for unthreaded messages.
text := msg.Username + msg.Text
content := &msgraph.ItemBody{Content: &text}
rmsg := &msgraph.ChatMessage{Body: content}
res, err := ct.Add(b.ctx, rmsg)
if err != nil {
return "", err
}
return *res.ID, nil
}
func (b *Bmsteams) getMessages(channel string) ([]msgraph.ChatMessage, error) {
ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(channel).Messages().Request()
rct, err := ct.Get(b.ctx)
if err != nil {
return nil, err
}
b.Log.Debugf("got %#v messages", len(rct))
return rct, nil
}
//nolint:gocognit
func (b *Bmsteams) poll(channelName string) error {
msgmap := make(map[string]time.Time)
b.Log.Debug("getting initial messages")
res, err := b.getMessages(channelName)
if err != nil {
return err
}
for _, msg := range res {
msgmap[*msg.ID] = *msg.CreatedDateTime
if msg.LastModifiedDateTime != nil {
msgmap[*msg.ID] = *msg.LastModifiedDateTime
}
}
time.Sleep(time.Second * 5)
b.Log.Debug("polling for messages")
for {
res, err := b.getMessages(channelName)
if err != nil {
return err
}
for i := len(res) - 1; i >= 0; i-- {
msg := res[i]
if mtime, ok := msgmap[*msg.ID]; ok {
if mtime == *msg.CreatedDateTime && msg.LastModifiedDateTime == nil {
continue
}
if msg.LastModifiedDateTime != nil && mtime == *msg.LastModifiedDateTime {
continue
}
}
if b.GetBool("debug") {
b.Log.Debug("Msg dump: ", spew.Sdump(msg))
}
// skip non-user message for now.
if msg.From == nil || msg.From.User == nil {
continue
}
if *msg.From.User.ID == b.botID {
b.Log.Debug("skipping own message")
msgmap[*msg.ID] = *msg.CreatedDateTime
continue
}
msgmap[*msg.ID] = *msg.CreatedDateTime
if msg.LastModifiedDateTime != nil {
msgmap[*msg.ID] = *msg.LastModifiedDateTime
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", *msg.From.User.DisplayName, b.Account)
text := b.convertToMD(*msg.Body.Content)
rmsg := config.Message{
Username: *msg.From.User.DisplayName,
Text: text,
Channel: channelName,
Account: b.Account,
Avatar: "",
UserID: *msg.From.User.ID,
ID: *msg.ID,
Extra: make(map[string][]interface{}),
}
b.handleAttachments(&rmsg, msg)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
time.Sleep(time.Second * 5)
}
}
func (b *Bmsteams) setBotID() error {
req := b.gc.Me().Request()
r, err := req.Get(b.ctx)
if err != nil {
return err
}
b.botID = *r.ID
return nil
}
func (b *Bmsteams) convertToMD(text string) string {
if !strings.Contains(text, "<div>") {
return text
}
var sb strings.Builder
err := godown.Convert(&sb, strings.NewReader(text), nil)
if err != nil {
b.Log.Errorf("Couldn't convert message to markdown %s", text)
return text
}
return sb.String()
}

70
bridge/mumble/codec.go Normal file
View File

@ -0,0 +1,70 @@
package bmumble
import (
"fmt"
"layeh.com/gumble/gumble"
)
// This is a dummy implementation of a Gumble audio codec which claims
// to implement Opus, but does not actually do anything. This serves
// as a workaround until https://github.com/layeh/gumble/pull/61 is
// merged.
// See https://github.com/42wim/matterbridge/issues/1750 for details.
const (
audioCodecIDOpus = 4
)
func registerNullCodecAsOpus() {
codec := &NullCodec{
encoder: &NullAudioEncoder{},
decoder: &NullAudioDecoder{},
}
gumble.RegisterAudioCodec(audioCodecIDOpus, codec)
}
type NullCodec struct {
encoder *NullAudioEncoder
decoder *NullAudioDecoder
}
func (c *NullCodec) ID() int {
return audioCodecIDOpus
}
func (c *NullCodec) NewEncoder() gumble.AudioEncoder {
e := &NullAudioEncoder{}
return e
}
func (c *NullCodec) NewDecoder() gumble.AudioDecoder {
d := &NullAudioDecoder{}
return d
}
type NullAudioEncoder struct{}
func (e *NullAudioEncoder) ID() int {
return audioCodecIDOpus
}
func (e *NullAudioEncoder) Encode(pcm []int16, mframeSize, maxDataBytes int) ([]byte, error) {
return nil, fmt.Errorf("not implemented")
}
func (e *NullAudioEncoder) Reset() {
}
type NullAudioDecoder struct{}
func (d *NullAudioDecoder) ID() int {
return audioCodecIDOpus
}
func (d *NullAudioDecoder) Decode(data []byte, frameSize int) ([]int16, error) {
return nil, fmt.Errorf("not implemented")
}
func (d *NullAudioDecoder) Reset() {
}

158
bridge/mumble/handlers.go Normal file
View File

@ -0,0 +1,158 @@
package bmumble
import (
"strconv"
"time"
"layeh.com/gumble/gumble"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
)
func (b *Bmumble) handleServerConfig(event *gumble.ServerConfigEvent) {
b.serverConfigUpdate <- *event
}
func (b *Bmumble) handleTextMessage(event *gumble.TextMessageEvent) {
sender := "unknown"
if event.TextMessage.Sender != nil {
sender = event.TextMessage.Sender.Name
}
// If the text message is received before receiving a ServerSync
// and UserState, Client.Self or Self.Channel are nil
if event.Client.Self == nil || event.Client.Self.Channel == nil {
b.Log.Warn("Connection bootstrap not finished, discarding text message")
return
}
// Convert Mumble HTML messages to markdown
parts, err := b.convertHTMLtoMarkdown(event.TextMessage.Message)
if err != nil {
b.Log.Error(err)
}
now := time.Now().UTC()
for i, part := range parts {
// Construct matterbridge message and pass on to the gateway
rmsg := config.Message{
Channel: strconv.FormatUint(uint64(event.Client.Self.Channel.ID), 10),
Username: sender,
UserID: sender + "@" + b.Host,
Account: b.Account,
}
if part.Image == nil {
rmsg.Text = part.Text
} else {
fileExt := part.FileExtension
if fileExt == ".jfif" {
fileExt = ".jpg"
}
if fileExt == ".jpe" {
fileExt = ".jpg"
}
fname := b.Account + "_" + strconv.FormatInt(now.UnixNano(), 10) + "_" + strconv.Itoa(i) + fileExt
rmsg.Extra = make(map[string][]interface{})
if err = helper.HandleDownloadSize(b.Log, &rmsg, fname, int64(len(part.Image)), b.General); err != nil {
b.Log.WithError(err).Warn("not including image in message")
continue
}
helper.HandleDownloadData(b.Log, &rmsg, fname, "", "", &part.Image, b.General)
}
b.Log.Debugf("Sending message to gateway: %+v", rmsg)
b.Remote <- rmsg
}
}
func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) {
// Set the user's "bio"/comment
if comment := b.GetString("UserComment"); comment != "" && event.Client.Self != nil {
event.Client.Self.SetComment(comment)
}
// No need to talk or listen
event.Client.Self.SetSelfDeafened(true)
// if the Channel variable is set, this is a reconnect -> rejoin channel
if b.Channel != nil {
if err := b.doJoin(event.Client, *b.Channel); err != nil {
b.Log.Error(err)
}
b.Remote <- config.Message{
Username: "system",
Text: "rejoin",
Channel: "",
Account: b.Account,
Event: config.EventRejoinChannels,
}
}
}
func (b *Bmumble) handleJoinLeave(event *gumble.UserChangeEvent) {
// Ignore events happening before setup is done
if b.Channel == nil {
return
}
if b.GetBool("nosendjoinpart") {
return
}
b.Log.Debugf("Received gumble user change event: %+v", event)
text := ""
switch {
case event.Type&gumble.UserChangeKicked > 0:
text = " was kicked"
case event.Type&gumble.UserChangeBanned > 0:
text = " was banned"
case event.Type&gumble.UserChangeDisconnected > 0:
if event.User.Channel != nil && event.User.Channel.ID == *b.Channel {
text = " left"
}
case event.Type&gumble.UserChangeConnected > 0:
if event.User.Channel != nil && event.User.Channel.ID == *b.Channel {
text = " joined"
}
case event.Type&gumble.UserChangeChannel > 0:
// Treat Mumble channel changes the same as connects/disconnects; as far as matterbridge is concerned, they are identical
if event.User.Channel != nil && event.User.Channel.ID == *b.Channel {
text = " joined"
} else {
text = " left"
}
}
if text != "" {
b.Remote <- config.Message{
Username: "system",
Text: event.User.Name + text,
Channel: strconv.FormatUint(uint64(*b.Channel), 10),
Account: b.Account,
Event: config.EventJoinLeave,
}
}
}
func (b *Bmumble) handleUserModified(event *gumble.UserChangeEvent) {
// Ignore events happening before setup is done
if b.Channel == nil {
return
}
if event.Type&gumble.UserChangeChannel > 0 {
// Someone attempted to move the user out of the configured channel; attempt to join back
if err := b.doJoin(event.Client, *b.Channel); err != nil {
b.Log.Error(err)
}
}
}
func (b *Bmumble) handleUserChange(event *gumble.UserChangeEvent) {
// The UserChangeEvent is used for both the gumble client itself as well as other clients
if event.User != event.Client.Self {
// other users
b.handleJoinLeave(event)
} else {
// gumble user
b.handleUserModified(event)
}
}
func (b *Bmumble) handleDisconnect(event *gumble.DisconnectEvent) {
b.connected <- *event
}

143
bridge/mumble/helpers.go Normal file
View File

@ -0,0 +1,143 @@
package bmumble
import (
"fmt"
"mime"
"net/http"
"regexp"
"strings"
"github.com/42wim/matterbridge/bridge/config"
"github.com/mattn/godown"
"github.com/vincent-petithory/dataurl"
)
type MessagePart struct {
Text string
FileExtension string
Image []byte
}
func (b *Bmumble) decodeImage(uri string, parts *[]MessagePart) error {
// Decode the data:image/... URI
image, err := dataurl.DecodeString(uri)
if err != nil {
b.Log.WithError(err).Info("No image extracted")
return err
}
// Determine the file extensions for that image
ext, err := mime.ExtensionsByType(image.MediaType.ContentType())
if err != nil || len(ext) == 0 {
b.Log.WithError(err).Infof("No file extension registered for MIME type '%s'", image.MediaType.ContentType())
return err
}
// Add the image to the MessagePart slice
*parts = append(*parts, MessagePart{"", ext[0], image.Data})
return nil
}
func (b *Bmumble) tokenize(t *string) ([]MessagePart, error) {
// `^(.*?)` matches everything before the image
// `!\[[^\]]*\]\(` matches the `![alt](` part of markdown images
// `(data:image\/[^)]+)` matches the data: URI used by Mumble
// `\)` matches the closing parenthesis after the URI
// `(.*)$` matches the remaining text to be examined in the next iteration
p := regexp.MustCompile(`^(?ms)(.*?)!\[[^\]]*\]\((data:image\/[^)]+)\)(.*)$`)
remaining := *t
var parts []MessagePart
for {
tokens := p.FindStringSubmatch(remaining)
if tokens == nil {
// no match -> remaining string is non-image text
pre := strings.TrimSpace(remaining)
if len(pre) > 0 {
parts = append(parts, MessagePart{pre, "", nil})
}
return parts, nil
}
// tokens[1] is the text before the image
if len(tokens[1]) > 0 {
pre := strings.TrimSpace(tokens[1])
parts = append(parts, MessagePart{pre, "", nil})
}
// tokens[2] is the image URL
uri, err := dataurl.UnescapeToString(strings.TrimSpace(strings.ReplaceAll(tokens[2], " ", "")))
if err != nil {
b.Log.WithError(err).Info("URL unescaping failed")
remaining = strings.TrimSpace(tokens[3])
continue
}
err = b.decodeImage(uri, &parts)
if err != nil {
b.Log.WithError(err).Info("Decoding the image failed")
}
// tokens[3] is the text after the image, processed in the next iteration
remaining = strings.TrimSpace(tokens[3])
}
}
func (b *Bmumble) convertHTMLtoMarkdown(html string) ([]MessagePart, error) {
var sb strings.Builder
err := godown.Convert(&sb, strings.NewReader(html), nil)
if err != nil {
return nil, err
}
markdown := sb.String()
b.Log.Debugf("### to markdown: %s", markdown)
return b.tokenize(&markdown)
}
func (b *Bmumble) extractFiles(msg *config.Message) []config.Message {
var messages []config.Message
if msg.Extra == nil || len(msg.Extra["file"]) == 0 {
return messages
}
// Create a separate message for each file
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
imsg := config.Message{
Channel: msg.Channel,
Username: msg.Username,
UserID: msg.UserID,
Account: msg.Account,
Protocol: msg.Protocol,
Timestamp: msg.Timestamp,
Event: "mumble_image",
}
// If no data is present for the file, send a link instead
if fi.Data == nil || len(*fi.Data) == 0 {
if len(fi.URL) > 0 {
imsg.Text = fmt.Sprintf(`<a href="%s">%s</a>`, fi.URL, fi.URL)
messages = append(messages, imsg)
} else {
b.Log.Infof("Not forwarding file without local data")
}
continue
}
mimeType := http.DetectContentType(*fi.Data)
// Mumble only supports images natively, send a link instead
if !strings.HasPrefix(mimeType, "image/") {
if len(fi.URL) > 0 {
imsg.Text = fmt.Sprintf(`<a href="%s">%s</a>`, fi.URL, fi.URL)
messages = append(messages, imsg)
} else {
b.Log.Infof("Not forwarding file of type %s", mimeType)
}
continue
}
mimeType = strings.TrimSpace(strings.Split(mimeType, ";")[0])
// Build data:image/...;base64,... style image URL and embed image directly into the message
du := dataurl.New(*fi.Data, mimeType)
dataURL, err := du.MarshalText()
if err != nil {
b.Log.WithError(err).Infof("Image Serialization into data URL failed (type: %s, length: %d)", mimeType, len(*fi.Data))
continue
}
imsg.Text = fmt.Sprintf(`<img src="%s"/>`, dataURL)
messages = append(messages, imsg)
}
// Remove files from original message
msg.Extra["file"] = nil
return messages
}

268
bridge/mumble/mumble.go Normal file
View File

@ -0,0 +1,268 @@
package bmumble
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"io/ioutil"
"net"
"strconv"
"strings"
"time"
"layeh.com/gumble/gumble"
"layeh.com/gumble/gumbleutil"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
stripmd "github.com/writeas/go-strip-markdown"
// We need to import the 'data' package as an implicit dependency.
// See: https://godoc.org/github.com/paulrosania/go-charset/charset
_ "github.com/paulrosania/go-charset/data"
)
type Bmumble struct {
client *gumble.Client
Nick string
Host string
Channel *uint32
local chan config.Message
running chan error
connected chan gumble.DisconnectEvent
serverConfigUpdate chan gumble.ServerConfigEvent
serverConfig gumble.ServerConfigEvent
tlsConfig tls.Config
*bridge.Config
}
func New(cfg *bridge.Config) bridge.Bridger {
b := &Bmumble{}
b.Config = cfg
b.Nick = b.GetString("Nick")
b.local = make(chan config.Message)
b.running = make(chan error)
b.connected = make(chan gumble.DisconnectEvent)
b.serverConfigUpdate = make(chan gumble.ServerConfigEvent)
return b
}
func (b *Bmumble) Connect() error {
b.Log.Infof("Connecting %s", b.GetString("Server"))
host, portstr, err := net.SplitHostPort(b.GetString("Server"))
if err != nil {
return err
}
b.Host = host
_, err = strconv.Atoi(portstr)
if err != nil {
return err
}
if err = b.buildTLSConfig(); err != nil {
return err
}
go b.doSend()
go b.connectLoop()
err = <-b.running
return err
}
func (b *Bmumble) Disconnect() error {
return b.client.Disconnect()
}
func (b *Bmumble) JoinChannel(channel config.ChannelInfo) error {
cid, err := strconv.ParseUint(channel.Name, 10, 32)
if err != nil {
return err
}
channelID := uint32(cid)
if b.Channel != nil && *b.Channel != channelID {
b.Log.Fatalf("Cannot join channel ID '%d', already joined to channel ID %d", channelID, *b.Channel)
return errors.New("the Mumble bridge can only join a single channel")
}
b.Channel = &channelID
return b.doJoin(b.client, channelID)
}
func (b *Bmumble) Send(msg config.Message) (string, error) {
// Only process text messages
b.Log.Debugf("=> Received local message %#v", msg)
if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave {
return "", nil
}
attachments := b.extractFiles(&msg)
b.local <- msg
for _, a := range attachments {
b.local <- a
}
return "", nil
}
func (b *Bmumble) buildTLSConfig() error {
b.tlsConfig = tls.Config{}
// Load TLS client certificate keypair required for registered user authentication
if cpath := b.GetString("TLSClientCertificate"); cpath != "" {
if ckey := b.GetString("TLSClientKey"); ckey != "" {
cert, err := tls.LoadX509KeyPair(cpath, ckey)
if err != nil {
return err
}
b.tlsConfig.Certificates = []tls.Certificate{cert}
}
}
// Load TLS CA used for server verification. If not provided, the Go system trust anchor is used
if capath := b.GetString("TLSCACertificate"); capath != "" {
ca, err := ioutil.ReadFile(capath)
if err != nil {
return err
}
b.tlsConfig.RootCAs = x509.NewCertPool()
b.tlsConfig.RootCAs.AppendCertsFromPEM(ca)
}
b.tlsConfig.InsecureSkipVerify = b.GetBool("SkipTLSVerify")
return nil
}
func (b *Bmumble) connectLoop() {
firstConnect := true
for {
err := b.doConnect()
if firstConnect {
b.running <- err
}
if err != nil {
b.Log.Errorf("Connection to server failed: %#v", err)
if firstConnect {
break
} else {
b.Log.Info("Retrying in 10s")
time.Sleep(10 * time.Second)
continue
}
}
firstConnect = false
d := <-b.connected
switch d.Type {
case gumble.DisconnectError:
b.Log.Errorf("Lost connection to the server (%s), attempting reconnect", d.String)
continue
case gumble.DisconnectKicked:
b.Log.Errorf("Kicked from the server (%s), attempting reconnect", d.String)
continue
case gumble.DisconnectBanned:
b.Log.Errorf("Banned from the server (%s), not attempting reconnect", d.String)
close(b.connected)
close(b.running)
return
case gumble.DisconnectUser:
b.Log.Infof("Disconnect successful")
close(b.connected)
close(b.running)
return
}
}
}
func (b *Bmumble) doConnect() error {
// Create new gumble config and attach event handlers
gumbleConfig := gumble.NewConfig()
gumbleConfig.Attach(gumbleutil.Listener{
ServerConfig: b.handleServerConfig,
TextMessage: b.handleTextMessage,
Connect: b.handleConnect,
Disconnect: b.handleDisconnect,
UserChange: b.handleUserChange,
})
gumbleConfig.Username = b.GetString("Nick")
if password := b.GetString("Password"); password != "" {
gumbleConfig.Password = password
}
registerNullCodecAsOpus()
client, err := gumble.DialWithDialer(new(net.Dialer), b.GetString("Server"), gumbleConfig, &b.tlsConfig)
if err != nil {
return err
}
b.client = client
return nil
}
func (b *Bmumble) doJoin(client *gumble.Client, channelID uint32) error {
channel, ok := client.Channels[channelID]
if !ok {
return fmt.Errorf("no channel with ID %d", channelID)
}
client.Self.Move(channel)
return nil
}
func (b *Bmumble) doSend() {
// Message sending loop that makes sure server-side
// restrictions and client-side message traits don't conflict
// with each other.
for {
select {
case serverConfig := <-b.serverConfigUpdate:
b.Log.Debugf("Received server config update: AllowHTML=%#v, MaximumMessageLength=%#v", serverConfig.AllowHTML, serverConfig.MaximumMessageLength)
b.serverConfig = serverConfig
case msg := <-b.local:
b.processMessage(&msg)
}
}
}
func (b *Bmumble) processMessage(msg *config.Message) {
b.Log.Debugf("Processing message %s", msg.Text)
allowHTML := true
if b.serverConfig.AllowHTML != nil {
allowHTML = *b.serverConfig.AllowHTML
}
// If this is a specially generated image message, send it unmodified
if msg.Event == "mumble_image" {
if allowHTML {
b.client.Self.Channel.Send(msg.Username+msg.Text, false)
} else {
b.Log.Info("Can't send image, server does not allow HTML messages")
}
return
}
// Don't process empty messages
if len(msg.Text) == 0 {
return
}
// If HTML is allowed, convert markdown into HTML, otherwise strip markdown
if allowHTML {
msg.Text = helper.ParseMarkdown(msg.Text)
} else {
msg.Text = stripmd.Strip(msg.Text)
}
// If there is a maximum message length, split and truncate the lines
var msgLines []string
if maxLength := b.serverConfig.MaximumMessageLength; maxLength != nil {
if *maxLength != 0 { // Some servers will have unlimited message lengths.
// Not doing this makes underflows happen.
msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username), b.GetString("MessageClipped"))
} else {
msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
}
} else {
msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
}
// Send the individual lines
for i := range msgLines {
// Remove unnecessary newline character, since either way we're sending it as individual lines
msgLines[i] = strings.TrimSuffix(msgLines[i], "\n")
b.client.Self.Channel.Send(msg.Username+msgLines[i], false)
}
}

295
bridge/nctalk/nctalk.go Normal file
View File

@ -0,0 +1,295 @@
package nctalk
import (
"context"
"crypto/tls"
"strconv"
"strings"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"gomod.garykim.dev/nc-talk/ocs"
"gomod.garykim.dev/nc-talk/room"
"gomod.garykim.dev/nc-talk/user"
)
type Btalk struct {
user *user.TalkUser
rooms []Broom
*bridge.Config
}
func New(cfg *bridge.Config) bridge.Bridger {
return &Btalk{Config: cfg}
}
type Broom struct {
room *room.TalkRoom
ctx context.Context
ctxCancel context.CancelFunc
}
func (b *Btalk) Connect() error {
b.Log.Info("Connecting")
tconfig := &user.TalkUserConfig{
TLSConfig: &tls.Config{
InsecureSkipVerify: b.GetBool("SkipTLSVerify"), //nolint:gosec
},
}
var err error
b.user, err = user.NewUser(b.GetString("Server"), b.GetString("Login"), b.GetString("Password"), tconfig)
if err != nil {
b.Log.Error("Config could not be used")
return err
}
_, err = b.user.Capabilities()
if err != nil {
b.Log.Error("Cannot Connect")
return err
}
b.Log.Info("Connected")
return nil
}
func (b *Btalk) Disconnect() error {
for _, r := range b.rooms {
r.ctxCancel()
}
return nil
}
func (b *Btalk) JoinChannel(channel config.ChannelInfo) error {
tr, err := room.NewTalkRoom(b.user, channel.Name)
if err != nil {
return err
}
newRoom := Broom{
room: tr,
}
newRoom.ctx, newRoom.ctxCancel = context.WithCancel(context.Background())
c, err := newRoom.room.ReceiveMessages(newRoom.ctx)
if err != nil {
return err
}
b.rooms = append(b.rooms, newRoom)
go func() {
for msg := range c {
msg := msg
if msg.Error != nil {
b.Log.Errorf("Fatal message poll error: %s\n", msg.Error)
return
}
// Ignore messages that are from the bot user
if msg.ActorID == b.user.User || msg.ActorType == "bridged" {
continue
}
// Handle deleting messages
if msg.MessageType == ocs.MessageSystem && msg.Parent != nil && msg.Parent.MessageType == ocs.MessageDelete {
b.handleDeletingMessage(&msg, &newRoom)
continue
}
// Handle sending messages
if msg.MessageType == ocs.MessageComment {
b.handleSendingMessage(&msg, &newRoom)
continue
}
}
}()
return nil
}
func (b *Btalk) Send(msg config.Message) (string, error) {
r := b.getRoom(msg.Channel)
if r == nil {
b.Log.Errorf("Could not find room for %v", msg.Channel)
return "", nil
}
// Standard Message Send
if msg.Event == "" {
// Handle sending files if they are included
err := b.handleSendingFile(&msg, r)
if err != nil {
b.Log.Errorf("Could not send files in message to room %v from %v: %v", msg.Channel, msg.Username, err)
return "", nil
}
sentMessage, err := b.sendText(r, &msg, msg.Text)
if err != nil {
b.Log.Errorf("Could not send message to room %v from %v: %v", msg.Channel, msg.Username, err)
return "", nil
}
return strconv.Itoa(sentMessage.ID), nil
}
// Message Deletion
if msg.Event == config.EventMsgDelete {
messageID, err := strconv.Atoi(msg.ID)
if err != nil {
return "", err
}
data, err := r.room.DeleteMessage(messageID)
if err != nil {
return "", err
}
return strconv.Itoa(data.ID), nil
}
// Message is not a type that is currently supported
return "", nil
}
func (b *Btalk) getRoom(token string) *Broom {
for _, r := range b.rooms {
if r.room.Token == token {
return &r
}
}
return nil
}
func (b *Btalk) sendText(r *Broom, msg *config.Message, text string) (*ocs.TalkRoomMessageData, error) {
messageToSend := &room.Message{Message: msg.Username + text}
if b.GetBool("SeparateDisplayName") {
messageToSend.Message = text
messageToSend.ActorDisplayName = msg.Username
}
return r.room.SendComplexMessage(messageToSend)
}
func (b *Btalk) handleFiles(mmsg *config.Message, message *ocs.TalkRoomMessageData) error {
for _, parameter := range message.MessageParameters {
if parameter.Type == ocs.ROSTypeFile {
// Get the file
file, err := b.user.DownloadFile(parameter.Path)
if err != nil {
return err
}
if mmsg.Extra == nil {
mmsg.Extra = make(map[string][]interface{})
}
mmsg.Extra["file"] = append(mmsg.Extra["file"], config.FileInfo{
Name: parameter.Name,
Data: file,
Size: int64(len(*file)),
Avatar: false,
})
}
}
return nil
}
func (b *Btalk) handleSendingFile(msg *config.Message, r *Broom) error {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL == "" {
continue
}
message := ""
if fi.Comment != "" {
message += fi.Comment + " "
}
message += fi.URL
_, err := b.sendText(r, msg, message)
if err != nil {
return err
}
}
return nil
}
func (b *Btalk) handleSendingMessage(msg *ocs.TalkRoomMessageData, r *Broom) {
remoteMessage := config.Message{
Text: formatRichObjectString(msg.Message, msg.MessageParameters),
Channel: r.room.Token,
Username: DisplayName(msg, b.guestSuffix()),
UserID: msg.ActorID,
Account: b.Account,
}
// It is possible for the ID to not be set on older versions of Talk so we only set it if
// the ID is not blank
if msg.ID != 0 {
remoteMessage.ID = strconv.Itoa(msg.ID)
}
// Handle Files
err := b.handleFiles(&remoteMessage, msg)
if err != nil {
b.Log.Errorf("Error handling file: %#v", msg)
return
}
b.Log.Debugf("<= Message is %#v", remoteMessage)
b.Remote <- remoteMessage
}
func (b *Btalk) handleDeletingMessage(msg *ocs.TalkRoomMessageData, r *Broom) {
remoteMessage := config.Message{
Event: config.EventMsgDelete,
Text: config.EventMsgDelete,
Channel: r.room.Token,
ID: strconv.Itoa(msg.Parent.ID),
Account: b.Account,
}
b.Log.Debugf("<= Message being deleted is %#v", remoteMessage)
b.Remote <- remoteMessage
}
func (b *Btalk) guestSuffix() string {
guestSuffix := " (Guest)"
if b.IsKeySet("GuestSuffix") {
guestSuffix = b.GetString("GuestSuffix")
}
return guestSuffix
}
// Spec: https://github.com/nextcloud/server/issues/1706#issue-182308785
func formatRichObjectString(message string, parameters map[string]ocs.RichObjectString) string {
for id, parameter := range parameters {
text := parameter.Name
switch parameter.Type {
case ocs.ROSTypeUser, ocs.ROSTypeGroup:
text = "@" + text
case ocs.ROSTypeFile:
if parameter.Link != "" {
text = parameter.Name
}
}
message = strings.ReplaceAll(message, "{"+id+"}", text)
}
return message
}
func DisplayName(msg *ocs.TalkRoomMessageData, suffix string) string {
if msg.ActorType == ocs.ActorGuest {
if msg.ActorDisplayName == "" {
return "Guest"
}
return msg.ActorDisplayName + suffix
}
return msg.ActorDisplayName
}

View File

@ -0,0 +1,136 @@
package brocketchat
import (
"fmt"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/matterbridge/Rocket.Chat.Go.SDK/models"
)
func (b *Brocketchat) handleRocket() {
messages := make(chan *config.Message)
if b.GetString("WebhookBindAddress") != "" {
b.Log.Debugf("Choosing webhooks based receiving")
go b.handleRocketHook(messages)
} else {
b.Log.Debugf("Choosing login/password based receiving")
go b.handleRocketClient(messages)
}
for message := range messages {
message.Account = b.Account
b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
b.Log.Debugf("<= Message is %#v", message)
b.Remote <- *message
}
}
func (b *Brocketchat) handleRocketHook(messages chan *config.Message) {
for {
message := b.rh.Receive()
b.Log.Debugf("Receiving from rockethook %#v", message)
// do not loop
if message.UserName == b.GetString("Nick") {
continue
}
messages <- &config.Message{
UserID: message.UserID,
Username: message.UserName,
Text: message.Text,
Channel: message.ChannelName,
}
}
}
func (b *Brocketchat) handleStatusEvent(ev models.Message, rmsg *config.Message) bool {
switch ev.Type {
case "":
// this is a normal message, no processing needed
// return true so the message is not dropped
return true
case sUserJoined, sUserLeft:
rmsg.Event = config.EventJoinLeave
return true
case sRoomChangedTopic:
rmsg.Event = config.EventTopicChange
return true
}
b.Log.Debugf("Dropping message with unknown type: %s", ev.Type)
return false
}
func (b *Brocketchat) handleRocketClient(messages chan *config.Message) {
for message := range b.messageChan {
message := message
// skip messages with same ID, apparently messages get duplicated for an unknown reason
if _, ok := b.cache.Get(message.ID); ok {
continue
}
b.cache.Add(message.ID, true)
b.Log.Debugf("message %#v", message)
m := message
if b.skipMessage(&m) {
b.Log.Debugf("Skipped message: %#v", message)
continue
}
rmsg := &config.Message{Text: message.Msg,
Username: message.User.UserName,
Channel: b.getChannelName(message.RoomID),
Account: b.Account,
UserID: message.User.ID,
ID: message.ID,
Extra: make(map[string][]interface{}),
}
b.handleAttachments(&message, rmsg)
// handleStatusEvent returns false if the message should be dropped
// in that case it is probably some modification to the channel we do not want to relay
if b.handleStatusEvent(m, rmsg) {
messages <- rmsg
}
}
}
func (b *Brocketchat) handleAttachments(message *models.Message, rmsg *config.Message) {
if rmsg.Text == "" {
for _, attachment := range message.Attachments {
if attachment.Title != "" {
rmsg.Text = attachment.Title + "\n"
}
if attachment.Title != "" && attachment.Text != "" {
rmsg.Text += "\n"
}
if attachment.Text != "" {
rmsg.Text += attachment.Text
}
}
}
for i := range message.Attachments {
if err := b.handleDownloadFile(rmsg, &message.Attachments[i]); err != nil {
b.Log.Errorf("Could not download incoming file: %#v", err)
}
}
}
func (b *Brocketchat) handleDownloadFile(rmsg *config.Message, file *models.Attachment) error {
downloadURL := b.GetString("server") + file.TitleLink
data, err := helper.DownloadFileAuthRocket(downloadURL, b.user.Token, b.user.ID)
if err != nil {
return fmt.Errorf("download %s failed %#v", downloadURL, err)
}
helper.HandleDownloadData(b.Log, rmsg, file.Title, rmsg.Text, downloadURL, data, b.General)
return nil
}
func (b *Brocketchat) handleUploadFile(msg *config.Message) error {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if err := b.uploadFile(&fi, b.getChannelID(msg.Channel)); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,202 @@
package brocketchat
import (
"context"
"io/ioutil"
"mime"
"net/http"
"net/url"
"strings"
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/hook/rockethook"
"github.com/42wim/matterbridge/matterhook"
"github.com/matterbridge/Rocket.Chat.Go.SDK/models"
"github.com/matterbridge/Rocket.Chat.Go.SDK/realtime"
"github.com/matterbridge/Rocket.Chat.Go.SDK/rest"
"github.com/nelsonken/gomf"
)
func (b *Brocketchat) doConnectWebhookBind() error {
switch {
case b.GetString("WebhookURL") != "":
b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
DisableServer: true})
b.rh = rockethook.New(b.GetString("WebhookURL"), rockethook.Config{BindAddress: b.GetString("WebhookBindAddress")})
case b.GetString("Login") != "":
b.Log.Info("Connecting using login/password (sending)")
err := b.apiLogin()
if err != nil {
return err
}
default:
b.Log.Info("Connecting using webhookbindaddress (receiving)")
b.rh = rockethook.New(b.GetString("WebhookURL"), rockethook.Config{BindAddress: b.GetString("WebhookBindAddress")})
}
return nil
}
func (b *Brocketchat) doConnectWebhookURL() error {
b.Log.Info("Connecting using webhookurl (sending)")
b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
DisableServer: true})
if b.GetString("Login") != "" {
b.Log.Info("Connecting using login/password (receiving)")
err := b.apiLogin()
if err != nil {
return err
}
}
return nil
}
func (b *Brocketchat) apiLogin() error {
b.Log.Debugf("handling apiLogin()")
credentials := &models.UserCredentials{Email: b.GetString("login"), Password: b.GetString("password")}
if b.GetString("Token") != "" {
credentials = &models.UserCredentials{ID: b.GetString("Login"), Token: b.GetString("Token")}
}
myURL, err := url.Parse(b.GetString("server"))
if err != nil {
return err
}
client, err := realtime.NewClient(myURL, b.GetBool("debug"))
b.c = client
if err != nil {
return err
}
restclient := rest.NewClient(myURL, b.GetBool("debug"))
user, err := b.c.Login(credentials)
if err != nil {
return err
}
b.user = user
b.r = restclient
err = b.r.Login(credentials)
if err != nil {
return err
}
b.Log.Info("Connection succeeded")
return nil
}
func (b *Brocketchat) getChannelName(id string) string {
b.RLock()
defer b.RUnlock()
if name, ok := b.channelMap[id]; ok {
return name
}
return ""
}
func (b *Brocketchat) getChannelID(name string) string {
b.RLock()
defer b.RUnlock()
for k, v := range b.channelMap {
if v == name || v == "#"+name {
return k
}
}
return ""
}
func (b *Brocketchat) skipMessage(message *models.Message) bool {
return message.User.ID == b.user.ID
}
func (b *Brocketchat) uploadFile(fi *config.FileInfo, channel string) error {
fb := gomf.New()
if err := fb.WriteField("description", fi.Comment); err != nil {
return err
}
sp := strings.Split(fi.Name, ".")
mtype := mime.TypeByExtension("." + sp[len(sp)-1])
if !strings.Contains(mtype, "image") && !strings.Contains(mtype, "video") {
return nil
}
if err := fb.WriteFile("file", fi.Name, mtype, *fi.Data); err != nil {
return err
}
req, err := fb.GetHTTPRequest(context.TODO(), b.GetString("server")+"/api/v1/rooms.upload/"+channel)
if err != nil {
return err
}
req.Header.Add("X-Auth-Token", b.user.Token)
req.Header.Add("X-User-Id", b.user.ID)
client := &http.Client{
Timeout: time.Second * 5,
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != 200 {
b.Log.Errorf("failed: %#v", string(body))
}
return nil
}
// sendWebhook uses the configured WebhookURL to send the message
func (b *Brocketchat) sendWebhook(msg *config.Message) error {
// skip events
if msg.Event != "" {
return nil
}
if b.GetBool("PrefixMessagesWithNick") {
msg.Text = msg.Username + msg.Text
}
if msg.Extra != nil {
// this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE
for _, rmsg := range helper.HandleExtra(msg, b.General) {
rmsg := rmsg // scopelint
iconURL := config.GetIconURL(&rmsg, b.GetString("iconurl"))
matterMessage := matterhook.OMessage{
IconURL: iconURL,
Channel: rmsg.Channel,
UserName: rmsg.Username,
Text: rmsg.Text,
Props: make(map[string]interface{}),
}
if err := b.mh.Send(matterMessage); err != nil {
b.Log.Errorf("sendWebhook failed: %s ", err)
}
}
// webhook doesn't support file uploads, so we add the url manually
if len(msg.Extra["file"]) > 0 {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
msg.Text += fi.URL
}
}
}
}
iconURL := config.GetIconURL(msg, b.GetString("iconurl"))
matterMessage := matterhook.OMessage{
IconURL: iconURL,
Channel: msg.Channel,
UserName: msg.Username,
Text: msg.Text,
}
if msg.Avatar != "" {
matterMessage.IconURL = msg.Avatar
}
err := b.mh.Send(matterMessage)
if err != nil {
b.Log.Info(err)
return err
}
return nil
}

View File

@ -0,0 +1,181 @@
package brocketchat
import (
"errors"
"strings"
"sync"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/hook/rockethook"
"github.com/42wim/matterbridge/matterhook"
lru "github.com/hashicorp/golang-lru"
"github.com/matterbridge/Rocket.Chat.Go.SDK/models"
"github.com/matterbridge/Rocket.Chat.Go.SDK/realtime"
"github.com/matterbridge/Rocket.Chat.Go.SDK/rest"
)
type Brocketchat struct {
mh *matterhook.Client
rh *rockethook.Client
c *realtime.Client
r *rest.Client
cache *lru.Cache
*bridge.Config
messageChan chan models.Message
channelMap map[string]string
user *models.User
sync.RWMutex
}
const (
sUserJoined = "uj"
sUserLeft = "ul"
sRoomChangedTopic = "room_changed_topic"
)
func New(cfg *bridge.Config) bridge.Bridger {
newCache, err := lru.New(100)
if err != nil {
cfg.Log.Fatalf("Could not create LRU cache for rocketchat bridge: %v", err)
}
b := &Brocketchat{
Config: cfg,
messageChan: make(chan models.Message),
channelMap: make(map[string]string),
cache: newCache,
}
b.Log.Debugf("enabling rocketchat")
return b
}
func (b *Brocketchat) Command(cmd string) string {
return ""
}
func (b *Brocketchat) Connect() error {
if b.GetString("WebhookBindAddress") != "" {
if err := b.doConnectWebhookBind(); err != nil {
return err
}
go b.handleRocket()
return nil
}
switch {
case b.GetString("WebhookURL") != "":
if err := b.doConnectWebhookURL(); err != nil {
return err
}
go b.handleRocket()
return nil
case b.GetString("Login") != "":
b.Log.Info("Connecting using login/password (sending and receiving)")
err := b.apiLogin()
if err != nil {
return err
}
go b.handleRocket()
}
if b.GetString("WebhookBindAddress") == "" && b.GetString("WebhookURL") == "" &&
b.GetString("Login") == "" {
return errors.New("no connection method found. See that you have WebhookBindAddress, WebhookURL or Login/Password/Server configured")
}
return nil
}
func (b *Brocketchat) Disconnect() error {
return nil
}
func (b *Brocketchat) JoinChannel(channel config.ChannelInfo) error {
if b.c == nil {
return nil
}
id, err := b.c.GetChannelId(strings.TrimPrefix(channel.Name, "#"))
if err != nil {
return err
}
b.Lock()
b.channelMap[id] = channel.Name
b.Unlock()
mychannel := &models.Channel{ID: id, Name: strings.TrimPrefix(channel.Name, "#")}
if err := b.c.JoinChannel(id); err != nil {
return err
}
if err := b.c.SubscribeToMessageStream(mychannel, b.messageChan); err != nil {
return err
}
return nil
}
func (b *Brocketchat) Send(msg config.Message) (string, error) {
// strip the # if people has set this
msg.Channel = strings.TrimPrefix(msg.Channel, "#")
channel := &models.Channel{ID: b.getChannelID(msg.Channel), Name: msg.Channel}
// Make a action /me of the message
if msg.Event == config.EventUserAction {
msg.Text = "_" + msg.Text + "_"
}
// Delete message
if msg.Event == config.EventMsgDelete {
if msg.ID == "" {
return "", nil
}
return msg.ID, b.c.DeleteMessage(&models.Message{ID: msg.ID})
}
// Use webhook to send the message
if b.GetString("WebhookURL") != "" {
return "", b.sendWebhook(&msg)
}
// Prepend nick if configured
if b.GetBool("PrefixMessagesWithNick") {
msg.Text = msg.Username + msg.Text
}
// Edit message if we have an ID
if msg.ID != "" {
return msg.ID, b.c.EditMessage(&models.Message{ID: msg.ID, Msg: msg.Text, RoomID: b.getChannelID(msg.Channel)})
}
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
// strip the # if people has set this
rmsg.Channel = strings.TrimPrefix(rmsg.Channel, "#")
smsg := &models.Message{
RoomID: b.getChannelID(rmsg.Channel),
Msg: rmsg.Username + rmsg.Text,
PostMessage: models.PostMessage{
Avatar: rmsg.Avatar,
Alias: rmsg.Username,
},
}
if _, err := b.c.SendMessage(smsg); err != nil {
b.Log.Errorf("SendMessage failed: %s", err)
}
}
if len(msg.Extra["file"]) > 0 {
return "", b.handleUploadFile(&msg)
}
}
smsg := &models.Message{
RoomID: channel.ID,
Msg: msg.Text,
PostMessage: models.PostMessage{
Avatar: msg.Avatar,
Alias: msg.Username,
},
}
rmsg, err := b.c.SendMessage(smsg)
if rmsg == nil {
return "", err
}
return rmsg.ID, err
}

415
bridge/slack/handlers.go Normal file
View File

@ -0,0 +1,415 @@
package bslack
import (
"errors"
"fmt"
"html"
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/slack-go/slack"
)
// ErrEventIgnored is for events that should be ignored
var ErrEventIgnored = errors.New("this event message should ignored")
func (b *Bslack) handleSlack() {
messages := make(chan *config.Message)
if b.GetString(incomingWebhookConfig) != "" && b.GetString(tokenConfig) == "" {
b.Log.Debugf("Choosing webhooks based receiving")
go b.handleMatterHook(messages)
} else {
b.Log.Debugf("Choosing token based receiving")
go b.handleSlackClient(messages)
}
time.Sleep(time.Second)
b.Log.Debug("Start listening for Slack messages")
for message := range messages {
// don't do any action on deleted/typing messages
if message.Event != config.EventUserTyping && message.Event != config.EventMsgDelete &&
message.Event != config.EventFileDelete {
b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
// cleanup the message
message.Text = b.replaceMention(message.Text)
message.Text = b.replaceVariable(message.Text)
message.Text = b.replaceChannel(message.Text)
message.Text = b.replaceURL(message.Text)
message.Text = b.replaceb0rkedMarkDown(message.Text)
message.Text = html.UnescapeString(message.Text)
// Add the avatar
message.Avatar = b.users.getAvatar(message.UserID)
}
b.Log.Debugf("<= Message is %#v", message)
b.Remote <- *message
}
}
func (b *Bslack) handleSlackClient(messages chan *config.Message) {
for msg := range b.rtm.IncomingEvents {
if msg.Type != sUserTyping && msg.Type != sHello && msg.Type != sLatencyReport {
b.Log.Debugf("== Receiving event %#v", msg.Data)
}
switch ev := msg.Data.(type) {
case *slack.UserTypingEvent:
if !b.GetBool("ShowUserTyping") {
continue
}
rmsg, err := b.handleTypingEvent(ev)
if err == ErrEventIgnored {
continue
} else if err != nil {
b.Log.Errorf("%#v", err)
continue
}
messages <- rmsg
case *slack.MessageEvent:
if b.skipMessageEvent(ev) {
b.Log.Debugf("Skipped message: %#v", ev)
continue
}
rmsg, err := b.handleMessageEvent(ev)
if err != nil {
b.Log.Errorf("%#v", err)
continue
}
messages <- rmsg
case *slack.FileDeletedEvent:
rmsg, err := b.handleFileDeletedEvent(ev)
if err != nil {
b.Log.Printf("%#v", err)
continue
}
messages <- rmsg
case *slack.OutgoingErrorEvent:
b.Log.Debugf("%#v", ev.Error())
case *slack.ChannelJoinedEvent:
// When we join a channel we update the full list of users as
// well as the information for the channel that we joined as this
// should now tell that we are a member of it.
b.channels.registerChannel(ev.Channel)
case *slack.ConnectedEvent:
b.si = ev.Info
b.channels.populateChannels(true)
b.users.populateUsers(true)
case *slack.InvalidAuthEvent:
b.Log.Fatalf("Invalid Token %#v", ev)
case *slack.ConnectionErrorEvent:
b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj)
case *slack.MemberJoinedChannelEvent:
b.users.populateUser(ev.User)
case *slack.HelloEvent, *slack.LatencyReport, *slack.ConnectingEvent:
continue
case *slack.UserChangeEvent:
b.users.invalidateUser(ev.User.ID)
default:
b.Log.Debugf("Unhandled incoming event: %T", ev)
}
}
}
func (b *Bslack) handleMatterHook(messages chan *config.Message) {
for {
message := b.mh.Receive()
b.Log.Debugf("receiving from matterhook (slack) %#v", message)
if message.UserName == "slackbot" {
continue
}
messages <- &config.Message{
Username: message.UserName,
Text: message.Text,
Channel: message.ChannelName,
}
}
}
// skipMessageEvent skips event that need to be skipped :-)
func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
switch ev.SubType {
case sChannelLeave, sChannelJoin:
return b.GetBool(noSendJoinConfig)
case sPinnedItem, sUnpinnedItem:
return true
case sChannelTopic, sChannelPurpose:
// Skip the event if our bot/user account changed the topic/purpose
if ev.User == b.si.User.ID {
return true
}
}
// Check for our callback ID
hasOurCallbackID := false
if len(ev.Blocks.BlockSet) == 1 {
block, ok := ev.Blocks.BlockSet[0].(*slack.SectionBlock)
hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid
}
if ev.SubMessage != nil {
// It seems ev.SubMessage.Edited == nil when slack unfurls.
// Do not forward these messages. See Github issue #266.
if ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp &&
ev.SubMessage.Edited == nil {
return true
}
// see hidden subtypes at https://api.slack.com/events/message
// these messages are sent when we add a message to a thread #709
if ev.SubType == "message_replied" && ev.Hidden {
return true
}
if len(ev.SubMessage.Blocks.BlockSet) == 1 {
block, ok := ev.SubMessage.Blocks.BlockSet[0].(*slack.SectionBlock)
hasOurCallbackID = ok && block.BlockID == "matterbridge_"+b.uuid
}
}
// Skip any messages that we made ourselves or from 'slackbot' (see #527).
if ev.Username == sSlackBotUser ||
(b.rtm != nil && ev.Username == b.si.User.Name) || hasOurCallbackID {
return true
}
if len(ev.Files) > 0 {
return b.filesCached(ev.Files)
}
return false
}
func (b *Bslack) filesCached(files []slack.File) bool {
for i := range files {
if !b.fileCached(&files[i]) {
return false
}
}
return true
}
// handleMessageEvent handles the message events. Together with any called sub-methods,
// this method implements the following event processing pipeline:
//
// 1. Check if the message should be ignored.
// NOTE: This is not actually part of the method below but is done just before it
// is called via the 'skipMessageEvent()' method.
// 2. Populate the Matterbridge message that will be sent to the router based on the
// received event and logic that is common to all events that are not skipped.
// 3. Detect and handle any message that is "status" related (think join channel, etc.).
// This might result in an early exit from the pipeline and passing of the
// pre-populated message to the Matterbridge router.
// 4. Handle the specific case of messages that edit existing messages depending on
// configuration.
// 5. Handle any attachments of the received event.
// 6. Check that the Matterbridge message that we end up with after at the end of the
// pipeline is valid before sending it to the Matterbridge router.
func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, error) {
rmsg, err := b.populateReceivedMessage(ev)
if err != nil {
return nil, err
}
// Handle some message types early.
if b.handleStatusEvent(ev, rmsg) {
return rmsg, nil
}
b.handleAttachments(ev, rmsg)
// Verify that we have the right information and the message
// is well-formed before sending it out to the router.
if len(ev.Files) == 0 && (rmsg.Text == "" || rmsg.Username == "") {
if ev.BotID != "" {
// This is probably a webhook we couldn't resolve.
return nil, fmt.Errorf("message handling resulted in an empty bot message (probably an incoming webhook we couldn't resolve): %#v", ev)
}
if ev.SubMessage != nil {
return nil, fmt.Errorf("message handling resulted in an empty message: %#v with submessage %#v", ev, ev.SubMessage)
}
return nil, fmt.Errorf("message handling resulted in an empty message: %#v", ev)
}
return rmsg, nil
}
func (b *Bslack) handleFileDeletedEvent(ev *slack.FileDeletedEvent) (*config.Message, error) {
if rawChannel, ok := b.cache.Get(cfileDownloadChannel + ev.FileID); ok {
channel, err := b.channels.getChannelByID(rawChannel.(string))
if err != nil {
return nil, err
}
return &config.Message{
Event: config.EventFileDelete,
Text: config.EventFileDelete,
Channel: channel.Name,
Account: b.Account,
ID: ev.FileID,
Protocol: b.Protocol,
}, nil
}
return nil, fmt.Errorf("channel ID for file ID %s not found", ev.FileID)
}
func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) bool {
switch ev.SubType {
case sChannelJoined, sMemberJoined:
// There's no further processing needed on channel events
// so we return 'true'.
return true
case sChannelJoin, sChannelLeave:
rmsg.Username = sSystemUser
rmsg.Event = config.EventJoinLeave
case sChannelTopic, sChannelPurpose:
b.channels.populateChannels(false)
rmsg.Event = config.EventTopicChange
case sMessageChanged:
rmsg.Text = ev.SubMessage.Text
// handle deleted thread starting messages
if ev.SubMessage.Text == "This message was deleted." {
rmsg.Event = config.EventMsgDelete
return true
}
case sMessageDeleted:
rmsg.Text = config.EventMsgDelete
rmsg.Event = config.EventMsgDelete
rmsg.ID = ev.DeletedTimestamp
// If a message is being deleted we do not need to process
// the event any further so we return 'true'.
return true
case sMeMessage:
rmsg.Event = config.EventUserAction
}
return false
}
func getMessageTitle(attach *slack.Attachment) string {
if attach.TitleLink != "" {
return fmt.Sprintf("[%s](%s)\n", attach.Title, attach.TitleLink)
}
return attach.Title
}
func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message) {
// File comments are set by the system (because there is no username given).
if ev.SubType == sFileComment {
rmsg.Username = sSystemUser
}
// See if we have some text in the attachments.
if rmsg.Text == "" {
for i, attach := range ev.Attachments {
if attach.Text != "" {
if attach.Title != "" {
rmsg.Text = getMessageTitle(&ev.Attachments[i])
}
rmsg.Text += attach.Text
if attach.Footer != "" {
rmsg.Text += "\n\n" + attach.Footer
}
} else {
rmsg.Text = attach.Fallback
}
}
}
// Save the attachments, so that we can send them to other slack (compatible) bridges.
if len(ev.Attachments) > 0 {
rmsg.Extra[sSlackAttachment] = append(rmsg.Extra[sSlackAttachment], ev.Attachments)
}
// If we have files attached, download them (in memory) and put a pointer to it in msg.Extra.
for i := range ev.Files {
// keep reference in cache on which channel we added this file
b.cache.Add(cfileDownloadChannel+ev.Files[i].ID, ev.Channel)
if err := b.handleDownloadFile(rmsg, &ev.Files[i], false); err != nil {
b.Log.Errorf("Could not download incoming file: %#v", err)
}
}
}
func (b *Bslack) handleTypingEvent(ev *slack.UserTypingEvent) (*config.Message, error) {
if ev.User == b.si.User.ID {
return nil, ErrEventIgnored
}
channelInfo, err := b.channels.getChannelByID(ev.Channel)
if err != nil {
return nil, err
}
return &config.Message{
Channel: channelInfo.Name,
Account: b.Account,
Event: config.EventUserTyping,
}, nil
}
// handleDownloadFile handles file download
func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File, retry bool) error {
if b.fileCached(file) {
return nil
}
// Check that the file is neither too large nor blacklisted.
if err := helper.HandleDownloadSize(b.Log, rmsg, file.Name, int64(file.Size), b.General); err != nil {
b.Log.WithError(err).Infof("Skipping download of incoming file.")
return nil
}
// Actually download the file.
data, err := helper.DownloadFileAuth(file.URLPrivateDownload, "Bearer "+b.GetString(tokenConfig))
if err != nil {
return fmt.Errorf("download %s failed %#v", file.URLPrivateDownload, err)
}
if len(*data) != file.Size && !retry {
b.Log.Debugf("Data size (%d) is not equal to size declared (%d)\n", len(*data), file.Size)
time.Sleep(1 * time.Second)
return b.handleDownloadFile(rmsg, file, true)
}
// If a comment is attached to the file(s) it is in the 'Text' field of the Slack messge event
// and should be added as comment to only one of the files. We reset the 'Text' field to ensure
// that the comment is not duplicated.
comment := rmsg.Text
rmsg.Text = ""
helper.HandleDownloadData2(b.Log, rmsg, file.Name, file.ID, comment, file.URLPrivateDownload, data, b.General)
return nil
}
// handleGetChannelMembers handles messages containing the GetChannelMembers event
// Sends a message to the router containing *config.ChannelMembers
func (b *Bslack) handleGetChannelMembers(rmsg *config.Message) bool {
if rmsg.Event != config.EventGetChannelMembers {
return false
}
cMembers := b.channels.getChannelMembers(b.users)
extra := make(map[string][]interface{})
extra[config.EventGetChannelMembers] = append(extra[config.EventGetChannelMembers], cMembers)
msg := config.Message{
Extra: extra,
Event: config.EventGetChannelMembers,
Account: b.Account,
}
b.Log.Debugf("sending msg to remote %#v", msg)
b.Remote <- msg
return true
}
// fileCached implements Matterbridge's caching logic for files
// shared via Slack.
//
// We consider that a file was cached if its ID was added in the last minute or
// it's name was registered in the last 10 seconds. This ensures that an
// identically named file but with different content will be uploaded correctly
// (the assumption is that such name collisions will not occur within the given
// timeframes).
func (b *Bslack) fileCached(file *slack.File) bool {
if ts, ok := b.cache.Get("file" + file.ID); ok && time.Since(ts.(time.Time)) < time.Minute {
return true
} else if ts, ok = b.cache.Get("filename" + file.Name); ok && time.Since(ts.(time.Time)) < 10*time.Second {
return true
}
return false
}

257
bridge/slack/helpers.go Normal file
View File

@ -0,0 +1,257 @@
package bslack
import (
"fmt"
"regexp"
"strings"
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/sirupsen/logrus"
"github.com/slack-go/slack"
)
// populateReceivedMessage shapes the initial Matterbridge message that we will forward to the
// router before we apply message-dependent modifications.
func (b *Bslack) populateReceivedMessage(ev *slack.MessageEvent) (*config.Message, error) {
// Use our own func because rtm.GetChannelInfo doesn't work for private channels.
channel, err := b.channels.getChannelByID(ev.Channel)
if err != nil {
return nil, err
}
rmsg := &config.Message{
Text: ev.Text,
Channel: channel.Name,
Account: b.Account,
ID: ev.Timestamp,
Extra: make(map[string][]interface{}),
ParentID: ev.ThreadTimestamp,
Protocol: b.Protocol,
}
if b.useChannelID {
rmsg.Channel = "ID:" + channel.ID
}
// Handle 'edit' messages.
if ev.SubMessage != nil && !b.GetBool(editDisableConfig) {
rmsg.ID = ev.SubMessage.Timestamp
if ev.SubMessage.ThreadTimestamp != ev.SubMessage.Timestamp {
b.Log.Debugf("SubMessage %#v", ev.SubMessage)
rmsg.Text = ev.SubMessage.Text + b.GetString(editSuffixConfig)
}
}
// For edits, only submessage has thread ts.
// Ensures edits to threaded messages maintain their prefix hint on the
// unthreaded end.
if ev.SubMessage != nil {
rmsg.ParentID = ev.SubMessage.ThreadTimestamp
}
if err = b.populateMessageWithUserInfo(ev, rmsg); err != nil {
return nil, err
}
return rmsg, err
}
func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *config.Message) error {
if ev.SubType == sMessageDeleted || ev.SubType == sFileComment {
return nil
}
// First, deal with bot-originating messages but only do so when not using webhooks: we
// would not be able to distinguish which bot would be sending them.
if err := b.populateMessageWithBotInfo(ev, rmsg); err != nil {
return err
}
// Second, deal with "real" users if we have the necessary information.
var userID string
switch {
case ev.User != "":
userID = ev.User
case ev.SubMessage != nil && ev.SubMessage.User != "":
userID = ev.SubMessage.User
default:
return nil
}
user := b.users.getUser(userID)
if user == nil {
return fmt.Errorf("could not find information for user with id %s", ev.User)
}
rmsg.UserID = user.ID
rmsg.Username = user.Name
if user.Profile.DisplayName != "" {
rmsg.Username = user.Profile.DisplayName
}
if b.GetBool("UseFullName") && user.Profile.RealName != "" {
rmsg.Username = user.Profile.RealName
}
return nil
}
func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config.Message) error {
if ev.BotID == "" || b.GetString(outgoingWebhookConfig) != "" {
return nil
}
var err error
var bot *slack.Bot
for {
bot, err = b.rtm.GetBotInfo(slack.GetBotInfoParameters{
Bot: ev.BotID,
})
if err == nil {
break
}
if err = handleRateLimit(b.Log, err); err != nil {
b.Log.Errorf("Could not retrieve bot information: %#v", err)
return err
}
}
b.Log.Debugf("Found bot %#v", bot)
if bot.Name != "" {
rmsg.Username = bot.Name
if ev.Username != "" {
rmsg.Username = ev.Username
}
rmsg.UserID = bot.ID
}
return nil
}
var (
mentionRE = regexp.MustCompile(`<@([a-zA-Z0-9]+)>`)
channelRE = regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`)
variableRE = regexp.MustCompile(`<!((?:subteam\^)?[a-zA-Z0-9]+)(?:\|@?(.+?))?>`)
urlRE = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
codeFenceRE = regexp.MustCompile(`(?m)^` + "```" + `\w+$`)
topicOrPurposeRE = regexp.MustCompile(`(?s)(@.+) (cleared|set)(?: the)? channel (topic|purpose)(?:: (.*))?`)
)
func (b *Bslack) extractTopicOrPurpose(text string) (string, string) {
r := topicOrPurposeRE.FindStringSubmatch(text)
if len(r) == 5 {
action, updateType, extracted := r[2], r[3], r[4]
switch action {
case "set":
return updateType, extracted
case "cleared":
return updateType, ""
}
}
b.Log.Warnf("Encountered channel topic or purpose change message with unexpected format: %s", text)
return "unknown", ""
}
// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
func (b *Bslack) replaceMention(text string) string {
replaceFunc := func(match string) string {
userID := strings.Trim(match, "@<>")
if username := b.users.getUsername(userID); userID != "" {
return "@" + username
}
return match
}
return mentionRE.ReplaceAllStringFunc(text, replaceFunc)
}
// @see https://api.slack.com/docs/message-formatting#linking_to_channels_and_users
func (b *Bslack) replaceChannel(text string) string {
for _, r := range channelRE.FindAllStringSubmatch(text, -1) {
text = strings.Replace(text, r[0], "#"+r[1], 1)
}
return text
}
// @see https://api.slack.com/docs/message-formatting#variables
func (b *Bslack) replaceVariable(text string) string {
for _, r := range variableRE.FindAllStringSubmatch(text, -1) {
if r[2] != "" {
text = strings.Replace(text, r[0], "@"+r[2], 1)
} else {
text = strings.Replace(text, r[0], "@"+r[1], 1)
}
}
return text
}
// @see https://api.slack.com/docs/message-formatting#linking_to_urls
func (b *Bslack) replaceURL(text string) string {
return urlRE.ReplaceAllString(text, "[${2}](${1})")
}
func (b *Bslack) replaceb0rkedMarkDown(text string) string {
// taken from https://github.com/mattermost/mattermost-server/blob/master/app/slackimport.go
//
regexReplaceAllString := []struct {
regex *regexp.Regexp
rpl string
}{
// bold
{
regexp.MustCompile(`(^|[\s.;,])\*(\S[^*\n]+)\*`),
"$1**$2**",
},
// strikethrough
{
regexp.MustCompile(`(^|[\s.;,])\~(\S[^~\n]+)\~`),
"$1~~$2~~",
},
// single paragraph blockquote
// Slack converts > character to &gt;
{
regexp.MustCompile(`(?sm)^&gt;`),
">",
},
}
for _, rule := range regexReplaceAllString {
text = rule.regex.ReplaceAllString(text, rule.rpl)
}
return text
}
func (b *Bslack) replaceCodeFence(text string) string {
return codeFenceRE.ReplaceAllString(text, "```")
}
// getUsersInConversation returns an array of userIDs that are members of channelID
func (b *Bslack) getUsersInConversation(channelID string) ([]string, error) {
channelMembers := []string{}
for {
queryParams := &slack.GetUsersInConversationParameters{
ChannelID: channelID,
}
members, nextCursor, err := b.sc.GetUsersInConversation(queryParams)
if err != nil {
if err = handleRateLimit(b.Log, err); err != nil {
return channelMembers, fmt.Errorf("Could not retrieve users in channels: %#v", err)
}
continue
}
channelMembers = append(channelMembers, members...)
if nextCursor == "" {
break
}
queryParams.Cursor = nextCursor
}
return channelMembers, nil
}
func handleRateLimit(log *logrus.Entry, err error) error {
rateLimit, ok := err.(*slack.RateLimitedError)
if !ok {
return err
}
log.Infof("Rate-limited by Slack. Sleeping for %v", rateLimit.RetryAfter)
time.Sleep(rateLimit.RetryAfter)
return nil
}

View File

@ -0,0 +1,36 @@
package bslack
import (
"io/ioutil"
"testing"
"github.com/42wim/matterbridge/bridge"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
)
func TestExtractTopicOrPurpose(t *testing.T) {
testcases := map[string]struct {
input string
wantChangeType string
wantOutput string
}{
"success - topic type": {"@someone set channel topic: foo bar", "topic", "foo bar"},
"success - purpose type": {"@someone set channel purpose: foo bar", "purpose", "foo bar"},
"success - one line": {"@someone set channel topic: foo bar", "topic", "foo bar"},
"success - multi-line": {"@someone set channel topic: foo\nbar", "topic", "foo\nbar"},
"success - cleared": {"@someone cleared channel topic", "topic", ""},
"error - unhandled": {"some unmatched message", "unknown", ""},
}
logger := logrus.New()
logger.SetOutput(ioutil.Discard)
cfg := &bridge.Config{Bridge: &bridge.Bridge{Log: logrus.NewEntry(logger)}}
b := newBridge(cfg)
for name, tc := range testcases {
gotChangeType, gotOutput := b.extractTopicOrPurpose(tc.input)
assert.Equalf(t, tc.wantChangeType, gotChangeType, "This testcase failed: %s", name)
assert.Equalf(t, tc.wantOutput, gotOutput, "This testcase failed: %s", name)
}
}

80
bridge/slack/legacy.go Normal file
View File

@ -0,0 +1,80 @@
package bslack
import (
"errors"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/matterhook"
"github.com/slack-go/slack"
)
type BLegacy struct {
*Bslack
}
func NewLegacy(cfg *bridge.Config) bridge.Bridger {
b := &BLegacy{Bslack: newBridge(cfg)}
b.legacy = true
return b
}
func (b *BLegacy) Connect() error {
b.RLock()
defer b.RUnlock()
if b.GetString(incomingWebhookConfig) != "" {
switch {
case b.GetString(outgoingWebhookConfig) != "":
b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{
InsecureSkipVerify: b.GetBool(skipTLSConfig),
BindAddress: b.GetString(incomingWebhookConfig),
})
case b.GetString(tokenConfig) != "":
b.Log.Info("Connecting using token (sending)")
b.sc = slack.New(b.GetString(tokenConfig))
b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection()
b.Log.Info("Connecting using webhookbindaddress (receiving)")
b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{
InsecureSkipVerify: b.GetBool(skipTLSConfig),
BindAddress: b.GetString(incomingWebhookConfig),
})
default:
b.Log.Info("Connecting using webhookbindaddress (receiving)")
b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{
InsecureSkipVerify: b.GetBool(skipTLSConfig),
BindAddress: b.GetString(incomingWebhookConfig),
})
}
go b.handleSlack()
return nil
}
if b.GetString(outgoingWebhookConfig) != "" {
b.Log.Info("Connecting using webhookurl (sending)")
b.mh = matterhook.New(b.GetString(outgoingWebhookConfig), matterhook.Config{
InsecureSkipVerify: b.GetBool(skipTLSConfig),
DisableServer: true,
})
if b.GetString(tokenConfig) != "" {
b.Log.Info("Connecting using token (receiving)")
b.sc = slack.New(b.GetString(tokenConfig), slack.OptionDebug(b.GetBool("debug")))
b.channels = newChannelManager(b.Log, b.sc)
b.users = newUserManager(b.Log, b.sc)
b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection()
go b.handleSlack()
}
} else if b.GetString(tokenConfig) != "" {
b.Log.Info("Connecting using token (sending and receiving)")
b.sc = slack.New(b.GetString(tokenConfig), slack.OptionDebug(b.GetBool("debug")))
b.channels = newChannelManager(b.Log, b.sc)
b.users = newUserManager(b.Log, b.sc)
b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection()
go b.handleSlack()
}
if b.GetString(incomingWebhookConfig) == "" && b.GetString(outgoingWebhookConfig) == "" && b.GetString(tokenConfig) == "" {
return errors.New("no connection method found. See that you have WebhookBindAddress, WebhookURL or Token configured")
}
return nil
}

566
bridge/slack/slack.go Normal file
View File

@ -0,0 +1,566 @@
package bslack
import (
"bytes"
"errors"
"fmt"
"strings"
"sync"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterhook"
lru "github.com/hashicorp/golang-lru"
"github.com/rs/xid"
"github.com/slack-go/slack"
)
type Bslack struct {
sync.RWMutex
*bridge.Config
mh *matterhook.Client
sc *slack.Client
rtm *slack.RTM
si *slack.Info
cache *lru.Cache
uuid string
useChannelID bool
channels *channels
users *users
legacy bool
}
const (
sHello = "hello"
sChannelJoin = "channel_join"
sChannelLeave = "channel_leave"
sChannelJoined = "channel_joined"
sMemberJoined = "member_joined_channel"
sMessageChanged = "message_changed"
sMessageDeleted = "message_deleted"
sSlackAttachment = "slack_attachment"
sPinnedItem = "pinned_item"
sUnpinnedItem = "unpinned_item"
sChannelTopic = "channel_topic"
sChannelPurpose = "channel_purpose"
sFileComment = "file_comment"
sMeMessage = "me_message"
sUserTyping = "user_typing"
sLatencyReport = "latency_report"
sSystemUser = "system"
sSlackBotUser = "slackbot"
cfileDownloadChannel = "file_download_channel"
tokenConfig = "Token"
incomingWebhookConfig = "WebhookBindAddress"
outgoingWebhookConfig = "WebhookURL"
skipTLSConfig = "SkipTLSVerify"
useNickPrefixConfig = "PrefixMessagesWithNick"
editDisableConfig = "EditDisable"
editSuffixConfig = "EditSuffix"
iconURLConfig = "iconurl"
noSendJoinConfig = "nosendjoinpart"
messageLength = 3000
)
func New(cfg *bridge.Config) bridge.Bridger {
// Print a deprecation warning for legacy non-bot tokens (#527).
token := cfg.GetString(tokenConfig)
if token != "" && !strings.HasPrefix(token, "xoxb") {
cfg.Log.Warn("Non-bot token detected. It is STRONGLY recommended to use a proper bot-token instead.")
cfg.Log.Warn("Legacy tokens may be deprecated by Slack at short notice. See the Matterbridge GitHub wiki for a migration guide.")
cfg.Log.Warn("See https://github.com/42wim/matterbridge/wiki/Slack-bot-setup")
return NewLegacy(cfg)
}
return newBridge(cfg)
}
func newBridge(cfg *bridge.Config) *Bslack {
newCache, err := lru.New(5000)
if err != nil {
cfg.Log.Fatalf("Could not create LRU cache for Slack bridge: %v", err)
}
b := &Bslack{
Config: cfg,
uuid: xid.New().String(),
cache: newCache,
}
return b
}
func (b *Bslack) Command(cmd string) string {
return ""
}
func (b *Bslack) Connect() error {
b.RLock()
defer b.RUnlock()
if b.GetString(incomingWebhookConfig) == "" && b.GetString(outgoingWebhookConfig) == "" && b.GetString(tokenConfig) == "" {
return errors.New("no connection method found: WebhookBindAddress, WebhookURL or Token need to be configured")
}
// If we have a token we use the Slack websocket-based RTM for both sending and receiving.
if token := b.GetString(tokenConfig); token != "" {
b.Log.Info("Connecting using token")
b.sc = slack.New(token, slack.OptionDebug(b.GetBool("Debug")))
b.channels = newChannelManager(b.Log, b.sc)
b.users = newUserManager(b.Log, b.sc)
b.rtm = b.sc.NewRTM()
go b.rtm.ManageConnection()
go b.handleSlack()
return nil
}
// In absence of a token we fall back to incoming and outgoing Webhooks.
b.mh = matterhook.New(
"",
matterhook.Config{
InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
DisableServer: true,
},
)
if b.GetString(outgoingWebhookConfig) != "" {
b.Log.Info("Using specified webhook for outgoing messages.")
b.mh.Url = b.GetString(outgoingWebhookConfig)
}
if b.GetString(incomingWebhookConfig) != "" {
b.Log.Info("Setting up local webhook for incoming messages.")
b.mh.BindAddress = b.GetString(incomingWebhookConfig)
b.mh.DisableServer = false
go b.handleSlack()
}
return nil
}
func (b *Bslack) Disconnect() error {
return b.rtm.Disconnect()
}
// JoinChannel only acts as a verification method that checks whether Matterbridge's
// Slack integration is already member of the channel. This is because Slack does not
// allow apps or bots to join channels themselves and they need to be invited
// manually by a user.
func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
// We can only join a channel through the Slack API.
if b.sc == nil {
return nil
}
// try to join a channel when in legacy
if b.legacy {
_, _, _, err := b.sc.JoinConversation(channel.Name)
if err != nil {
switch err.Error() {
case "name_taken", "restricted_action":
case "default":
return err
}
}
}
b.channels.populateChannels(false)
channelInfo, err := b.channels.getChannel(channel.Name)
if err != nil {
return fmt.Errorf("could not join channel: %#v", err)
}
if strings.HasPrefix(channel.Name, "ID:") {
b.useChannelID = true
channel.Name = channelInfo.Name
}
// we can't join a channel unless we are using legacy tokens #651
if !channelInfo.IsMember && !b.legacy {
return fmt.Errorf("slack integration that matterbridge is using is not member of channel '%s', please add it manually", channelInfo.Name)
}
return nil
}
func (b *Bslack) Reload(cfg *bridge.Config) (string, error) {
return "", nil
}
func (b *Bslack) Send(msg config.Message) (string, error) {
// Too noisy to log like other events
if msg.Event != config.EventUserTyping {
b.Log.Debugf("=> Receiving %#v", msg)
}
msg.Text = helper.ClipMessage(msg.Text, messageLength, b.GetString("MessageClipped"))
msg.Text = b.replaceCodeFence(msg.Text)
// Make a action /me of the message
if msg.Event == config.EventUserAction {
msg.Text = "_" + msg.Text + "_"
}
// Use webhook to send the message
if b.GetString(outgoingWebhookConfig) != "" && b.GetString(tokenConfig) == "" {
return "", b.sendWebhook(msg)
}
return b.sendRTM(msg)
}
// sendWebhook uses the configured WebhookURL to send the message
func (b *Bslack) sendWebhook(msg config.Message) error {
// Skip events.
if msg.Event != "" {
return nil
}
if b.GetBool(useNickPrefixConfig) {
msg.Text = msg.Username + msg.Text
}
if msg.Extra != nil {
// This sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE.
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
rmsg := rmsg // scopelint
iconURL := config.GetIconURL(&rmsg, b.GetString(iconURLConfig))
matterMessage := matterhook.OMessage{
IconURL: iconURL,
Channel: msg.Channel,
UserName: rmsg.Username,
Text: rmsg.Text,
}
if err := b.mh.Send(matterMessage); err != nil {
b.Log.Errorf("Failed to send message: %v", err)
}
}
// Webhook doesn't support file uploads, so we add the URL manually.
for _, f := range msg.Extra["file"] {
fi, ok := f.(config.FileInfo)
if !ok {
b.Log.Errorf("Received a file with unexpected content: %#v", f)
continue
}
if fi.URL != "" {
msg.Text += " " + fi.URL
}
}
}
// If we have native slack_attachments add them.
var attachs []slack.Attachment
for _, attach := range msg.Extra[sSlackAttachment] {
attachs = append(attachs, attach.([]slack.Attachment)...)
}
iconURL := config.GetIconURL(&msg, b.GetString(iconURLConfig))
matterMessage := matterhook.OMessage{
IconURL: iconURL,
Attachments: attachs,
Channel: msg.Channel,
UserName: msg.Username,
Text: msg.Text,
}
if msg.Avatar != "" {
matterMessage.IconURL = msg.Avatar
}
if err := b.mh.Send(matterMessage); err != nil {
b.Log.Errorf("Failed to send message via webhook: %#v", err)
return err
}
return nil
}
func (b *Bslack) sendRTM(msg config.Message) (string, error) {
// Handle channelmember messages.
if handled := b.handleGetChannelMembers(&msg); handled {
return "", nil
}
channelInfo, err := b.channels.getChannel(msg.Channel)
if err != nil {
return "", fmt.Errorf("could not send message: %v", err)
}
if msg.Event == config.EventUserTyping {
if b.GetBool("ShowUserTyping") {
b.rtm.SendMessage(b.rtm.NewTypingMessage(channelInfo.ID))
}
return "", nil
}
var handled bool
// Handle topic/purpose updates.
if handled, err = b.handleTopicOrPurpose(&msg, channelInfo); handled {
return "", err
}
// Handle prefix hint for unthreaded messages.
if msg.ParentNotFound() {
msg.ParentID = ""
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
}
// Handle message deletions.
if handled, err = b.deleteMessage(&msg, channelInfo); handled {
return msg.ID, err
}
// Prepend nickname if configured.
if b.GetBool(useNickPrefixConfig) {
msg.Text = msg.Username + msg.Text
}
// Handle message edits.
if handled, err = b.editMessage(&msg, channelInfo); handled {
return msg.ID, err
}
// Upload a file if it exists.
if len(msg.Extra) > 0 {
extraMsgs := helper.HandleExtra(&msg, b.General)
for i := range extraMsgs {
rmsg := &extraMsgs[i]
rmsg.Text = rmsg.Username + rmsg.Text
_, err = b.postMessage(rmsg, channelInfo)
if err != nil {
b.Log.Error(err)
}
}
// Upload files if necessary (from Slack, Telegram or Mattermost).
return b.uploadFile(&msg, channelInfo.ID)
}
// Post message.
return b.postMessage(&msg, channelInfo)
}
func (b *Bslack) updateTopicOrPurpose(msg *config.Message, channelInfo *slack.Channel) error {
var updateFunc func(channelID string, value string) (*slack.Channel, error)
incomingChangeType, text := b.extractTopicOrPurpose(msg.Text)
switch incomingChangeType {
case "topic":
updateFunc = b.rtm.SetTopicOfConversation
case "purpose":
updateFunc = b.rtm.SetPurposeOfConversation
default:
b.Log.Errorf("Unhandled type received from extractTopicOrPurpose: %s", incomingChangeType)
return nil
}
for {
_, err := updateFunc(channelInfo.ID, text)
if err == nil {
return nil
}
if err = handleRateLimit(b.Log, err); err != nil {
return err
}
}
}
// handles updating topic/purpose and determining whether to further propagate update messages.
func (b *Bslack) handleTopicOrPurpose(msg *config.Message, channelInfo *slack.Channel) (bool, error) {
if msg.Event != config.EventTopicChange {
return false, nil
}
if b.GetBool("SyncTopic") {
return true, b.updateTopicOrPurpose(msg, channelInfo)
}
// Pass along to normal message handlers.
if b.GetBool("ShowTopicChange") {
return false, nil
}
// Swallow message as handled no-op.
return true, nil
}
func (b *Bslack) deleteMessage(msg *config.Message, channelInfo *slack.Channel) (bool, error) {
if msg.Event != config.EventMsgDelete {
return false, nil
}
// Some protocols echo deletes, but with an empty ID.
if msg.ID == "" {
return true, nil
}
for {
_, _, err := b.rtm.DeleteMessage(channelInfo.ID, msg.ID)
if err == nil {
return true, nil
}
if err = handleRateLimit(b.Log, err); err != nil {
b.Log.Errorf("Failed to delete user message from Slack: %#v", err)
return true, err
}
}
}
func (b *Bslack) editMessage(msg *config.Message, channelInfo *slack.Channel) (bool, error) {
if msg.ID == "" {
return false, nil
}
messageOptions := b.prepareMessageOptions(msg)
for {
_, _, _, err := b.rtm.UpdateMessage(channelInfo.ID, msg.ID, messageOptions...)
if err == nil {
return true, nil
}
if err = handleRateLimit(b.Log, err); err != nil {
b.Log.Errorf("Failed to edit user message on Slack: %#v", err)
return true, err
}
}
}
func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (string, error) {
// don't post empty messages
if msg.Text == "" {
return "", nil
}
messageOptions := b.prepareMessageOptions(msg)
for {
_, id, err := b.rtm.PostMessage(channelInfo.ID, messageOptions...)
if err == nil {
return id, nil
}
if err = handleRateLimit(b.Log, err); err != nil {
b.Log.Errorf("Failed to sent user message to Slack: %#v", err)
return "", err
}
}
}
// uploadFile handles native upload of files
func (b *Bslack) uploadFile(msg *config.Message, channelID string) (string, error) {
var messageID string
for _, f := range msg.Extra["file"] {
fi, ok := f.(config.FileInfo)
if !ok {
b.Log.Errorf("Received a file with unexpected content: %#v", f)
continue
}
if msg.Text == fi.Comment {
msg.Text = ""
}
// Because the result of the UploadFile is slower than the MessageEvent from slack
// we can't match on the file ID yet, so we have to match on the filename too.
ts := time.Now()
b.Log.Debugf("Adding file %s to cache at %s with timestamp", fi.Name, ts.String())
b.cache.Add("filename"+fi.Name, ts)
initialComment := fmt.Sprintf("File from %s", msg.Username)
if fi.Comment != "" {
initialComment += fmt.Sprintf(" with comment: %s", fi.Comment)
}
res, err := b.sc.UploadFile(slack.FileUploadParameters{
Reader: bytes.NewReader(*fi.Data),
Filename: fi.Name,
Channels: []string{channelID},
InitialComment: initialComment,
ThreadTimestamp: msg.ParentID,
})
if err != nil {
b.Log.Errorf("uploadfile %#v", err)
return "", err
}
if res.ID != "" {
b.Log.Debugf("Adding file ID %s to cache with timestamp %s", res.ID, ts.String())
b.cache.Add("file"+res.ID, ts)
// search for message id by uploaded file in private/public channels, get thread timestamp from uploaded file
if v, ok := res.Shares.Private[channelID]; ok && len(v) > 0 {
messageID = v[0].Ts
}
if v, ok := res.Shares.Public[channelID]; ok && len(v) > 0 {
messageID = v[0].Ts
}
}
}
return messageID, nil
}
func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption {
params := slack.NewPostMessageParameters()
if b.GetBool(useNickPrefixConfig) {
params.AsUser = true
}
params.Username = msg.Username
params.LinkNames = 1 // replace mentions
params.IconURL = config.GetIconURL(msg, b.GetString(iconURLConfig))
params.ThreadTimestamp = msg.ParentID
if msg.Avatar != "" {
params.IconURL = msg.Avatar
}
var attachments []slack.Attachment
// add file attachments
attachments = append(attachments, b.createAttach(msg.Extra)...)
// add slack attachments (from another slack bridge)
if msg.Extra != nil {
for _, attach := range msg.Extra[sSlackAttachment] {
attachments = append(attachments, attach.([]slack.Attachment)...)
}
}
var opts []slack.MsgOption
opts = append(opts,
// provide regular text field (fallback used in Slack notifications, etc.)
slack.MsgOptionText(msg.Text, false),
// add a callback ID so we can see we created it
slack.MsgOptionBlocks(slack.NewSectionBlock(
slack.NewTextBlockObject(slack.MarkdownType, msg.Text, false, false),
nil, nil,
slack.SectionBlockOptionBlockID("matterbridge_"+b.uuid),
)),
slack.MsgOptionEnableLinkUnfurl(),
)
opts = append(opts, slack.MsgOptionAttachments(attachments...))
opts = append(opts, slack.MsgOptionPostMessageParameters(params))
return opts
}
func (b *Bslack) createAttach(extra map[string][]interface{}) []slack.Attachment {
var attachements []slack.Attachment
for _, v := range extra["attachments"] {
entry := v.(map[string]interface{})
s := slack.Attachment{
Fallback: extractStringField(entry, "fallback"),
Color: extractStringField(entry, "color"),
Pretext: extractStringField(entry, "pretext"),
AuthorName: extractStringField(entry, "author_name"),
AuthorLink: extractStringField(entry, "author_link"),
AuthorIcon: extractStringField(entry, "author_icon"),
Title: extractStringField(entry, "title"),
TitleLink: extractStringField(entry, "title_link"),
Text: extractStringField(entry, "text"),
ImageURL: extractStringField(entry, "image_url"),
ThumbURL: extractStringField(entry, "thumb_url"),
Footer: extractStringField(entry, "footer"),
FooterIcon: extractStringField(entry, "footer_icon"),
}
attachements = append(attachements, s)
}
return attachements
}
func extractStringField(data map[string]interface{}, field string) string {
if rawValue, found := data[field]; found {
if value, ok := rawValue.(string); ok {
return value
}
}
return ""
}

View File

@ -0,0 +1,343 @@
package bslack
import (
"context"
"fmt"
"strings"
"sync"
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/sirupsen/logrus"
"github.com/slack-go/slack"
)
const minimumRefreshInterval = 10 * time.Second
type users struct {
log *logrus.Entry
sc *slack.Client
users map[string]*slack.User
usersMutex sync.RWMutex
usersSyncPoints map[string]chan struct{}
refreshInProgress bool
earliestRefresh time.Time
refreshMutex sync.Mutex
}
func newUserManager(log *logrus.Entry, sc *slack.Client) *users {
return &users{
log: log,
sc: sc,
users: make(map[string]*slack.User),
usersSyncPoints: make(map[string]chan struct{}),
earliestRefresh: time.Now(),
}
}
func (b *users) getUser(id string) *slack.User {
b.usersMutex.RLock()
user, ok := b.users[id]
b.usersMutex.RUnlock()
if ok {
return user
}
b.populateUser(id)
b.usersMutex.RLock()
defer b.usersMutex.RUnlock()
return b.users[id]
}
func (b *users) getUsername(id string) string {
if user := b.getUser(id); user != nil {
if user.Profile.DisplayName != "" {
return user.Profile.DisplayName
}
return user.Name
}
b.log.Warnf("Could not find user with ID '%s'", id)
return ""
}
func (b *users) getAvatar(id string) string {
if user := b.getUser(id); user != nil {
return user.Profile.Image48
}
return ""
}
func (b *users) populateUser(userID string) {
for {
b.usersMutex.Lock()
_, exists := b.users[userID]
if exists {
// already in cache
b.usersMutex.Unlock()
return
}
if syncPoint, ok := b.usersSyncPoints[userID]; ok {
// Another goroutine is already populating this user for us so wait on it to finish.
b.usersMutex.Unlock()
<-syncPoint
// We do not return and iterate again to check that the entry does indeed exist
// in case the previous query failed for some reason.
} else {
b.usersSyncPoints[userID] = make(chan struct{})
defer func() {
// Wake up any waiting goroutines and remove the synchronization point.
close(b.usersSyncPoints[userID])
delete(b.usersSyncPoints, userID)
}()
break
}
}
// Do not hold the lock while fetching information from Slack
// as this might take an unbounded amount of time.
b.usersMutex.Unlock()
user, err := b.sc.GetUserInfo(userID)
if err != nil {
b.log.Debugf("GetUserInfo failed for %v: %v", userID, err)
return
}
b.usersMutex.Lock()
defer b.usersMutex.Unlock()
// Register user information.
b.users[userID] = user
}
func (b *users) invalidateUser(userID string) {
b.usersMutex.Lock()
defer b.usersMutex.Unlock()
delete(b.users, userID)
}
func (b *users) populateUsers(wait bool) {
b.refreshMutex.Lock()
if !wait && (time.Now().Before(b.earliestRefresh) || b.refreshInProgress) {
b.log.Debugf("Not refreshing user list as it was done less than %v ago.", minimumRefreshInterval)
b.refreshMutex.Unlock()
return
}
for b.refreshInProgress {
b.refreshMutex.Unlock()
time.Sleep(time.Second)
b.refreshMutex.Lock()
}
b.refreshInProgress = true
b.refreshMutex.Unlock()
newUsers := map[string]*slack.User{}
pagination := b.sc.GetUsersPaginated(slack.GetUsersOptionLimit(200))
count := 0
for {
var err error
pagination, err = pagination.Next(context.Background())
time.Sleep(time.Second)
if err != nil {
if pagination.Done(err) {
break
}
if err = handleRateLimit(b.log, err); err != nil {
b.log.Errorf("Could not retrieve users: %#v", err)
return
}
continue
}
for i := range pagination.Users {
newUsers[pagination.Users[i].ID] = &pagination.Users[i]
}
b.log.Debugf("getting %d users", len(pagination.Users))
count++
// more > 2000 users, slack will complain and ratelimit. break
if count > 10 {
b.log.Info("Large slack detected > 2000 users, skipping loading complete userlist.")
break
}
}
b.usersMutex.Lock()
defer b.usersMutex.Unlock()
b.users = newUsers
b.refreshMutex.Lock()
defer b.refreshMutex.Unlock()
b.earliestRefresh = time.Now().Add(minimumRefreshInterval)
b.refreshInProgress = false
}
type channels struct {
log *logrus.Entry
sc *slack.Client
channelsByID map[string]*slack.Channel
channelsByName map[string]*slack.Channel
channelsMutex sync.RWMutex
channelMembers map[string][]string
channelMembersMutex sync.RWMutex
refreshInProgress bool
earliestRefresh time.Time
refreshMutex sync.Mutex
}
func newChannelManager(log *logrus.Entry, sc *slack.Client) *channels {
return &channels{
log: log,
sc: sc,
channelsByID: make(map[string]*slack.Channel),
channelsByName: make(map[string]*slack.Channel),
earliestRefresh: time.Now(),
}
}
func (b *channels) getChannel(channel string) (*slack.Channel, error) {
if strings.HasPrefix(channel, "ID:") {
return b.getChannelByID(strings.TrimPrefix(channel, "ID:"))
}
return b.getChannelByName(channel)
}
func (b *channels) getChannelByName(name string) (*slack.Channel, error) {
return b.getChannelBy(name, b.channelsByName)
}
func (b *channels) getChannelByID(id string) (*slack.Channel, error) {
return b.getChannelBy(id, b.channelsByID)
}
func (b *channels) getChannelBy(lookupKey string, lookupMap map[string]*slack.Channel) (*slack.Channel, error) {
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
if channel, ok := lookupMap[lookupKey]; ok {
return channel, nil
}
return nil, fmt.Errorf("channel %s not found", lookupKey)
}
func (b *channels) getChannelMembers(users *users) config.ChannelMembers {
b.channelMembersMutex.RLock()
defer b.channelMembersMutex.RUnlock()
membersInfo := config.ChannelMembers{}
for channelID, members := range b.channelMembers {
for _, member := range members {
channelName := ""
userName := ""
userNick := ""
user := users.getUser(member)
if user != nil {
userName = user.Name
userNick = user.Profile.DisplayName
}
channel, _ := b.getChannelByID(channelID)
if channel != nil {
channelName = channel.Name
}
memberInfo := config.ChannelMember{
Username: userName,
Nick: userNick,
UserID: member,
ChannelID: channelID,
ChannelName: channelName,
}
membersInfo = append(membersInfo, memberInfo)
}
}
return membersInfo
}
func (b *channels) registerChannel(channel slack.Channel) {
b.channelsMutex.Lock()
defer b.channelsMutex.Unlock()
b.channelsByID[channel.ID] = &channel
b.channelsByName[channel.Name] = &channel
}
func (b *channels) populateChannels(wait bool) {
b.refreshMutex.Lock()
if !wait && (time.Now().Before(b.earliestRefresh) || b.refreshInProgress) {
b.log.Debugf("Not refreshing channel list as it was done less than %v seconds ago.", minimumRefreshInterval)
b.refreshMutex.Unlock()
return
}
for b.refreshInProgress {
b.refreshMutex.Unlock()
time.Sleep(time.Second)
b.refreshMutex.Lock()
}
b.refreshInProgress = true
b.refreshMutex.Unlock()
newChannelsByID := map[string]*slack.Channel{}
newChannelsByName := map[string]*slack.Channel{}
newChannelMembers := make(map[string][]string)
// We only retrieve public and private channels, not IMs
// and MPIMs as those do not have a channel name.
queryParams := &slack.GetConversationsParameters{
ExcludeArchived: true,
Types: []string{"public_channel,private_channel"},
Limit: 1000,
}
for {
channels, nextCursor, err := b.sc.GetConversations(queryParams)
if err != nil {
if err = handleRateLimit(b.log, err); err != nil {
b.log.Errorf("Could not retrieve channels: %#v", err)
return
}
continue
}
for i := range channels {
newChannelsByID[channels[i].ID] = &channels[i]
newChannelsByName[channels[i].Name] = &channels[i]
// also find all the members in every channel
// comment for now, issues on big slacks
/*
members, err := b.getUsersInConversation(channels[i].ID)
if err != nil {
if err = b.handleRateLimit(err); err != nil {
b.Log.Errorf("Could not retrieve channel members: %#v", err)
return
}
continue
}
newChannelMembers[channels[i].ID] = members
*/
}
if nextCursor == "" {
break
}
queryParams.Cursor = nextCursor
}
b.channelsMutex.Lock()
defer b.channelsMutex.Unlock()
b.channelsByID = newChannelsByID
b.channelsByName = newChannelsByName
b.channelMembersMutex.Lock()
defer b.channelMembersMutex.Unlock()
b.channelMembers = newChannelMembers
b.refreshMutex.Lock()
defer b.refreshMutex.Unlock()
b.earliestRefresh = time.Now().Add(minimumRefreshInterval)
b.refreshInProgress = false
}

169
bridge/sshchat/sshchat.go Normal file
View File

@ -0,0 +1,169 @@
package bsshchat
import (
"bufio"
"io"
"strings"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/shazow/ssh-chat/sshd"
)
type Bsshchat struct {
r *bufio.Scanner
w io.WriteCloser
*bridge.Config
}
func New(cfg *bridge.Config) bridge.Bridger {
return &Bsshchat{Config: cfg}
}
func (b *Bsshchat) Connect() error {
b.Log.Infof("Connecting %s", b.GetString("Server"))
// connHandler will be called by 'sshd.ConnectShell()' below
// once the connection is established in order to handle it.
connErr := make(chan error, 1) // Needs to be buffered.
connSignal := make(chan struct{})
connHandler := func(r io.Reader, w io.WriteCloser) error {
b.r = bufio.NewScanner(r)
b.r.Scan()
b.w = w
if _, err := b.w.Write([]byte("/theme mono\r\n/quiet\r\n")); err != nil {
return err
}
close(connSignal) // Connection is established so we can signal the success.
return b.handleSSHChat()
}
go func() {
// As a successful connection will result in this returning after the Connection
// method has already returned point we NEED to have a buffered channel to still
// be able to write.
connErr <- sshd.ConnectShell(b.GetString("Server"), b.GetString("Nick"), connHandler)
}()
select {
case err := <-connErr:
b.Log.Error("Connection failed")
return err
case <-connSignal:
}
b.Log.Info("Connection succeeded")
return nil
}
func (b *Bsshchat) Disconnect() error {
return nil
}
func (b *Bsshchat) JoinChannel(channel config.ChannelInfo) error {
return nil
}
func (b *Bsshchat) Send(msg config.Message) (string, error) {
// ignore delete messages
if msg.Event == config.EventMsgDelete {
return "", nil
}
b.Log.Debugf("=> Receiving %#v", msg)
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
if _, err := b.w.Write([]byte(rmsg.Username + rmsg.Text + "\r\n")); err != nil {
b.Log.Errorf("Could not send extra message: %#v", err)
}
}
if len(msg.Extra["file"]) > 0 {
return b.handleUploadFile(&msg)
}
}
_, err := b.w.Write([]byte(msg.Username + msg.Text + "\r\n"))
return "", err
}
/*
func (b *Bsshchat) sshchatKeepAlive() chan bool {
done := make(chan bool)
go func() {
ticker := time.NewTicker(90 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
b.Log.Debugf("PING")
err := b.xc.PingC2S("", "")
if err != nil {
b.Log.Debugf("PING failed %#v", err)
}
case <-done:
return
}
}
}()
return done
}
*/
func stripPrompt(s string) string {
pos := strings.LastIndex(s, "\033[K")
if pos < 0 {
return s
}
return s[pos+3:]
}
func (b *Bsshchat) handleSSHChat() error {
/*
done := b.sshchatKeepAlive()
defer close(done)
*/
wait := true
for {
if b.r.Scan() {
// ignore messages from ourselves
if !strings.Contains(b.r.Text(), "\033[K") {
continue
}
if strings.Contains(b.r.Text(), "Rate limiting is in effect") {
continue
}
// skip our own messages
if !strings.HasPrefix(b.r.Text(), "["+b.GetString("Nick")+"] \x1b") {
continue
}
res := strings.Split(stripPrompt(b.r.Text()), ":")
if res[0] == "-> Set theme" {
wait = false
b.Log.Debugf("mono found, allowing")
continue
}
if !wait {
b.Log.Debugf("<= Message %#v", res)
rmsg := config.Message{Username: res[0], Text: strings.TrimSpace(strings.Join(res[1:], ":")), Channel: "sshchat", Account: b.Account, UserID: "nick"}
b.Remote <- rmsg
}
}
}
}
func (b *Bsshchat) handleUploadFile(msg *config.Message) (string, error) {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.Comment != "" {
msg.Text += fi.Comment + ": "
}
if fi.URL != "" {
msg.Text = fi.URL
if fi.Comment != "" {
msg.Text = fi.Comment + ": " + fi.URL
}
}
if _, err := b.w.Write([]byte(msg.Username + msg.Text + "\r\n")); err != nil {
b.Log.Errorf("Could not send file message: %#v", err)
}
}
return "", nil
}

126
bridge/steam/handlers.go Normal file
View File

@ -0,0 +1,126 @@
package bsteam
import (
"fmt"
"strconv"
"github.com/42wim/matterbridge/bridge/config"
"github.com/Philipp15b/go-steam"
"github.com/Philipp15b/go-steam/protocol/steamlang"
)
func (b *Bsteam) handleChatMsg(e *steam.ChatMsgEvent) {
b.Log.Debugf("Receiving ChatMsgEvent: %#v", e)
b.Log.Debugf("<= Sending message from %s on %s to gateway", b.getNick(e.ChatterId), b.Account)
var channel int64
if e.ChatRoomId == 0 {
channel = int64(e.ChatterId)
} else {
// for some reason we have to remove 0x18000000000000
// TODO
// https://github.com/42wim/matterbridge/pull/630#discussion_r238102751
// channel = int64(e.ChatRoomId) & 0xfffffffffffff
channel = int64(e.ChatRoomId) - 0x18000000000000
}
msg := config.Message{
Username: b.getNick(e.ChatterId),
Text: e.Message,
Channel: strconv.FormatInt(channel, 10),
Account: b.Account,
UserID: strconv.FormatInt(int64(e.ChatterId), 10),
}
b.Remote <- msg
}
func (b *Bsteam) handleEvents() {
myLoginInfo := &steam.LogOnDetails{
Username: b.GetString("Login"),
Password: b.GetString("Password"),
AuthCode: b.GetString("AuthCode"),
}
// TODO Attempt to read existing auth hash to avoid steam guard.
// Maybe works
//myLoginInfo.SentryFileHash, _ = ioutil.ReadFile("sentry")
for event := range b.c.Events() {
switch e := event.(type) {
case *steam.ChatMsgEvent:
b.handleChatMsg(e)
case *steam.PersonaStateEvent:
b.Log.Debugf("PersonaStateEvent: %#v\n", e)
b.Lock()
b.userMap[e.FriendId] = e.Name
b.Unlock()
case *steam.ConnectedEvent:
b.c.Auth.LogOn(myLoginInfo)
case *steam.MachineAuthUpdateEvent:
// TODO sentry files for 2 auth
/*
b.Log.Info("authupdate", e)
b.Log.Info("hash", e.Hash)
ioutil.WriteFile("sentry", e.Hash, 0666)
*/
case *steam.LogOnFailedEvent:
b.Log.Info("Logon failed", e)
err := b.handleLogOnFailed(e, myLoginInfo)
if err != nil {
b.Log.Error(err)
return
}
case *steam.LoggedOnEvent:
b.Log.Debugf("LoggedOnEvent: %#v", e)
b.connected <- struct{}{}
b.Log.Debugf("setting online")
b.c.Social.SetPersonaState(steamlang.EPersonaState_Online)
case *steam.DisconnectedEvent:
b.Log.Info("Disconnected")
b.Log.Info("Attempting to reconnect...")
b.c.Connect()
case steam.FatalErrorEvent:
b.Log.Errorf("steam FatalErrorEvent: %#v", e)
default:
b.Log.Debugf("unknown event %#v", e)
}
}
}
func (b *Bsteam) handleLogOnFailed(e *steam.LogOnFailedEvent, myLoginInfo *steam.LogOnDetails) error {
switch e.Result {
case steamlang.EResult_AccountLoginDeniedNeedTwoFactor:
b.Log.Info("Steam guard isn't letting me in! Enter 2FA code:")
var code string
fmt.Scanf("%s", &code)
// TODO https://github.com/42wim/matterbridge/pull/630#discussion_r238103978
myLoginInfo.TwoFactorCode = code
case steamlang.EResult_AccountLogonDenied:
b.Log.Info("Steam guard isn't letting me in! Enter auth code:")
var code string
fmt.Scanf("%s", &code)
// TODO https://github.com/42wim/matterbridge/pull/630#discussion_r238103978
myLoginInfo.AuthCode = code
case steamlang.EResult_InvalidLoginAuthCode:
return fmt.Errorf("Steam guard: invalid login auth code: %#v ", e.Result)
default:
return fmt.Errorf("LogOnFailedEvent: %#v ", e.Result)
// TODO: Handle EResult_InvalidLoginAuthCode
}
return nil
}
// handleFileInfo handles config.FileInfo and adds correct file comment or URL to msg.Text.
// Returns error if cast fails.
func (b *Bsteam) handleFileInfo(msg *config.Message, f interface{}) error {
if _, ok := f.(config.FileInfo); !ok {
return fmt.Errorf("handleFileInfo cast failed %#v", f)
}
fi := f.(config.FileInfo)
if fi.Comment != "" {
msg.Text += fi.Comment + ": "
}
if fi.URL != "" {
msg.Text = fi.URL
if fi.Comment != "" {
msg.Text = fi.Comment + ": " + fi.URL
}
}
return nil
}

95
bridge/steam/steam.go Normal file
View File

@ -0,0 +1,95 @@
package bsteam
import (
"fmt"
"sync"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/Philipp15b/go-steam"
"github.com/Philipp15b/go-steam/protocol/steamlang"
"github.com/Philipp15b/go-steam/steamid"
)
type Bsteam struct {
c *steam.Client
connected chan struct{}
userMap map[steamid.SteamId]string
sync.RWMutex
*bridge.Config
}
func New(cfg *bridge.Config) bridge.Bridger {
b := &Bsteam{Config: cfg}
b.userMap = make(map[steamid.SteamId]string)
b.connected = make(chan struct{})
return b
}
func (b *Bsteam) Connect() error {
b.Log.Info("Connecting")
b.c = steam.NewClient()
go b.handleEvents()
go b.c.Connect()
select {
case <-b.connected:
b.Log.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) (string, error) {
// ignore delete messages
if msg.Event == config.EventMsgDelete {
return "", nil
}
id, err := steamid.NewId(msg.Channel)
if err != nil {
return "", err
}
// Handle files
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, rmsg.Username+rmsg.Text)
}
for i := range msg.Extra["file"] {
if err := b.handleFileInfo(&msg, msg.Extra["file"][i]); err != nil {
b.Log.Error(err)
}
b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text)
}
return "", nil
}
b.c.Social.SendMessage(id, steamlang.EChatEntryType_ChatMsg, msg.Username+msg.Text)
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"
}

646
bridge/telegram/handlers.go Normal file
View File

@ -0,0 +1,646 @@
package btelegram
import (
"fmt"
"html"
"path/filepath"
"strconv"
"strings"
"unicode/utf16"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/davecgh/go-spew/spew"
tgbotapi "github.com/matterbridge/telegram-bot-api/v6"
)
func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message {
// handle channels
if posted != nil {
if posted.Text == "/chatId" {
chatID := strconv.FormatInt(posted.Chat.ID, 10)
// Handle chat topics
if posted.IsTopicMessage {
chatID = chatID + "/" + strconv.Itoa(posted.MessageThreadID)
}
_, err := b.Send(config.Message{
Channel: chatID,
Text: fmt.Sprintf("ID of this chat: %s", chatID),
})
if err != nil {
b.Log.Warnf("Unable to send chatID to %s", chatID)
}
} else {
message = posted
rmsg.Text = message.Text
}
}
// edited channel message
if edited != nil && !b.GetBool("EditDisable") {
message = edited
rmsg.Text = rmsg.Text + message.Text + b.GetString("EditSuffix")
}
return message
}
// handleChannels checks if it's a channel message and if the message is a new or edited messages
func (b *Btelegram) handleChannels(rmsg *config.Message, message *tgbotapi.Message, update tgbotapi.Update) *tgbotapi.Message {
return b.handleUpdate(rmsg, message, update.ChannelPost, update.EditedChannelPost)
}
// handleGroups checks if it's a group message and if the message is a new or edited messages
func (b *Btelegram) handleGroups(rmsg *config.Message, message *tgbotapi.Message, update tgbotapi.Update) *tgbotapi.Message {
return b.handleUpdate(rmsg, message, update.Message, update.EditedMessage)
}
// handleForwarded handles forwarded messages
func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Message) {
if message.ForwardDate == 0 {
return
}
if message.ForwardFromChat != nil && message.ForwardFrom == nil {
rmsg.Text = "Forwarded from " + message.ForwardFromChat.Title + ": " + rmsg.Text
return
}
if message.ForwardFrom == nil {
rmsg.Text = "Forwarded from " + unknownUser + ": " + rmsg.Text
return
}
usernameForward := ""
if b.GetBool("UseFirstName") {
usernameForward = message.ForwardFrom.FirstName
}
if b.GetBool("UseFullName") {
usernameForward = message.ForwardFrom.FirstName + " " + message.ForwardFrom.LastName
}
if usernameForward == "" {
usernameForward = message.ForwardFrom.UserName
if usernameForward == "" {
usernameForward = message.ForwardFrom.FirstName
}
}
if usernameForward == "" {
usernameForward = unknownUser
}
rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text
}
// handleQuoting handles quoting of previous messages
func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Message) {
// Used to check if the message was a reply to the root topic
if message.ReplyToMessage != nil && (!message.IsTopicMessage || message.ReplyToMessage.MessageID != message.MessageThreadID) { //nolint:nestif
usernameReply := ""
if message.ReplyToMessage.From != nil {
if b.GetBool("UseFirstName") {
usernameReply = message.ReplyToMessage.From.FirstName
}
if b.GetBool("UseFullName") {
usernameReply = message.ReplyToMessage.From.FirstName + " " + message.ReplyToMessage.From.LastName
}
if usernameReply == "" {
usernameReply = message.ReplyToMessage.From.UserName
if usernameReply == "" {
usernameReply = message.ReplyToMessage.From.FirstName
}
}
}
if usernameReply == "" {
usernameReply = unknownUser
}
if !b.GetBool("QuoteDisable") {
quote := message.ReplyToMessage.Text
if quote == "" {
quote = message.ReplyToMessage.Caption
}
rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, quote)
}
}
}
// handleUsername handles the correct setting of the username
func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) {
if message.From != nil {
rmsg.UserID = strconv.FormatInt(message.From.ID, 10)
if b.GetBool("UseFirstName") {
rmsg.Username = message.From.FirstName
}
if b.GetBool("UseFullName") {
if message.From.FirstName != "" && message.From.LastName != "" {
rmsg.Username = message.From.FirstName + " " + message.From.LastName
}
}
if rmsg.Username == "" {
rmsg.Username = message.From.UserName
if rmsg.Username == "" {
rmsg.Username = message.From.FirstName
}
}
// only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" || (b.General.MediaServerDownload != "" && b.General.MediaDownloadPath != "") {
b.handleDownloadAvatar(message.From.ID, rmsg.Channel)
}
}
if message.SenderChat != nil { //nolint:nestif
rmsg.UserID = strconv.FormatInt(message.SenderChat.ID, 10)
if b.GetBool("UseFirstName") {
rmsg.Username = message.SenderChat.FirstName
}
if b.GetBool("UseFullName") {
if message.SenderChat.FirstName != "" && message.SenderChat.LastName != "" {
rmsg.Username = message.SenderChat.FirstName + " " + message.SenderChat.LastName
}
}
if rmsg.Username == "" || rmsg.Username == "Channel_Bot" {
rmsg.Username = message.SenderChat.UserName
if rmsg.Username == "" || rmsg.Username == "Channel_Bot" {
rmsg.Username = message.SenderChat.FirstName
}
}
// only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" || (b.General.MediaServerDownload != "" && b.General.MediaDownloadPath != "") {
b.handleDownloadAvatar(message.SenderChat.ID, rmsg.Channel)
}
}
// Fallback on author signature (used in "channel" type of chat)
if rmsg.Username == "" && message.AuthorSignature != "" {
rmsg.Username = message.AuthorSignature
}
// if we really didn't find a username, set it to unknown
if rmsg.Username == "" {
rmsg.Username = unknownUser
}
}
func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
for update := range updates {
b.Log.Debugf("== Receiving event: %#v", update.Message)
if update.Message == nil && update.ChannelPost == nil &&
update.EditedMessage == nil && update.EditedChannelPost == nil {
b.Log.Info("Received event without messages, skipping.")
continue
}
if b.GetInt("debuglevel") == 1 {
spew.Dump(update.Message)
}
b.handleGroupUpdate(update)
var message *tgbotapi.Message
rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})}
// handle channels
message = b.handleChannels(&rmsg, message, update)
// handle groups
message = b.handleGroups(&rmsg, message, update)
if message == nil {
b.Log.Error("message is nil, this shouldn't happen.")
continue
}
// set the ID's from the channel or group message
rmsg.ID = strconv.Itoa(message.MessageID)
rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)
if message.IsTopicMessage {
rmsg.Channel += "/" + strconv.Itoa(message.MessageThreadID)
}
// preserve threading from telegram reply
if message.ReplyToMessage != nil &&
// Used to check if the message was a reply to the root topic
(!message.IsTopicMessage || message.ReplyToMessage.MessageID != message.MessageThreadID) {
rmsg.ParentID = strconv.Itoa(message.ReplyToMessage.MessageID)
}
// handle entities (adding URLs)
b.handleEntities(&rmsg, message)
// handle username
b.handleUsername(&rmsg, message)
// handle any downloads
err := b.handleDownload(&rmsg, message)
if err != nil {
b.Log.Errorf("download failed: %s", err)
}
// handle forwarded messages
b.handleForwarded(&rmsg, message)
// quote the previous message
b.handleQuoting(&rmsg, message)
if rmsg.Text != "" || len(rmsg.Extra) > 0 {
// Comment the next line out due to avoid removing empty lines in Telegram
// rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
// channels don't have (always?) user information. see #410
if message.From != nil {
rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.FormatInt(message.From.ID, 10), b.General)
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
}
}
func (b *Btelegram) handleGroupUpdate(update tgbotapi.Update) {
if msg := update.Message; msg != nil {
switch {
case msg.NewChatMembers != nil:
b.handleUserJoin(update)
case msg.LeftChatMember != nil:
b.handleUserLeave(update)
}
}
}
func (b *Btelegram) handleUserJoin(update tgbotapi.Update) {
msg := update.Message
for _, user := range msg.NewChatMembers {
rmsg := config.Message{
UserID: strconv.FormatInt(user.ID, 10),
Username: user.FirstName, // for some reason all the other name felids are empty on this event (at least for me)
Channel: strconv.FormatInt(msg.Chat.ID, 10),
Account: b.Account,
Protocol: b.Protocol,
Event: config.EventJoinLeave,
Text: "joined chat",
}
b.Remote <- rmsg
}
}
func (b *Btelegram) handleUserLeave(update tgbotapi.Update) {
msg := update.Message
user := msg.LeftChatMember
rmsg := config.Message{
UserID: strconv.FormatInt(user.ID, 10),
Username: user.FirstName, // for some reason all the other name felids are empty on this event (at least for me)
Channel: strconv.FormatInt(msg.Chat.ID, 10),
Account: b.Account,
Protocol: b.Protocol,
Event: config.EventJoinLeave,
Text: "left chat",
}
b.Remote <- rmsg
}
// handleDownloadAvatar downloads the avatar of userid from channel
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
// logs an error message if it fails
func (b *Btelegram) handleDownloadAvatar(userid int64, channel string) {
rmsg := config.Message{
Username: "system",
Text: "avatar",
Channel: channel,
Account: b.Account,
UserID: strconv.FormatInt(userid, 10),
Event: config.EventAvatarDownload,
Extra: make(map[string][]interface{}),
}
if _, ok := b.avatarMap[strconv.FormatInt(userid, 10)]; ok {
return
}
photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1})
if err != nil {
b.Log.Errorf("Userprofile download failed for %#v %s", userid, err)
}
if len(photos.Photos) > 0 {
photo := photos.Photos[0][0]
url := b.getFileDirectURL(photo.FileID)
name := strconv.FormatInt(userid, 10) + ".png"
b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)
err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General)
if err != nil {
b.Log.Error(err)
return
}
data, err := helper.DownloadFile(url)
if err != nil {
b.Log.Errorf("download %s failed %#v", url, err)
return
}
helper.HandleDownloadData(b.Log, &rmsg, name, rmsg.Text, "", data, b.General)
b.Remote <- rmsg
}
}
func (b *Btelegram) maybeConvertTgs(name *string, data *[]byte) {
format := b.GetString("MediaConvertTgs")
if helper.SupportsFormat(format) {
b.Log.Debugf("Format supported by %s, converting %v", helper.LottieBackend(), name)
} else {
// Otherwise, no conversion was requested. Trying to run the usual webp
// converter would fail, because '.tgs.webp' is actually a gzipped JSON
// file, and has nothing to do with WebP.
return
}
err := helper.ConvertTgsToX(data, format, b.Log)
if err != nil {
b.Log.Errorf("conversion failed: %v", err)
} else {
*name = strings.Replace(*name, "tgs.webp", format, 1)
}
}
func (b *Btelegram) maybeConvertWebp(name *string, data *[]byte) {
if b.GetBool("MediaConvertWebPToPNG") {
b.Log.Debugf("WebP to PNG conversion enabled, converting %v", name)
err := helper.ConvertWebPToPNG(data)
if err != nil {
b.Log.Errorf("conversion failed: %v", err)
} else {
*name = strings.Replace(*name, ".webp", ".png", 1)
}
}
}
// handleDownloadFile handles file download
func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error {
size := int64(0)
var url, name, text string
switch {
case message.Sticker != nil:
text, name, url = b.getDownloadInfo(message.Sticker.FileID, ".webp", true)
size = int64(message.Sticker.FileSize)
case message.Voice != nil:
text, name, url = b.getDownloadInfo(message.Voice.FileID, ".ogg", true)
size = message.Voice.FileSize
case message.Video != nil:
text, name, url = b.getDownloadInfo(message.Video.FileID, "", true)
size = message.Video.FileSize
case message.Audio != nil:
text, name, url = b.getDownloadInfo(message.Audio.FileID, "", true)
size = message.Audio.FileSize
case message.Document != nil:
_, _, url = b.getDownloadInfo(message.Document.FileID, "", false)
size = message.Document.FileSize
name = message.Document.FileName
text = " " + message.Document.FileName + " : " + url
case message.Photo != nil:
photos := message.Photo
size = int64(photos[len(photos)-1].FileSize)
text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true)
}
// if name is empty we didn't match a thing to download
if name == "" {
return nil
}
// use the URL instead of native upload
if b.GetBool("UseInsecureURL") {
b.Log.Debugf("Setting message text to :%s", text)
rmsg.Text += text
return nil
}
// if we have a file attached, download it (in memory) and put a pointer to it in msg.Extra
err := helper.HandleDownloadSize(b.Log, rmsg, name, int64(size), b.General)
if err != nil {
return err
}
data, err := helper.DownloadFile(url)
if err != nil {
return err
}
if strings.HasSuffix(name, ".tgs.webp") {
b.maybeConvertTgs(&name, data)
} else if strings.HasSuffix(name, ".webp") {
b.maybeConvertWebp(&name, data)
}
// rename .oga to .ogg https://github.com/42wim/matterbridge/issues/906#issuecomment-741793512
if strings.HasSuffix(name, ".oga") && message.Audio != nil {
name = strings.Replace(name, ".oga", ".ogg", 1)
}
helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General)
return nil
}
func (b *Btelegram) getDownloadInfo(id string, suffix string, urlpart bool) (string, string, string) {
url := b.getFileDirectURL(id)
name := ""
if urlpart {
urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
}
if suffix != "" && !strings.HasSuffix(name, suffix) && !strings.HasSuffix(name, ".webm") {
name += suffix
}
text := " " + url
return text, name, url
}
// handleDelete handles message deleting
func (b *Btelegram) handleDelete(msg *config.Message, chatid int64) (string, error) {
if msg.ID == "" {
return "", nil
}
msgid, err := strconv.Atoi(msg.ID)
if err != nil {
return "", err
}
cfg := tgbotapi.NewDeleteMessage(chatid, msgid)
_, err = b.c.Request(cfg)
return "", err
}
// handleEdit handles message editing.
func (b *Btelegram) handleEdit(msg *config.Message, chatid int64) (string, error) {
msgid, err := strconv.Atoi(msg.ID)
if err != nil {
return "", err
}
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
b.Log.Debug("Using mode HTML - nick only")
msg.Text = html.EscapeString(msg.Text)
}
m := tgbotapi.NewEditMessageText(chatid, msgid, msg.Username+msg.Text)
switch b.GetString("MessageFormat") {
case HTMLFormat:
b.Log.Debug("Using mode HTML")
m.ParseMode = tgbotapi.ModeHTML
case "Markdown":
b.Log.Debug("Using mode markdown")
m.ParseMode = tgbotapi.ModeMarkdown
case MarkdownV2:
b.Log.Debug("Using mode MarkdownV2")
m.ParseMode = MarkdownV2
}
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
b.Log.Debug("Using mode HTML - nick only")
m.ParseMode = tgbotapi.ModeHTML
}
_, err = b.c.Send(m)
if err != nil {
return "", err
}
return "", nil
}
// handleUploadFile handles native upload of files
func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64, threadid int, parentID int) (string, error) {
var media []interface{}
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
file := tgbotapi.FileBytes{
Name: fi.Name,
Bytes: *fi.Data,
}
if b.GetString("MessageFormat") == HTMLFormat {
fi.Comment = makeHTML(html.EscapeString(fi.Comment))
}
switch filepath.Ext(fi.Name) {
case ".jpg", ".jpe", ".png":
pc := tgbotapi.NewInputMediaPhoto(file)
if fi.Comment != "" {
pc.Caption, pc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
}
media = append(media, pc)
case ".mp4", ".m4v":
vc := tgbotapi.NewInputMediaVideo(file)
if fi.Comment != "" {
vc.Caption, vc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
}
media = append(media, vc)
case ".mp3", ".oga":
ac := tgbotapi.NewInputMediaAudio(file)
if fi.Comment != "" {
ac.Caption, ac.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
}
media = append(media, ac)
case ".ogg":
voc := tgbotapi.NewVoice(chatid, file)
voc.Caption, voc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
voc.ReplyToMessageID = parentID
res, err := b.c.Send(voc)
if err != nil {
return "", err
}
return strconv.Itoa(res.MessageID), nil
default:
dc := tgbotapi.NewInputMediaDocument(file)
if fi.Comment != "" {
dc.Caption, dc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
}
media = append(media, dc)
}
}
return b.sendMediaFiles(msg, chatid, threadid, parentID, media)
}
func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string {
format := b.GetString("quoteformat")
if format == "" {
format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
}
quoteMessagelength := len([]rune(quoteMessage))
if b.GetInt("QuoteLengthLimit") != 0 && quoteMessagelength >= b.GetInt("QuoteLengthLimit") {
runes := []rune(quoteMessage)
quoteMessage = string(runes[0:b.GetInt("QuoteLengthLimit")])
if quoteMessagelength > b.GetInt("QuoteLengthLimit") {
quoteMessage += "..."
}
}
format = strings.Replace(format, "{MESSAGE}", message, -1)
format = strings.Replace(format, "{QUOTENICK}", quoteNick, -1)
format = strings.Replace(format, "{QUOTEMESSAGE}", quoteMessage, -1)
return format
}
// handleEntities handles messageEntities
func (b *Btelegram) handleEntities(rmsg *config.Message, message *tgbotapi.Message) {
if message.Entities == nil {
return
}
indexMovedBy := 0
prevLinkOffset := -1
for _, e := range message.Entities {
asRunes := utf16.Encode([]rune(rmsg.Text))
if e.Type == "text_link" {
offset := e.Offset + indexMovedBy
url, err := e.ParseURL()
if err != nil {
b.Log.Errorf("entity text_link url parse failed: %s", err)
continue
}
utfEncodedString := utf16.Encode([]rune(rmsg.Text))
if offset+e.Length > len(utfEncodedString) {
b.Log.Errorf("entity length is too long %d > %d", offset+e.Length, len(utfEncodedString))
continue
}
rmsg.Text = string(utf16.Decode(asRunes[:offset+e.Length])) + " (" + url.String() + ")" + string(utf16.Decode(asRunes[offset+e.Length:]))
indexMovedBy += len(url.String()) + 3
prevLinkOffset = e.Offset
}
if e.Offset == prevLinkOffset {
continue
}
if e.Type == "code" {
offset := e.Offset + indexMovedBy
rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "`" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "`" + string(utf16.Decode(asRunes[offset+e.Length:]))
indexMovedBy += 2
}
if e.Type == "pre" {
offset := e.Offset + indexMovedBy
rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "```\n" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "```\n" + string(utf16.Decode(asRunes[offset+e.Length:]))
indexMovedBy += 8
}
if e.Type == "bold" {
offset := e.Offset + indexMovedBy
rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "*" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "*" + string(utf16.Decode(asRunes[offset+e.Length:]))
indexMovedBy += 2
}
if e.Type == "italic" {
offset := e.Offset + indexMovedBy
rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "_" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "_" + string(utf16.Decode(asRunes[offset+e.Length:]))
indexMovedBy += 2
}
if e.Type == "strike" {
offset := e.Offset + indexMovedBy
rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "~" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "~" + string(utf16.Decode(asRunes[offset+e.Length:]))
indexMovedBy += 2
}
}
}

74
bridge/telegram/html.go Normal file
View File

@ -0,0 +1,74 @@
package btelegram
import (
"bytes"
"github.com/russross/blackfriday"
)
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(string(text))
out.WriteString("</pre>\n")
}
func (options *customHTML) CodeSpan(out *bytes.Buffer, text []byte) {
out.WriteString("<code>")
out.WriteString(string(text))
out.WriteString("</code>")
}
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') //nolint:errcheck
}
func (options *customHTML) BlockQuote(out *bytes.Buffer, text []byte) {
out.WriteString("> ")
out.Write(text)
out.WriteByte('\n')
}
func (options *customHTML) LineBreak(out *bytes.Buffer) {
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))
}

246
bridge/telegram/telegram.go Normal file
View File

@ -0,0 +1,246 @@
package btelegram
import (
"fmt"
"html"
"log"
"strconv"
"strings"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
tgbotapi "github.com/matterbridge/telegram-bot-api/v6"
)
const (
unknownUser = "unknown"
HTMLFormat = "HTML"
HTMLNick = "htmlnick"
MarkdownV2 = "MarkdownV2"
)
type Btelegram struct {
c *tgbotapi.BotAPI
*bridge.Config
avatarMap map[string]string // keep cache of userid and avatar sha
}
func New(cfg *bridge.Config) bridge.Bridger {
tgsConvertFormat := cfg.GetString("MediaConvertTgs")
if tgsConvertFormat != "" {
err := helper.CanConvertTgsToX()
if err != nil {
log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but %s does not appear to work:\n%#v", tgsConvertFormat, helper.LottieBackend(), err)
}
if !helper.SupportsFormat(tgsConvertFormat) {
log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but %s doesn't support it.", tgsConvertFormat, helper.LottieBackend())
}
}
return &Btelegram{Config: cfg, avatarMap: make(map[string]string)}
}
func (b *Btelegram) Connect() error {
var err error
b.Log.Info("Connecting")
b.c, err = tgbotapi.NewBotAPI(b.GetString("Token"))
if err != nil {
b.Log.Debugf("%#v", err)
return err
}
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := b.c.GetUpdatesChan(u)
b.Log.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 TGGetParseMode(b *Btelegram, username string, text string) (textout string, parsemode string) {
textout = username + text
if b.GetString("MessageFormat") == HTMLFormat {
b.Log.Debug("Using mode HTML")
parsemode = tgbotapi.ModeHTML
}
if b.GetString("MessageFormat") == "Markdown" {
b.Log.Debug("Using mode markdown")
parsemode = tgbotapi.ModeMarkdown
}
if b.GetString("MessageFormat") == MarkdownV2 {
b.Log.Debug("Using mode MarkdownV2")
parsemode = MarkdownV2
}
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
b.Log.Debug("Using mode HTML - nick only")
textout = username + html.EscapeString(text)
parsemode = tgbotapi.ModeHTML
}
return textout, parsemode
}
func (b *Btelegram) getIds(channel string) (int64, int, error) {
var chatid int64
topicid := 0
// get the chatid
if strings.Contains(channel, "/") { //nolint:nestif
s := strings.Split(channel, "/")
if len(s) < 2 {
b.Log.Errorf("Invalid channel format: %#v\n", channel)
return 0, 0, nil
}
id, err := strconv.ParseInt(s[0], 10, 64)
if err != nil {
return 0, 0, err
}
chatid = id
tid, err := strconv.Atoi(s[1])
if err != nil {
return 0, 0, err
}
topicid = tid
} else {
id, err := strconv.ParseInt(channel, 10, 64)
if err != nil {
return 0, 0, err
}
chatid = id
}
return chatid, topicid, nil
}
func (b *Btelegram) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg)
chatid, topicid, err := b.getIds(msg.Channel)
if err != nil {
return "", err
}
// map the file SHA to our user (caches the avatar)
if msg.Event == config.EventAvatarDownload {
return b.cacheAvatar(&msg)
}
if b.GetString("MessageFormat") == HTMLFormat {
msg.Text = makeHTML(html.EscapeString(msg.Text))
}
// Delete message
if msg.Event == config.EventMsgDelete {
return b.handleDelete(&msg, chatid)
}
// Handle prefix hint for unthreaded messages.
if msg.ParentNotFound() {
msg.ParentID = ""
msg.Text = fmt.Sprintf("[reply]: %s", msg.Text)
}
var parentID int
if msg.ParentID != "" {
parentID, _ = b.intParentID(msg.ParentID)
}
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
if _, msgErr := b.sendMessage(chatid, topicid, rmsg.Username, rmsg.Text, parentID); msgErr != nil {
b.Log.Errorf("sendMessage failed: %s", msgErr)
}
}
// check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 {
return b.handleUploadFile(&msg, chatid, topicid, parentID)
}
}
// edit the message if we have a msg ID
if msg.ID != "" {
return b.handleEdit(&msg, chatid)
}
// Post normal message
// TODO: recheck it.
// Ignore empty text field needs for prevent double messages from whatsapp to telegram
// when sending media with text caption
if msg.Text != "" {
return b.sendMessage(chatid, topicid, msg.Username, msg.Text, parentID)
}
return "", nil
}
func (b *Btelegram) getFileDirectURL(id string) string {
res, err := b.c.GetFileDirectURL(id)
if err != nil {
return ""
}
return res
}
func (b *Btelegram) sendMessage(chatid int64, topicid int, username, text string, parentID int) (string, error) {
m := tgbotapi.NewMessage(chatid, "")
m.Text, m.ParseMode = TGGetParseMode(b, username, text)
if topicid != 0 {
m.BaseChat.MessageThreadID = topicid
}
m.ReplyToMessageID = parentID
m.DisableWebPagePreview = b.GetBool("DisableWebPagePreview")
res, err := b.c.Send(m)
if err != nil {
return "", err
}
return strconv.Itoa(res.MessageID), nil
}
// sendMediaFiles native upload media files via media group
func (b *Btelegram) sendMediaFiles(msg *config.Message, chatid int64, threadid int, parentID int, media []interface{}) (string, error) {
if len(media) == 0 {
return "", nil
}
mg := tgbotapi.MediaGroupConfig{
BaseChat: tgbotapi.BaseChat{
ChatID: chatid,
MessageThreadID: threadid,
ChannelUsername: msg.Username,
ReplyToMessageID: parentID,
},
Media: media,
}
messages, err := b.c.SendMediaGroup(mg)
if err != nil {
return "", err
}
// return first message id
return strconv.Itoa(messages[0].MessageID), nil
}
// intParentID return integer parent id for telegram message
func (b *Btelegram) intParentID(parentID string) (int, error) {
pid, err := strconv.Atoi(parentID)
if err != nil {
return 0, err
}
return pid, nil
}
func (b *Btelegram) cacheAvatar(msg *config.Message) (string, error) {
fi := msg.Extra["file"][0].(config.FileInfo)
/* if we have a sha we have successfully uploaded the file to the media server,
so we can now cache the sha */
if fi.SHA != "" {
b.Log.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID)
b.avatarMap[msg.UserID] = fi.SHA
}
return "", nil
}

333
bridge/vk/vk.go Normal file
View File

@ -0,0 +1,333 @@
package bvk
import (
"bytes"
"context"
"regexp"
"strconv"
"strings"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/SevereCloud/vksdk/v2/api"
"github.com/SevereCloud/vksdk/v2/events"
longpoll "github.com/SevereCloud/vksdk/v2/longpoll-bot"
"github.com/SevereCloud/vksdk/v2/object"
)
const (
audioMessage = "audio_message"
document = "doc"
photo = "photo"
video = "video"
graffiti = "graffiti"
sticker = "sticker"
wall = "wall"
)
type user struct {
lastname, firstname, avatar string
}
type Bvk struct {
c *api.VK
lp *longpoll.LongPoll
usernamesMap map[int]user // cache of user names and avatar URLs
*bridge.Config
}
func New(cfg *bridge.Config) bridge.Bridger {
return &Bvk{usernamesMap: make(map[int]user), Config: cfg}
}
func (b *Bvk) Connect() error {
b.Log.Info("Connecting")
b.c = api.NewVK(b.GetString("Token"))
var err error
b.lp, err = longpoll.NewLongPollCommunity(b.c)
if err != nil {
b.Log.Debugf("%#v", err)
return err
}
b.lp.MessageNew(func(ctx context.Context, obj events.MessageNewObject) {
b.handleMessage(obj.Message, false)
})
b.Log.Info("Connection succeeded")
go func() {
err := b.lp.Run()
if err != nil {
b.Log.WithError(err).Fatal("Enable longpoll in group management")
}
}()
return nil
}
func (b *Bvk) Disconnect() error {
b.lp.Shutdown()
return nil
}
func (b *Bvk) JoinChannel(channel config.ChannelInfo) error {
return nil
}
func (b *Bvk) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg)
peerID, err := strconv.Atoi(msg.Channel)
if err != nil {
return "", err
}
params := api.Params{}
text := msg.Username + msg.Text
if msg.Extra != nil {
if len(msg.Extra["file"]) > 0 {
// generate attachments string
attachment, urls := b.uploadFiles(msg.Extra, peerID)
params["attachment"] = attachment
text += urls
}
}
params["message"] = text
if msg.ID == "" {
// New message
params["random_id"] = time.Now().Unix()
params["peer_ids"] = msg.Channel
res, e := b.c.MessagesSendPeerIDs(params)
if e != nil {
return "", err
}
return strconv.Itoa(res[0].ConversationMessageID), nil
}
// Edit message
messageID, err := strconv.ParseInt(msg.ID, 10, 64)
if err != nil {
return "", err
}
params["peer_id"] = peerID
params["conversation_message_id"] = messageID
_, err = b.c.MessagesEdit(params)
if err != nil {
return "", err
}
return msg.ID, nil
}
func (b *Bvk) getUser(id int) user {
u, found := b.usernamesMap[id]
if !found {
b.Log.Debug("Fetching username for ", id)
if id >= 0 {
result, _ := b.c.UsersGet(api.Params{
"user_ids": id,
"fields": "photo_200",
})
resUser := result[0]
u = user{lastname: resUser.LastName, firstname: resUser.FirstName, avatar: resUser.Photo200}
b.usernamesMap[id] = u
} else {
result, _ := b.c.GroupsGetByID(api.Params{
"group_id": id * -1,
})
resGroup := result[0]
u = user{lastname: resGroup.Name, avatar: resGroup.Photo200}
}
}
return u
}
func (b *Bvk) handleMessage(msg object.MessagesMessage, isFwd bool) {
b.Log.Debug("ChatID: ", msg.PeerID)
// fetch user info
u := b.getUser(msg.FromID)
rmsg := config.Message{
Text: msg.Text,
Username: u.firstname + " " + u.lastname,
Avatar: u.avatar,
Channel: strconv.Itoa(msg.PeerID),
Account: b.Account,
UserID: strconv.Itoa(msg.FromID),
ID: strconv.Itoa(msg.ConversationMessageID),
Extra: make(map[string][]interface{}),
}
if msg.ReplyMessage != nil {
ur := b.getUser(msg.ReplyMessage.FromID)
rmsg.Text = "Re: " + ur.firstname + " " + ur.lastname + "\n" + rmsg.Text
}
if isFwd {
rmsg.Username = "Fwd: " + rmsg.Username
}
if len(msg.Attachments) > 0 {
urls, text := b.getFiles(msg.Attachments)
if text != "" {
rmsg.Text += "\n" + text
}
// download
b.downloadFiles(&rmsg, urls)
}
if len(msg.FwdMessages) > 0 {
rmsg.Text += strconv.Itoa(len(msg.FwdMessages)) + " forwarded messages"
}
b.Remote <- rmsg
if len(msg.FwdMessages) > 0 {
// recursive processing of forwarded messages
for _, m := range msg.FwdMessages {
m.PeerID = msg.PeerID
b.handleMessage(m, true)
}
}
}
func (b *Bvk) uploadFiles(extra map[string][]interface{}, peerID int) (string, string) {
var attachments []string
text := ""
for _, f := range extra["file"] {
fi := f.(config.FileInfo)
if fi.Comment != "" {
text += fi.Comment + "\n"
}
a, err := b.uploadFile(fi, peerID)
if err != nil {
b.Log.WithError(err).Error("File upload error ", fi.Name)
}
attachments = append(attachments, a)
}
return strings.Join(attachments, ","), text
}
func (b *Bvk) uploadFile(file config.FileInfo, peerID int) (string, error) {
r := bytes.NewReader(*file.Data)
photoRE := regexp.MustCompile(".(jpg|jpe|png)$")
if photoRE.MatchString(file.Name) {
// BUG(VK): for community chat peerID=0
p, err := b.c.UploadMessagesPhoto(0, r)
if err != nil {
return "", err
}
return photo + strconv.Itoa(p[0].OwnerID) + "_" + strconv.Itoa(p[0].ID), nil
}
var doctype string
if strings.Contains(file.Name, ".ogg") {
doctype = audioMessage
} else {
doctype = document
}
doc, err := b.c.UploadMessagesDoc(peerID, doctype, file.Name, "", r)
if err != nil {
return "", err
}
switch doc.Type {
case audioMessage:
return document + strconv.Itoa(doc.AudioMessage.OwnerID) + "_" + strconv.Itoa(doc.AudioMessage.ID), nil
case document:
return document + strconv.Itoa(doc.Doc.OwnerID) + "_" + strconv.Itoa(doc.Doc.ID), nil
}
return "", nil
}
func (b *Bvk) getFiles(attachments []object.MessagesMessageAttachment) ([]string, string) {
var urls []string
var text []string
for _, a := range attachments {
switch a.Type {
case photo:
var resolution float64 = 0
url := a.Photo.Sizes[0].URL
for _, size := range a.Photo.Sizes {
r := size.Height * size.Width
if resolution < r {
resolution = r
url = size.URL
}
}
urls = append(urls, url)
case document:
urls = append(urls, a.Doc.URL)
case graffiti:
urls = append(urls, a.Graffiti.URL)
case audioMessage:
urls = append(urls, a.AudioMessage.DocsDocPreviewAudioMessage.LinkOgg)
case sticker:
var resolution float64 = 0
url := a.Sticker.Images[0].URL
for _, size := range a.Sticker.Images {
r := size.Height * size.Width
if resolution < r {
resolution = r
url = size.URL
}
}
urls = append(urls, url+".png")
case video:
text = append(text, "https://vk.com/video"+strconv.Itoa(a.Video.OwnerID)+"_"+strconv.Itoa(a.Video.ID))
case wall:
text = append(text, "https://vk.com/wall"+strconv.Itoa(a.Wall.FromID)+"_"+strconv.Itoa(a.Wall.ID))
default:
text = append(text, "This attachment is not supported ("+a.Type+")")
}
}
return urls, strings.Join(text, "\n")
}
func (b *Bvk) downloadFiles(rmsg *config.Message, urls []string) {
for _, url := range urls {
data, err := helper.DownloadFile(url)
if err == nil {
urlPart := strings.Split(url, "/")
name := strings.Split(urlPart[len(urlPart)-1], "?")[0]
helper.HandleDownloadData(b.Log, rmsg, name, "", url, data, b.General)
}
}
}

382
bridge/whatsapp/handlers.go Normal file
View File

@ -0,0 +1,382 @@
// nolint:goconst
package bwhatsapp
import (
"fmt"
"mime"
"strings"
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/Rhymen/go-whatsapp"
"github.com/jpillora/backoff"
)
/*
Implement handling messages coming from WhatsApp
Check:
- https://github.com/Rhymen/go-whatsapp#add-message-handlers
- https://github.com/Rhymen/go-whatsapp/blob/master/handler.go
- https://github.com/tulir/mautrix-whatsapp/tree/master/whatsapp-ext for more advanced command handling
*/
// HandleError received from WhatsApp
func (b *Bwhatsapp) HandleError(err error) {
// ignore received invalid data errors. https://github.com/42wim/matterbridge/issues/843
// ignore tag 174 errors. https://github.com/42wim/matterbridge/issues/1094
if strings.Contains(err.Error(), "error processing data: received invalid data") ||
strings.Contains(err.Error(), "invalid string with tag 174") {
return
}
switch err.(type) {
case *whatsapp.ErrConnectionClosed, *whatsapp.ErrConnectionFailed:
b.reconnect(err)
default:
switch err {
case whatsapp.ErrConnectionTimeout:
b.reconnect(err)
default:
b.Log.Errorf("%v", err)
}
}
}
func (b *Bwhatsapp) reconnect(err error) {
bf := &backoff.Backoff{
Min: time.Second,
Max: 5 * time.Minute,
Jitter: true,
}
for {
d := bf.Duration()
b.Log.Errorf("Connection failed, underlying error: %v", err)
b.Log.Infof("Waiting %s...", d)
time.Sleep(d)
b.Log.Info("Reconnecting...")
err := b.conn.Restore()
if err == nil {
bf.Reset()
b.startedAt = uint64(time.Now().Unix())
return
}
}
}
// HandleTextMessage sent from WhatsApp, relay it to the brige
func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
if message.Info.FromMe {
return
}
// whatsapp sends last messages to show context , cut them
if message.Info.Timestamp < b.startedAt {
return
}
groupJID := message.Info.RemoteJid
senderJID := message.Info.SenderJid
if len(senderJID) == 0 {
if message.Info.Source != nil && message.Info.Source.Participant != nil {
senderJID = *message.Info.Source.Participant
}
}
// translate sender's JID to the nicest username we can get
senderName := b.getSenderName(senderJID)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
}
extText := message.Info.Source.Message.ExtendedTextMessage
if extText != nil && extText.ContextInfo != nil && extText.ContextInfo.MentionedJid != nil {
// handle user mentions
for _, mentionedJID := range extText.ContextInfo.MentionedJid {
numberAndSuffix := strings.SplitN(mentionedJID, "@", 2)
// mentions comes as telephone numbers and we don't want to expose it to other bridges
// replace it with something more meaninful to others
mention := b.getSenderNotify(numberAndSuffix[0] + "@s.whatsapp.net")
if mention == "" {
mention = "someone"
}
message.Text = strings.Replace(message.Text, "@"+numberAndSuffix[0], "@"+mention, 1)
}
}
rmsg := config.Message{
UserID: senderJID,
Username: senderName,
Text: message.Text,
Channel: groupJID,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
// ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string
ID: message.Info.Id,
}
if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
// HandleImageMessage sent from WhatsApp, relay it to the brige
// nolint:funlen
func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) {
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
return
}
senderJID := message.Info.SenderJid
if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
senderJID = *message.Info.Source.Participant
}
senderName := b.getSenderName(message.Info.SenderJid)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
}
rmsg := config.Message{
UserID: senderJID,
Username: senderName,
Channel: message.Info.RemoteJid,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: message.Info.Id,
}
if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(message.Type)
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
// rename .jfif to .jpg https://github.com/42wim/matterbridge/issues/1292
if fileExt[0] == ".jfif" {
fileExt[0] = ".jpg"
}
// rename .jpe to .jpg https://github.com/42wim/matterbridge/issues/1463
if fileExt[0] == ".jpe" {
fileExt[0] = ".jpg"
}
filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
b.Log.Debugf("Trying to download %s with type %s", filename, message.Type)
data, err := message.Download()
if err != nil {
b.Log.Errorf("Download image failed: %s", err)
return
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
// HandleVideoMessage downloads video messages
func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) {
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
return
}
senderJID := message.Info.SenderJid
if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
senderJID = *message.Info.Source.Participant
}
senderName := b.getSenderName(message.Info.SenderJid)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
}
rmsg := config.Message{
UserID: senderJID,
Username: senderName,
Channel: message.Info.RemoteJid,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: message.Info.Id,
}
if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(message.Type)
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
if len(fileExt) == 0 {
fileExt = append(fileExt, ".mp4")
}
filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type)
data, err := message.Download()
if err != nil {
b.Log.Errorf("Download video failed: %s", err)
return
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
// HandleAudioMessage downloads audio messages
func (b *Bwhatsapp) HandleAudioMessage(message whatsapp.AudioMessage) {
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
return
}
senderJID := message.Info.SenderJid
if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
senderJID = *message.Info.Source.Participant
}
senderName := b.getSenderName(message.Info.SenderJid)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
}
rmsg := config.Message{
UserID: senderJID,
Username: senderName,
Channel: message.Info.RemoteJid,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: message.Info.Id,
}
if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(message.Type)
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
if len(fileExt) == 0 {
fileExt = append(fileExt, ".ogg")
}
filename := fmt.Sprintf("%v%v", message.Info.Id, fileExt[0])
b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, message.Length, message.Type)
data, err := message.Download()
if err != nil {
b.Log.Errorf("Download audio failed: %s", err)
return
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, "audio message", "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
// HandleDocumentMessage downloads documents
func (b *Bwhatsapp) HandleDocumentMessage(message whatsapp.DocumentMessage) {
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
return
}
senderJID := message.Info.SenderJid
if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
senderJID = *message.Info.Source.Participant
}
senderName := b.getSenderName(message.Info.SenderJid)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
}
rmsg := config.Message{
UserID: senderJID,
Username: senderName,
Channel: message.Info.RemoteJid,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: message.Info.Id,
}
if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(message.Type)
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
filename := fmt.Sprintf("%v", message.FileName)
b.Log.Debugf("Trying to download %s with extension %s and type %s", filename, fileExt, message.Type)
data, err := message.Download()
if err != nil {
b.Log.Errorf("Download document message failed: %s", err)
return
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, "document", "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}

162
bridge/whatsapp/helpers.go Normal file
View File

@ -0,0 +1,162 @@
package bwhatsapp
import (
"encoding/gob"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go"
"github.com/Rhymen/go-whatsapp"
)
type ProfilePicInfo struct {
URL string `json:"eurl"`
Tag string `json:"tag"`
Status int16 `json:"status"`
}
func qrFromTerminal(invert bool) chan string {
qr := make(chan string)
go func() {
terminal := qrcodeTerminal.New()
if invert {
terminal = qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightWhite, qrcodeTerminal.ConsoleColors.BrightBlack, qrcodeTerminal.QRCodeRecoveryLevels.Medium)
}
terminal.Get(<-qr).Print()
}()
return qr
}
func (b *Bwhatsapp) readSession() (whatsapp.Session, error) {
session := whatsapp.Session{}
sessionFile := b.Config.GetString(sessionFile)
if sessionFile == "" {
return session, errors.New("if you won't set SessionFile then you will need to scan QR code on every restart")
}
file, err := os.Open(sessionFile)
if err != nil {
return session, err
}
defer file.Close()
decoder := gob.NewDecoder(file)
return session, decoder.Decode(&session)
}
func (b *Bwhatsapp) writeSession(session whatsapp.Session) error {
sessionFile := b.Config.GetString(sessionFile)
if sessionFile == "" {
// we already sent a warning while starting the bridge, so let's be quiet here
return nil
}
file, err := os.Create(sessionFile)
if err != nil {
return err
}
defer file.Close()
encoder := gob.NewEncoder(file)
return encoder.Encode(session)
}
func (b *Bwhatsapp) restoreSession() (*whatsapp.Session, error) {
session, err := b.readSession()
if err != nil {
b.Log.Warn(err.Error())
}
b.Log.Debugln("Restoring WhatsApp session..")
session, err = b.conn.RestoreWithSession(session)
if err != nil {
// restore session connection timed out (I couldn't get over it without logging in again)
return nil, errors.New("failed to restore session: " + err.Error())
}
b.Log.Debugln("Session restored successfully!")
return &session, nil
}
func (b *Bwhatsapp) getSenderName(senderJid string) string {
if sender, exists := b.users[senderJid]; exists {
if sender.Name != "" {
return sender.Name
}
// if user is not in phone contacts
// it is the most obvious scenario unless you sync your phone contacts with some remote updated source
// users can change it in their WhatsApp settings -> profile -> click on Avatar
if sender.Notify != "" {
return sender.Notify
}
if sender.Short != "" {
return sender.Short
}
}
// try to reload this contact
if _, err := b.conn.Contacts(); err != nil {
b.Log.Errorf("error on update of contacts: %v", err)
}
if contact, exists := b.conn.Store.Contacts[senderJid]; exists {
// Add it to the user map
b.users[senderJid] = contact
if contact.Name != "" {
return contact.Name
}
// if user is not in phone contacts
// same as above
return contact.Notify
}
return ""
}
func (b *Bwhatsapp) getSenderNotify(senderJid string) string {
if sender, exists := b.users[senderJid]; exists {
return sender.Notify
}
return ""
}
func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*ProfilePicInfo, error) {
data, err := b.conn.GetProfilePicThumb(jid)
if err != nil {
return nil, fmt.Errorf("failed to get avatar: %v", err)
}
content := <-data
info := &ProfilePicInfo{}
err = json.Unmarshal([]byte(content), info)
if err != nil {
return info, fmt.Errorf("failed to unmarshal avatar info: %v", err)
}
return info, nil
}
func isGroupJid(identifier string) bool {
return strings.HasSuffix(identifier, "@g.us") ||
strings.HasSuffix(identifier, "@temp") ||
strings.HasSuffix(identifier, "@broadcast")
}

341
bridge/whatsapp/whatsapp.go Normal file
View File

@ -0,0 +1,341 @@
package bwhatsapp
import (
"bytes"
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"mime"
"os"
"path/filepath"
"strings"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/Rhymen/go-whatsapp"
)
const (
// Account config parameters
cfgNumber = "Number"
qrOnWhiteTerminal = "QrOnWhiteTerminal"
sessionFile = "SessionFile"
)
// Bwhatsapp Bridge structure keeping all the information needed for relying
type Bwhatsapp struct {
*bridge.Config
session *whatsapp.Session
conn *whatsapp.Conn
startedAt uint64
users map[string]whatsapp.Contact
userAvatars map[string]string
}
// New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file
func New(cfg *bridge.Config) bridge.Bridger {
number := cfg.GetString(cfgNumber)
cfg.Log.Warn("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
cfg.Log.Warn("This bridge is deprecated and not supported anymore. Use the new multidevice whatsapp bridge")
cfg.Log.Warn("See https://github.com/42wim/matterbridge#building-with-whatsapp-beta-multidevice-support for more info")
cfg.Log.Warn("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
if number == "" {
cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number")
}
b := &Bwhatsapp{
Config: cfg,
users: make(map[string]whatsapp.Contact),
userAvatars: make(map[string]string),
}
return b
}
// Connect to WhatsApp. Required implementation of the Bridger interface
func (b *Bwhatsapp) Connect() error {
number := b.GetString(cfgNumber)
if number == "" {
return errors.New("whatsapp's telephone number need to be configured")
}
b.Log.Debugln("Connecting to WhatsApp..")
conn, err := whatsapp.NewConn(20 * time.Second)
if err != nil {
return errors.New("failed to connect to WhatsApp: " + err.Error())
}
b.conn = conn
b.conn.AddHandler(b)
b.Log.Debugln("WhatsApp connection successful")
// load existing session in order to keep it between restarts
b.session, err = b.restoreSession()
if err != nil {
b.Log.Warn(err.Error())
}
// login to a new session
if b.session == nil {
if err = b.Login(); err != nil {
return err
}
}
b.startedAt = uint64(time.Now().Unix())
_, err = b.conn.Contacts()
if err != nil {
return fmt.Errorf("error on update of contacts: %v", err)
}
// see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013
for len(b.conn.Store.Contacts) == 0 {
b.conn.Contacts() // nolint:errcheck
<-time.After(1 * time.Second)
}
// map all the users
for id, contact := range b.conn.Store.Contacts {
if !isGroupJid(id) && id != "status@broadcast" {
// it is user
b.users[id] = contact
}
}
// get user avatar asynchronously
go func() {
b.Log.Debug("Getting user avatars..")
for jid := range b.users {
info, err := b.GetProfilePicThumb(jid)
if err != nil {
b.Log.Warnf("Could not get profile photo of %s: %v", jid, err)
} else {
b.Lock()
b.userAvatars[jid] = info.URL
b.Unlock()
}
}
b.Log.Debug("Finished getting avatars..")
}()
return nil
}
// Login to WhatsApp creating a new session. This will require to scan a QR code on your mobile device
func (b *Bwhatsapp) Login() error {
b.Log.Debugln("Logging in..")
invert := b.GetBool(qrOnWhiteTerminal) // false is the default
qrChan := qrFromTerminal(invert)
session, err := b.conn.Login(qrChan)
if err != nil {
b.Log.Warnln("Failed to log in:", err)
return err
}
b.session = &session
b.Log.Infof("Logged into session: %#v", session)
b.Log.Infof("Connection: %#v", b.conn)
err = b.writeSession(session)
if err != nil {
fmt.Fprintf(os.Stderr, "error saving session: %v\n", err)
}
return nil
}
// Disconnect is called while reconnecting to the bridge
// Required implementation of the Bridger interface
func (b *Bwhatsapp) Disconnect() error {
// We could Logout, but that would close the session completely and would require a new QR code scan
// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L377-L381
return nil
}
// JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name'
// Required implementation of the Bridger interface
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
byJid := isGroupJid(channel.Name)
// see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013
for len(b.conn.Store.Contacts) == 0 {
b.conn.Contacts() // nolint:errcheck
<-time.After(1 * time.Second)
}
// verify if we are member of the given group
if byJid {
// channel.Name specifies static group jID, not the name
if _, exists := b.conn.Store.Contacts[channel.Name]; !exists {
return fmt.Errorf("account doesn't belong to group with jid %s", channel.Name)
}
return nil
}
// channel.Name specifies group name that might change, warn about it
var jids []string
for id, contact := range b.conn.Store.Contacts {
if isGroupJid(id) && contact.Name == channel.Name {
jids = append(jids, id)
}
}
switch len(jids) {
case 0:
// didn't match any group - print out possibilites
for id, contact := range b.conn.Store.Contacts {
if isGroupJid(id) {
b.Log.Infof("%s %s", contact.Jid, contact.Name)
}
}
return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name)
case 1:
return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", jids[0], channel.Name)
default:
return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, jids)
}
}
// Post a document message from the bridge to WhatsApp
func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (string, error) {
fi := msg.Extra["file"][0].(config.FileInfo)
// Post document message
message := whatsapp.DocumentMessage{
Info: whatsapp.MessageInfo{
RemoteJid: msg.Channel,
},
Title: fi.Name,
FileName: fi.Name,
Type: filetype,
Content: bytes.NewReader(*fi.Data),
}
b.Log.Debugf("=> Sending %#v", msg)
// create message ID
// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented
idBytes := make([]byte, 10)
if _, err := rand.Read(idBytes); err != nil {
b.Log.Warn(err.Error())
}
message.Info.Id = strings.ToUpper(hex.EncodeToString(idBytes))
_, err := b.conn.Send(message)
return message.Info.Id, err
}
// Post an image message from the bridge to WhatsApp
// Handle, for sure image/jpeg, image/png and image/gif MIME types
func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (string, error) {
fi := msg.Extra["file"][0].(config.FileInfo)
// Post image message
message := whatsapp.ImageMessage{
Info: whatsapp.MessageInfo{
RemoteJid: msg.Channel,
},
Type: filetype,
Caption: msg.Username + fi.Comment,
Content: bytes.NewReader(*fi.Data),
}
b.Log.Debugf("=> Sending %#v", msg)
// create message ID
// TODO follow and act if https://github.com/Rhymen/go-whatsapp/issues/101 implemented
idBytes := make([]byte, 10)
if _, err := rand.Read(idBytes); err != nil {
b.Log.Warn(err.Error())
}
message.Info.Id = strings.ToUpper(hex.EncodeToString(idBytes))
_, err := b.conn.Send(message)
return message.Info.Id, err
}
// Send a message from the bridge to WhatsApp
// Required implementation of the Bridger interface
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg)
// Delete message
if msg.Event == config.EventMsgDelete {
if msg.ID == "" {
// No message ID in case action is executed on a message sent before the bridge was started
// and then the bridge cache doesn't have this message ID mapped
return "", nil
}
_, err := b.conn.RevokeMessage(msg.Channel, msg.ID, true)
return "", err
}
// Edit message
if msg.ID != "" {
b.Log.Debugf("updating message with id %s", msg.ID)
if b.GetString("editsuffix") != "" {
msg.Text += b.GetString("EditSuffix")
} else {
msg.Text += " (edited)"
}
}
// Handle Upload a file
if msg.Extra["file"] != nil {
fi := msg.Extra["file"][0].(config.FileInfo)
filetype := mime.TypeByExtension(filepath.Ext(fi.Name))
b.Log.Debugf("Extra file is %#v", filetype)
// TODO: add different types
// TODO: add webp conversion
switch filetype {
case "image/jpeg", "image/png", "image/gif":
return b.PostImageMessage(msg, filetype)
default:
return b.PostDocumentMessage(msg, filetype)
}
}
// Post text message
message := whatsapp.TextMessage{
Info: whatsapp.MessageInfo{
RemoteJid: msg.Channel, // which equals to group id
},
Text: msg.Username + msg.Text,
}
b.Log.Debugf("=> Sending %#v", msg)
return b.conn.Send(message)
}
// TODO do we want that? to allow login with QR code from a bridged channel? https://github.com/tulir/mautrix-whatsapp/blob/513eb18e2d59bada0dd515ee1abaaf38a3bfe3d5/commands.go#L76
//func (b *Bwhatsapp) Command(cmd string) string {
// return ""
//}

View File

@ -0,0 +1,454 @@
//go:build whatsappmulti
// +build whatsappmulti
package bwhatsapp
import (
"fmt"
"mime"
"strings"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
)
// nolint:gocritic
func (b *Bwhatsapp) eventHandler(evt interface{}) {
switch e := evt.(type) {
case *events.Message:
b.handleMessage(e)
case *events.GroupInfo:
b.handleGroupInfo(e)
}
}
func (b *Bwhatsapp) handleGroupInfo(event *events.GroupInfo) {
b.Log.Debugf("Receiving event %#v", event)
switch {
case event.Join != nil:
b.handleUserJoin(event)
case event.Leave != nil:
b.handleUserLeave(event)
case event.Topic != nil:
b.handleTopicChange(event)
}
}
func (b *Bwhatsapp) handleUserJoin(event *events.GroupInfo) {
for _, joinedJid := range event.Join {
senderName := b.getSenderNameFromJID(joinedJid)
rmsg := config.Message{
UserID: joinedJid.String(),
Username: senderName,
Channel: event.JID.String(),
Account: b.Account,
Protocol: b.Protocol,
Event: config.EventJoinLeave,
Text: "joined chat",
}
b.Remote <- rmsg
}
}
func (b *Bwhatsapp) handleUserLeave(event *events.GroupInfo) {
for _, leftJid := range event.Leave {
senderName := b.getSenderNameFromJID(leftJid)
rmsg := config.Message{
UserID: leftJid.String(),
Username: senderName,
Channel: event.JID.String(),
Account: b.Account,
Protocol: b.Protocol,
Event: config.EventJoinLeave,
Text: "left chat",
}
b.Remote <- rmsg
}
}
func (b *Bwhatsapp) handleTopicChange(event *events.GroupInfo) {
msg := event.Topic
senderJid := msg.TopicSetBy
senderName := b.getSenderNameFromJID(senderJid)
text := msg.Topic
if text == "" {
text = "removed topic"
}
rmsg := config.Message{
UserID: senderJid.String(),
Username: senderName,
Channel: event.JID.String(),
Account: b.Account,
Protocol: b.Protocol,
Event: config.EventTopicChange,
Text: "Topic changed: " + text,
}
b.Remote <- rmsg
}
func (b *Bwhatsapp) handleMessage(message *events.Message) {
msg := message.Message
switch {
case msg == nil, message.Info.IsFromMe, message.Info.Timestamp.Before(b.startedAt):
return
}
b.Log.Debugf("Receiving message %#v", msg)
switch {
case msg.Conversation != nil || msg.ExtendedTextMessage != nil:
b.handleTextMessage(message.Info, msg)
case msg.VideoMessage != nil:
b.handleVideoMessage(message)
case msg.AudioMessage != nil:
b.handleAudioMessage(message)
case msg.DocumentMessage != nil:
b.handleDocumentMessage(message)
case msg.ImageMessage != nil:
b.handleImageMessage(message)
case msg.ProtocolMessage != nil && *msg.ProtocolMessage.Type == proto.ProtocolMessage_REVOKE:
b.handleDelete(msg.ProtocolMessage)
}
}
// nolint:funlen
func (b *Bwhatsapp) handleTextMessage(messageInfo types.MessageInfo, msg *proto.Message) {
senderJID := messageInfo.Sender
channel := messageInfo.Chat
senderName := b.getSenderName(messageInfo)
if msg.GetExtendedTextMessage() == nil && msg.GetConversation() == "" {
b.Log.Debugf("message without text content? %#v", msg)
return
}
var text string
// nolint:nestif
if msg.GetExtendedTextMessage() == nil {
text = msg.GetConversation()
} else if msg.GetExtendedTextMessage().GetContextInfo() == nil {
// Handle pure text message with a link preview
// A pure text message with a link preview acts as an extended text message but will not contain any context info
text = msg.GetExtendedTextMessage().GetText()
} else {
text = msg.GetExtendedTextMessage().GetText()
ci := msg.GetExtendedTextMessage().GetContextInfo()
if senderJID == (types.JID{}) && ci.Participant != nil {
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
}
if ci.MentionedJID != nil {
// handle user mentions
for _, mentionedJID := range ci.MentionedJID {
numberAndSuffix := strings.SplitN(mentionedJID, "@", 2)
// mentions comes as telephone numbers and we don't want to expose it to other bridges
// replace it with something more meaninful to others
mention := b.getSenderNotify(types.NewJID(numberAndSuffix[0], types.DefaultUserServer))
text = strings.Replace(text, "@"+numberAndSuffix[0], "@"+mention, 1)
}
}
}
parentID := ""
if msg.GetExtendedTextMessage() != nil {
ci := msg.GetExtendedTextMessage().GetContextInfo()
parentID = getParentIdFromCtx(ci)
}
rmsg := config.Message{
UserID: senderJID.String(),
Username: senderName,
Text: text,
Channel: channel.String(),
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: getMessageIdFormat(senderJID, messageInfo.ID),
ParentID: parentID,
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
rmsg.Avatar = avatarURL
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
// HandleImageMessage sent from WhatsApp, relay it to the brige
func (b *Bwhatsapp) handleImageMessage(msg *events.Message) {
imsg := msg.Message.GetImageMessage()
senderJID := msg.Info.Sender
senderName := b.getSenderName(msg.Info)
ci := imsg.GetContextInfo()
if senderJID == (types.JID{}) && ci.Participant != nil {
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
}
rmsg := config.Message{
UserID: senderJID.String(),
Username: senderName,
Channel: msg.Info.Chat.String(),
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: getMessageIdFormat(senderJID, msg.Info.ID),
ParentID: getParentIdFromCtx(ci),
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
// rename .jfif to .jpg https://github.com/42wim/matterbridge/issues/1292
if fileExt[0] == ".jfif" {
fileExt[0] = ".jpg"
}
// rename .jpe to .jpg https://github.com/42wim/matterbridge/issues/1463
if fileExt[0] == ".jpe" {
fileExt[0] = ".jpg"
}
filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[0])
b.Log.Debugf("Trying to download %s with type %s", filename, imsg.GetMimetype())
data, err := b.wc.Download(imsg)
if err != nil {
b.Log.Errorf("Download image failed: %s", err)
return
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
// HandleVideoMessage downloads video messages
func (b *Bwhatsapp) handleVideoMessage(msg *events.Message) {
imsg := msg.Message.GetVideoMessage()
senderJID := msg.Info.Sender
senderName := b.getSenderName(msg.Info)
ci := imsg.GetContextInfo()
if senderJID == (types.JID{}) && ci.Participant != nil {
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
}
rmsg := config.Message{
UserID: senderJID.String(),
Username: senderName,
Channel: msg.Info.Chat.String(),
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: getMessageIdFormat(senderJID, msg.Info.ID),
ParentID: getParentIdFromCtx(ci),
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
if len(fileExt) == 0 {
fileExt = append(fileExt, ".mp4")
}
// Prefer .mp4 extension, otherwise fallback to first index
fileExtIndex := 0
for i, n := range fileExt {
if ".mp4" == n {
fileExtIndex = i
break
}
}
filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[fileExtIndex])
b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, imsg.GetFileLength(), imsg.GetMimetype())
data, err := b.wc.Download(imsg)
if err != nil {
b.Log.Errorf("Download video failed: %s", err)
return
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
// HandleAudioMessage downloads audio messages
func (b *Bwhatsapp) handleAudioMessage(msg *events.Message) {
imsg := msg.Message.GetAudioMessage()
senderJID := msg.Info.Sender
senderName := b.getSenderName(msg.Info)
ci := imsg.GetContextInfo()
if senderJID == (types.JID{}) && ci.Participant != nil {
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
}
rmsg := config.Message{
UserID: senderJID.String(),
Username: senderName,
Channel: msg.Info.Chat.String(),
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: getMessageIdFormat(senderJID, msg.Info.ID),
ParentID: getParentIdFromCtx(ci),
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
if len(fileExt) == 0 {
fileExt = append(fileExt, ".ogg")
}
filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[0])
b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, imsg.GetFileLength(), imsg.GetMimetype())
data, err := b.wc.Download(imsg)
if err != nil {
b.Log.Errorf("Download video failed: %s", err)
return
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, "audio message", "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
// HandleDocumentMessage downloads documents
func (b *Bwhatsapp) handleDocumentMessage(msg *events.Message) {
imsg := msg.Message.GetDocumentMessage()
senderJID := msg.Info.Sender
senderName := b.getSenderName(msg.Info)
ci := imsg.GetContextInfo()
if senderJID == (types.JID{}) && ci.Participant != nil {
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
}
rmsg := config.Message{
UserID: senderJID.String(),
Username: senderName,
Channel: msg.Info.Chat.String(),
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: getMessageIdFormat(senderJID, msg.Info.ID),
ParentID: getParentIdFromCtx(ci),
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
rmsg.Avatar = avatarURL
}
fileExt, err := mime.ExtensionsByType(imsg.GetMimetype())
if err != nil {
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
filename := fmt.Sprintf("%v", imsg.GetFileName())
b.Log.Debugf("Trying to download %s with extension %s and type %s", filename, fileExt, imsg.GetMimetype())
data, err := b.wc.Download(imsg)
if err != nil {
b.Log.Errorf("Download document message failed: %s", err)
return
}
// Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, imsg.GetCaption(), "", &data, b.General)
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
func (b *Bwhatsapp) handleDelete(messageInfo *proto.ProtocolMessage) {
sender, _ := types.ParseJID(*messageInfo.Key.Participant)
rmsg := config.Message{
Account: b.Account,
Protocol: b.Protocol,
ID: getMessageIdFormat(sender, *messageInfo.Key.ID),
Event: config.EventMsgDelete,
Text: config.EventMsgDelete,
Channel: *messageInfo.Key.RemoteJID,
}
b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}

View File

@ -0,0 +1,209 @@
//go:build whatsappmulti
// +build whatsappmulti
package bwhatsapp
import (
"fmt"
"strings"
goproto "google.golang.org/protobuf/proto"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/store/sqlstore"
"go.mau.fi/whatsmeow/types"
)
type ProfilePicInfo struct {
URL string `json:"eurl"`
Tag string `json:"tag"`
Status int16 `json:"status"`
}
func (b *Bwhatsapp) reloadContacts() {
if _, err := b.wc.Store.Contacts.GetAllContacts(); err != nil {
b.Log.Errorf("error on update of contacts: %v", err)
}
allcontacts, err := b.wc.Store.Contacts.GetAllContacts()
if err != nil {
b.Log.Errorf("error on update of contacts: %v", err)
}
if len(allcontacts) > 0 {
b.contacts = allcontacts
}
}
func (b *Bwhatsapp) getSenderName(info types.MessageInfo) string {
// Parse AD JID
var senderJid types.JID
senderJid.User, senderJid.Server = info.Sender.User, info.Sender.Server
sender, exists := b.contacts[senderJid]
if !exists || (sender.FullName == "" && sender.FirstName == "") {
b.reloadContacts() // Contacts may need to be reloaded
sender, exists = b.contacts[senderJid]
}
if exists && sender.FullName != "" {
return sender.FullName
}
if info.PushName != "" {
return info.PushName
}
if exists && sender.FirstName != "" {
return sender.FirstName
}
return "Someone"
}
func (b *Bwhatsapp) getSenderNameFromJID(senderJid types.JID) string {
sender, exists := b.contacts[senderJid]
if !exists || (sender.FullName == "" && sender.FirstName == "") {
b.reloadContacts() // Contacts may need to be reloaded
sender, exists = b.contacts[senderJid]
}
if exists && sender.FullName != "" {
return sender.FullName
}
if exists && sender.FirstName != "" {
return sender.FirstName
}
if sender.PushName != "" {
return sender.PushName
}
return "Someone"
}
func (b *Bwhatsapp) getSenderNotify(senderJid types.JID) string {
sender, exists := b.contacts[senderJid]
if !exists || (sender.FullName == "" && sender.PushName == "" && sender.FirstName == "") {
b.reloadContacts() // Contacts may need to be reloaded
sender, exists = b.contacts[senderJid]
}
if !exists {
return "someone"
}
if exists && sender.FullName != "" {
return sender.FullName
}
if exists && sender.PushName != "" {
return sender.PushName
}
if exists && sender.FirstName != "" {
return sender.FirstName
}
return "someone"
}
func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*types.ProfilePictureInfo, error) {
pjid, _ := types.ParseJID(jid)
info, err := b.wc.GetProfilePictureInfo(pjid, &whatsmeow.GetProfilePictureParams{
Preview: true,
})
if err != nil {
return nil, fmt.Errorf("failed to get avatar: %v", err)
}
return info, nil
}
func isGroupJid(identifier string) bool {
return strings.HasSuffix(identifier, "@g.us") ||
strings.HasSuffix(identifier, "@temp") ||
strings.HasSuffix(identifier, "@broadcast")
}
func (b *Bwhatsapp) getDevice() (*store.Device, error) {
device := &store.Device{}
storeContainer, err := sqlstore.New("sqlite", "file:"+b.Config.GetString("sessionfile")+".db?_pragma=foreign_keys(1)&_pragma=busy_timeout=10000", nil)
if err != nil {
return device, fmt.Errorf("failed to connect to database: %v", err)
}
device, err = storeContainer.GetFirstDevice()
if err != nil {
return device, fmt.Errorf("failed to get device: %v", err)
}
return device, nil
}
func (b *Bwhatsapp) getNewReplyContext(parentID string) (*proto.ContextInfo, error) {
replyInfo, err := b.parseMessageID(parentID)
if err != nil {
return nil, err
}
sender := fmt.Sprintf("%s@%s", replyInfo.Sender.User, replyInfo.Sender.Server)
ctx := &proto.ContextInfo{
StanzaID: &replyInfo.MessageID,
Participant: &sender,
QuotedMessage: &proto.Message{Conversation: goproto.String("")},
}
return ctx, nil
}
func (b *Bwhatsapp) parseMessageID(id string) (*Replyable, error) {
// No message ID in case action is executed on a message sent before the bridge was started
// and then the bridge cache doesn't have this message ID mapped
if id == "" {
return &Replyable{MessageID: id}, nil
}
replyInfo := strings.Split(id, "/")
if len(replyInfo) == 2 {
sender, err := types.ParseJID(replyInfo[0])
if err == nil {
return &Replyable{
MessageID: types.MessageID(replyInfo[1]),
Sender: sender,
}, nil
}
}
err := fmt.Errorf("MessageID does not match format of {senderJID}:{messageID} : \"%s\"", id)
return &Replyable{MessageID: id}, err
}
func getParentIdFromCtx(ci *proto.ContextInfo) string {
if ci != nil && ci.StanzaID != nil {
senderJid, err := types.ParseJID(*ci.Participant)
if err == nil {
return getMessageIdFormat(senderJid, *ci.StanzaID)
}
}
return ""
}
func getMessageIdFormat(jid types.JID, messageID string) string {
// we're crafting our own JID str as AD JID format messes with how stuff looks on a webclient
jidStr := fmt.Sprintf("%s@%s", jid.User, jid.Server)
return fmt.Sprintf("%s/%s", jidStr, messageID)
}

View File

@ -0,0 +1,456 @@
//go:build whatsappmulti
// +build whatsappmulti
package bwhatsapp
import (
"context"
"errors"
"fmt"
"mime"
"os"
"path/filepath"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/mdp/qrterminal"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/types"
waLog "go.mau.fi/whatsmeow/util/log"
goproto "google.golang.org/protobuf/proto"
_ "modernc.org/sqlite" // needed for sqlite
)
const (
// Account config parameters
cfgNumber = "Number"
)
// Bwhatsapp Bridge structure keeping all the information needed for relying
type Bwhatsapp struct {
*bridge.Config
startedAt time.Time
wc *whatsmeow.Client
contacts map[types.JID]types.ContactInfo
users map[string]types.ContactInfo
userAvatars map[string]string
joinedGroups []*types.GroupInfo
}
type Replyable struct {
MessageID types.MessageID
Sender types.JID
}
// New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file
func New(cfg *bridge.Config) bridge.Bridger {
number := cfg.GetString(cfgNumber)
if number == "" {
cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number")
}
b := &Bwhatsapp{
Config: cfg,
users: make(map[string]types.ContactInfo),
userAvatars: make(map[string]string),
}
return b
}
// Connect to WhatsApp. Required implementation of the Bridger interface
func (b *Bwhatsapp) Connect() error {
device, err := b.getDevice()
if err != nil {
return err
}
number := b.GetString(cfgNumber)
if number == "" {
return errors.New("whatsapp's telephone number need to be configured")
}
b.Log.Debugln("Connecting to WhatsApp..")
b.wc = whatsmeow.NewClient(device, waLog.Stdout("Client", "INFO", true))
b.wc.AddEventHandler(b.eventHandler)
firstlogin := false
var qrChan <-chan whatsmeow.QRChannelItem
if b.wc.Store.ID == nil {
firstlogin = true
qrChan, err = b.wc.GetQRChannel(context.Background())
if err != nil && !errors.Is(err, whatsmeow.ErrQRStoreContainsID) {
return errors.New("failed to to get QR channel:" + err.Error())
}
}
err = b.wc.Connect()
if err != nil {
return errors.New("failed to connect to WhatsApp: " + err.Error())
}
if b.wc.Store.ID == nil {
for evt := range qrChan {
if evt.Event == "code" {
qrterminal.GenerateHalfBlock(evt.Code, qrterminal.L, os.Stdout)
} else {
b.Log.Infof("QR channel result: %s", evt.Event)
}
}
}
// disconnect and reconnect on our first login/pairing
// for some reason the GetJoinedGroups in JoinChannel doesn't work on first login
if firstlogin {
b.wc.Disconnect()
time.Sleep(time.Second)
err = b.wc.Connect()
if err != nil {
return errors.New("failed to connect to WhatsApp: " + err.Error())
}
}
b.Log.Infoln("WhatsApp connection successful")
b.contacts, err = b.wc.Store.Contacts.GetAllContacts()
if err != nil {
return errors.New("failed to get contacts: " + err.Error())
}
b.joinedGroups, err = b.wc.GetJoinedGroups()
if err != nil {
return errors.New("failed to get list of joined groups: " + err.Error())
}
b.startedAt = time.Now()
// map all the users
for id, contact := range b.contacts {
if !isGroupJid(id.String()) && id.String() != "status@broadcast" {
// it is user
b.users[id.String()] = contact
}
}
// get user avatar asynchronously
b.Log.Info("Getting user avatars..")
for jid := range b.users {
info, err := b.GetProfilePicThumb(jid)
if err != nil {
b.Log.Warnf("Could not get profile photo of %s: %v", jid, err)
} else {
b.Lock()
if info != nil {
b.userAvatars[jid] = info.URL
}
b.Unlock()
}
}
b.Log.Info("Finished getting avatars..")
return nil
}
// Disconnect is called while reconnecting to the bridge
// Required implementation of the Bridger interface
func (b *Bwhatsapp) Disconnect() error {
b.wc.Disconnect()
return nil
}
// JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name'
// Required implementation of the Bridger interface
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
byJid := isGroupJid(channel.Name)
// verify if we are member of the given group
if byJid {
gJID, err := types.ParseJID(channel.Name)
if err != nil {
return err
}
for _, group := range b.joinedGroups {
if group.JID == gJID {
return nil
}
}
}
foundGroups := []string{}
for _, group := range b.joinedGroups {
if group.Name == channel.Name {
foundGroups = append(foundGroups, group.Name)
}
}
switch len(foundGroups) {
case 0:
// didn't match any group - print out possibilites
for _, group := range b.joinedGroups {
b.Log.Infof("%s %s", group.JID, group.Name)
}
return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name)
case 1:
return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", foundGroups[0], channel.Name)
default:
return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, foundGroups)
}
}
// Post a document message from the bridge to WhatsApp
func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (string, error) {
groupJID, _ := types.ParseJID(msg.Channel)
fi := msg.Extra["file"][0].(config.FileInfo)
caption := msg.Username + fi.Comment
resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaDocument)
if err != nil {
return "", err
}
// Post document message
var message proto.Message
var ctx *proto.ContextInfo
if msg.ParentID != "" {
ctx, _ = b.getNewReplyContext(msg.ParentID)
}
message.DocumentMessage = &proto.DocumentMessage{
Title: &fi.Name,
FileName: &fi.Name,
Mimetype: &filetype,
Caption: &caption,
MediaKey: resp.MediaKey,
FileEncSHA256: resp.FileEncSHA256,
FileSHA256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength),
URL: &resp.URL,
DirectPath: &resp.DirectPath,
ContextInfo: ctx,
}
b.Log.Debugf("=> Sending %#v as a document", msg)
ID := whatsmeow.GenerateMessageID()
_, err = b.wc.SendMessage(context.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID})
return ID, err
}
// Post an image message from the bridge to WhatsApp
// Handle, for sure image/jpeg, image/png and image/gif MIME types
func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (string, error) {
fi := msg.Extra["file"][0].(config.FileInfo)
caption := msg.Username + fi.Comment
resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaImage)
if err != nil {
return "", err
}
var message proto.Message
var ctx *proto.ContextInfo
if msg.ParentID != "" {
ctx, _ = b.getNewReplyContext(msg.ParentID)
}
message.ImageMessage = &proto.ImageMessage{
Mimetype: &filetype,
Caption: &caption,
MediaKey: resp.MediaKey,
FileEncSHA256: resp.FileEncSHA256,
FileSHA256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength),
URL: &resp.URL,
DirectPath: &resp.DirectPath,
ContextInfo: ctx,
}
b.Log.Debugf("=> Sending %#v as an image", msg)
return b.sendMessage(msg, &message)
}
// Post a video message from the bridge to WhatsApp
func (b *Bwhatsapp) PostVideoMessage(msg config.Message, filetype string) (string, error) {
fi := msg.Extra["file"][0].(config.FileInfo)
caption := msg.Username + fi.Comment
resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaVideo)
if err != nil {
return "", err
}
var message proto.Message
var ctx *proto.ContextInfo
if msg.ParentID != "" {
ctx, _ = b.getNewReplyContext(msg.ParentID)
}
message.VideoMessage = &proto.VideoMessage{
Mimetype: &filetype,
Caption: &caption,
MediaKey: resp.MediaKey,
FileEncSHA256: resp.FileEncSHA256,
FileSHA256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength),
URL: &resp.URL,
DirectPath: &resp.DirectPath,
ContextInfo: ctx,
}
b.Log.Debugf("=> Sending %#v as a video", msg)
return b.sendMessage(msg, &message)
}
// Post audio inline
func (b *Bwhatsapp) PostAudioMessage(msg config.Message, filetype string) (string, error) {
groupJID, _ := types.ParseJID(msg.Channel)
fi := msg.Extra["file"][0].(config.FileInfo)
resp, err := b.wc.Upload(context.Background(), *fi.Data, whatsmeow.MediaAudio)
if err != nil {
return "", err
}
var message proto.Message
var ctx *proto.ContextInfo
if msg.ParentID != "" {
ctx, _ = b.getNewReplyContext(msg.ParentID)
}
message.AudioMessage = &proto.AudioMessage{
Mimetype: &filetype,
MediaKey: resp.MediaKey,
FileEncSHA256: resp.FileEncSHA256,
FileSHA256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength),
URL: &resp.URL,
DirectPath: &resp.DirectPath,
ContextInfo: ctx,
}
b.Log.Debugf("=> Sending %#v as audio", msg)
ID, err := b.sendMessage(msg, &message)
var captionMessage proto.Message
caption := msg.Username + fi.Comment + "\u2B06" // the char on the end is upwards arrow emoji
captionMessage.Conversation = &caption
captionID := whatsmeow.GenerateMessageID()
_, err = b.wc.SendMessage(context.TODO(), groupJID, &captionMessage, whatsmeow.SendRequestExtra{ID: captionID})
return ID, err
}
// Send a message from the bridge to WhatsApp
func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
groupJID, _ := types.ParseJID(msg.Channel)
extendedMsgID, _ := b.parseMessageID(msg.ID)
msg.ID = extendedMsgID.MessageID
b.Log.Debugf("=> Receiving %#v", msg)
// Delete message
if msg.Event == config.EventMsgDelete {
if msg.ID == "" {
// No message ID in case action is executed on a message sent before the bridge was started
// and then the bridge cache doesn't have this message ID mapped
return "", nil
}
_, err := b.wc.RevokeMessage(groupJID, msg.ID)
return "", err
}
// Edit message
if msg.ID != "" {
b.Log.Debugf("updating message with id %s", msg.ID)
if b.GetString("editsuffix") != "" {
msg.Text += b.GetString("EditSuffix")
} else {
msg.Text += " (edited)"
}
}
// Handle Upload a file
if msg.Extra["file"] != nil {
fi := msg.Extra["file"][0].(config.FileInfo)
filetype := mime.TypeByExtension(filepath.Ext(fi.Name))
b.Log.Debugf("Extra file is %#v", filetype)
// TODO: add different types
// TODO: add webp conversion
switch filetype {
case "image/jpeg", "image/png", "image/gif":
return b.PostImageMessage(msg, filetype)
case "video/mp4", "video/3gpp": // TODO: Check if codecs are supported by WA
return b.PostVideoMessage(msg, filetype)
case "audio/ogg":
return b.PostAudioMessage(msg, "audio/ogg; codecs=opus") // TODO: Detect if it is actually OPUS
case "audio/aac", "audio/mp4", "audio/amr", "audio/mpeg":
return b.PostAudioMessage(msg, filetype)
default:
return b.PostDocumentMessage(msg, filetype)
}
}
var message proto.Message
text := msg.Username + msg.Text
// If we have a parent ID send an extended message
if msg.ParentID != "" {
replyContext, err := b.getNewReplyContext(msg.ParentID)
if err == nil {
message = proto.Message{
ExtendedTextMessage: &proto.ExtendedTextMessage{
Text: &text,
ContextInfo: replyContext,
},
}
return b.sendMessage(msg, &message)
}
}
message.Conversation = &text
return b.sendMessage(msg, &message)
}
func (b *Bwhatsapp) sendMessage(rmsg config.Message, message *proto.Message) (string, error) {
groupJID, _ := types.ParseJID(rmsg.Channel)
ID := whatsmeow.GenerateMessageID()
_, err := b.wc.SendMessage(context.Background(), groupJID, message, whatsmeow.SendRequestExtra{ID: ID})
return getMessageIdFormat(*b.wc.Store.ID, ID), err
}

34
bridge/xmpp/handler.go Normal file
View File

@ -0,0 +1,34 @@
package bxmpp
import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/matterbridge/go-xmpp"
)
// handleDownloadAvatar downloads the avatar of userid from channel
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
// logs an error message if it fails
func (b *Bxmpp) handleDownloadAvatar(avatar xmpp.AvatarData) {
rmsg := config.Message{
Username: "system",
Text: "avatar",
Channel: b.parseChannel(avatar.From),
Account: b.Account,
UserID: avatar.From,
Event: config.EventAvatarDownload,
Extra: make(map[string][]interface{}),
}
if _, ok := b.avatarMap[avatar.From]; !ok {
b.Log.Debugf("Avatar.From: %s", avatar.From)
err := helper.HandleDownloadSize(b.Log, &rmsg, avatar.From+".png", int64(len(avatar.Data)), b.General)
if err != nil {
b.Log.Error(err)
return
}
helper.HandleDownloadData(b.Log, &rmsg, avatar.From+".png", rmsg.Text, "", &avatar.Data, b.General)
b.Log.Debugf("Avatar download complete")
b.Remote <- rmsg
}
}

30
bridge/xmpp/helpers.go Normal file
View File

@ -0,0 +1,30 @@
package bxmpp
import (
"regexp"
"github.com/42wim/matterbridge/bridge/config"
)
var pathRegex = regexp.MustCompile("[^a-zA-Z0-9]+")
// GetAvatar constructs a URL for a given user-avatar if it is available in the cache.
func getAvatar(av map[string]string, userid string, general *config.Protocol) string {
if hash, ok := av[userid]; ok {
// NOTE: This does not happen in bridge/helper/helper.go but messes up XMPP
id := pathRegex.ReplaceAllString(userid, "_")
return general.MediaServerDownload + "/" + hash + "/" + id + ".png"
}
return ""
}
func (b *Bxmpp) cacheAvatar(msg *config.Message) string {
fi := msg.Extra["file"][0].(config.FileInfo)
/* if we have a sha we have successfully uploaded the file to the media server,
so we can now cache the sha */
if fi.SHA != "" {
b.Log.Debugf("Added %s to %s in avatarMap", fi.SHA, msg.UserID)
b.avatarMap[msg.UserID] = fi.SHA
}
return ""
}

490
bridge/xmpp/xmpp.go Normal file
View File

@ -0,0 +1,490 @@
package bxmpp
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
lru "github.com/hashicorp/golang-lru"
"github.com/jpillora/backoff"
"github.com/matterbridge/go-xmpp"
"github.com/rs/xid"
)
type Bxmpp struct {
*bridge.Config
startTime time.Time
xc *xmpp.Client
xmppMap map[string]string
connected bool
sync.RWMutex
StanzaIDs *lru.Cache
OriginIDs *lru.Cache
avatarAvailability map[string]bool
avatarMap map[string]string
}
func New(cfg *bridge.Config) bridge.Bridger {
stanzaIDs, _ := lru.New(5000)
originIDs, _ := lru.New(5000)
return &Bxmpp{
Config: cfg,
StanzaIDs: stanzaIDs,
OriginIDs: originIDs,
xmppMap: make(map[string]string),
avatarAvailability: make(map[string]bool),
avatarMap: make(map[string]string),
}
}
func (b *Bxmpp) Connect() error {
b.Log.Infof("Connecting %s", b.GetString("Server"))
if err := b.createXMPP(); err != nil {
b.Log.Debugf("%#v", err)
return err
}
b.Log.Info("Connection succeeded")
go b.manageConnection()
return nil
}
func (b *Bxmpp) Disconnect() error {
return nil
}
func (b *Bxmpp) JoinChannel(channel config.ChannelInfo) error {
if channel.Options.Key != "" {
b.Log.Debugf("using key %s for channel %s", channel.Options.Key, channel.Name)
b.xc.JoinProtectedMUC(channel.Name+"@"+b.GetString("Muc"), b.GetString("Nick"), channel.Options.Key, xmpp.NoHistory, 0, nil)
} else {
b.xc.JoinMUCNoHistory(channel.Name+"@"+b.GetString("Muc"), b.GetString("Nick"))
}
return nil
}
func (b *Bxmpp) Send(msg config.Message) (string, error) {
// should be fixed by using a cache instead of dropping
if !b.Connected() {
return "", fmt.Errorf("bridge %s not connected, dropping message %#v to bridge", b.Account, msg)
}
// ignore delete messages
if msg.Event == config.EventMsgDelete {
return "", nil
}
b.Log.Debugf("=> Receiving %#v", msg)
if msg.Event == config.EventAvatarDownload {
return b.cacheAvatar(&msg), nil
}
// Make a action /me of the message, prepend the username with it.
// https://xmpp.org/extensions/xep-0245.html
if msg.Event == config.EventUserAction {
msg.Username = "/me " + msg.Username
}
// Upload a file (in XMPP case send the upload URL because XMPP has no native upload support).
var err error
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.Log.Debugf("=> Sending attachement message %#v", rmsg)
if b.GetString("WebhookURL") != "" {
err = b.postSlackCompatibleWebhook(msg)
} else {
_, err = b.xc.Send(xmpp.Chat{
Type: "groupchat",
Remote: rmsg.Channel + "@" + b.GetString("Muc"),
Text: rmsg.Username + rmsg.Text,
})
}
if err != nil {
b.Log.WithError(err).Error("Unable to send message with share URL.")
}
}
if len(msg.Extra["file"]) > 0 {
return "", b.handleUploadFile(&msg)
}
}
if b.GetString("WebhookURL") != "" {
b.Log.Debugf("Sending message using Webhook")
err := b.postSlackCompatibleWebhook(msg)
if err != nil {
b.Log.Errorf("Failed to send message using webhook: %s", err)
return "", err
}
return "", nil
}
if msg.ParentNotFound() {
msg.ParentID = ""
}
// Post normal message.
var msgReplaceID string
msgID := xid.New().String()
if msg.ID != "" {
msgReplaceID = msg.ID
}
var replyID string
if res, ok := b.StanzaIDs.Get(msg.ParentID); ok {
replyID, _ = res.(string)
}
b.Log.Debugf("=> Sending message %#v", msg)
if _, err := b.xc.Send(xmpp.Chat{
Type: "groupchat",
Remote: msg.Channel + "@" + b.GetString("Muc"),
Text: msg.Username + msg.Text,
ID: msgID,
ReplaceID: msgReplaceID,
ReplyID: replyID,
}); err != nil {
return "", err
}
return msgID, nil
}
func (b *Bxmpp) postSlackCompatibleWebhook(msg config.Message) error {
type XMPPWebhook struct {
Username string `json:"username"`
Text string `json:"text"`
}
webhookBody, err := json.Marshal(XMPPWebhook{
Username: msg.Username,
Text: msg.Text,
})
if err != nil {
b.Log.Errorf("Failed to marshal webhook: %s", err)
return err
}
resp, err := http.Post(b.GetString("WebhookURL")+"/"+url.QueryEscape(msg.Channel), "application/json", bytes.NewReader(webhookBody))
if err != nil {
b.Log.Errorf("Failed to POST webhook: %s", err)
return err
}
resp.Body.Close()
return nil
}
func (b *Bxmpp) createXMPP() error {
var serverName string
switch {
case !b.GetBool("Anonymous"):
if !strings.Contains(b.GetString("Jid"), "@") {
return fmt.Errorf("the Jid %s doesn't contain an @", b.GetString("Jid"))
}
serverName = strings.Split(b.GetString("Jid"), "@")[1]
case !strings.Contains(b.GetString("Server"), ":"):
serverName = strings.Split(b.GetString("Server"), ":")[0]
default:
serverName = b.GetString("Server")
}
tc := &tls.Config{
ServerName: serverName,
InsecureSkipVerify: b.GetBool("SkipTLSVerify"), // nolint: gosec
}
xmpp.DebugWriter = b.Log.Writer()
options := xmpp.Options{
Host: b.GetString("Server"),
User: b.GetString("Jid"),
Password: b.GetString("Password"),
NoTLS: true,
StartTLS: !b.GetBool("NoTLS"),
TLSConfig: tc,
Debug: b.GetBool("debug"),
Session: true,
Status: "",
StatusMessage: "",
Resource: "",
InsecureAllowUnencryptedAuth: b.GetBool("NoTLS"),
}
var err error
b.xc, err = options.NewClient()
return err
}
func (b *Bxmpp) manageConnection() {
b.setConnected(true)
initial := true
bf := &backoff.Backoff{
Min: time.Second,
Max: 5 * time.Minute,
Jitter: true,
}
// Main connection loop. Each iteration corresponds to a successful
// connection attempt and the subsequent handling of the connection.
for {
if initial {
initial = false
} else {
b.Remote <- config.Message{
Username: "system",
Text: "rejoin",
Channel: "",
Account: b.Account,
Event: config.EventRejoinChannels,
}
}
if err := b.handleXMPP(); err != nil {
b.Log.WithError(err).Error("Disconnected.")
b.setConnected(false)
}
// Reconnection loop using an exponential back-off strategy. We
// only break out of the loop if we have successfully reconnected.
for {
d := bf.Duration()
b.Log.Infof("Reconnecting in %s.", d)
time.Sleep(d)
b.Log.Infof("Reconnecting now.")
if err := b.createXMPP(); err == nil {
b.setConnected(true)
bf.Reset()
break
}
b.Log.Warn("Failed to reconnect.")
}
}
}
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:
b.Log.Debugf("PING")
if err := b.xc.PingC2S("", ""); err != nil {
b.Log.Debugf("PING failed %#v", err)
}
case <-done:
return
}
}
}()
return done
}
func (b *Bxmpp) handleXMPP() error {
b.startTime = time.Now()
done := b.xmppKeepAlive()
defer close(done)
for {
m, err := b.xc.Recv()
if err != nil {
// An error together with AvatarData is non-fatal
switch m.(type) {
case xmpp.AvatarData:
continue
default:
return err
}
}
switch v := m.(type) {
case xmpp.Chat:
if v.Type == "groupchat" {
b.Log.Debugf("== Receiving %#v", v)
if v.ID != "" && v.StanzaID != "" {
b.StanzaIDs.Add(v.ID, v.StanzaID)
b.OriginIDs.Add(v.StanzaID, v.ID)
}
// Skip invalid messages.
if b.skipMessage(v) {
continue
}
var event string
if strings.Contains(v.Text, "has set the subject to:") {
event = config.EventTopicChange
}
available, sok := b.avatarAvailability[v.Remote]
avatar := ""
if !sok {
b.Log.Debugf("Requesting avatar data")
b.avatarAvailability[v.Remote] = false
b.xc.AvatarRequestData(v.Remote)
} else if available {
avatar = getAvatar(b.avatarMap, v.Remote, b.General)
}
msgID := v.ID
if v.ReplaceID != "" {
msgID = v.ReplaceID
}
var parentID string
if res, ok := b.OriginIDs.Get(v.ReplyID); ok {
parentID, _ = res.(string)
}
rmsg := config.Message{
Username: b.parseNick(v.Remote),
Text: v.Text,
Channel: b.parseChannel(v.Remote),
Account: b.Account,
Avatar: avatar,
UserID: v.Remote,
ParentID: parentID,
ID: msgID,
Event: event,
}
// Check if we have an action event.
var ok bool
rmsg.Text, ok = b.replaceAction(rmsg.Text)
if ok {
rmsg.Event = config.EventUserAction
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
case xmpp.AvatarData:
b.handleDownloadAvatar(v)
b.avatarAvailability[v.From] = true
b.Log.Debugf("Avatar for %s is now available", v.From)
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
}
// handleUploadFile handles native upload of files
func (b *Bxmpp) handleUploadFile(msg *config.Message) error {
var urlDesc string
for _, file := range msg.Extra["file"] {
fileInfo := file.(config.FileInfo)
if fileInfo.Comment != "" {
msg.Text += fileInfo.Comment + ": "
}
if fileInfo.URL != "" {
msg.Text = fileInfo.URL
if fileInfo.Comment != "" {
msg.Text = fileInfo.Comment + ": " + fileInfo.URL
urlDesc = fileInfo.Comment
}
}
if _, err := b.xc.Send(xmpp.Chat{
Type: "groupchat",
Remote: msg.Channel + "@" + b.GetString("Muc"),
Text: msg.Username + msg.Text,
}); err != nil {
return err
}
if fileInfo.URL != "" {
if _, err := b.xc.SendOOB(xmpp.Chat{
Type: "groupchat",
Remote: msg.Channel + "@" + b.GetString("Muc"),
Ooburl: fileInfo.URL,
Oobdesc: urlDesc,
}); err != nil {
b.Log.WithError(err).Warn("Failed to send share URL.")
}
}
}
return nil
}
func (b *Bxmpp) parseNick(remote string) string {
s := strings.Split(remote, "@")
if len(s) > 1 {
s = strings.Split(s[1], "/")
if len(s) == 2 {
return s[1] // nick
}
}
return ""
}
func (b *Bxmpp) parseChannel(remote string) string {
s := strings.Split(remote, "@")
if len(s) >= 2 {
return s[0] // channel
}
return ""
}
// skipMessage skips messages that need to be skipped
func (b *Bxmpp) skipMessage(message xmpp.Chat) bool {
// skip messages from ourselves
if b.parseNick(message.Remote) == b.GetString("Nick") {
return true
}
// skip empty messages
if message.Text == "" {
return true
}
// skip subject messages
if strings.Contains(message.Text, "</subject>") {
return true
}
// do not show subjects on connect #732
if strings.Contains(message.Text, "has set the subject to:") && time.Since(b.startTime) < time.Second*5 {
return true
}
// Ignore messages posted by our webhook
if b.GetString("WebhookURL") != "" && strings.Contains(message.ID, "webhookbot") {
return true
}
// skip delayed messages
return !message.Stamp.IsZero() && time.Since(message.Stamp).Minutes() > 5
}
func (b *Bxmpp) setConnected(state bool) {
b.Lock()
b.connected = state
defer b.Unlock()
}
func (b *Bxmpp) Connected() bool {
b.RLock()
defer b.RUnlock()
return b.connected
}

217
bridge/zulip/zulip.go Normal file
View File

@ -0,0 +1,217 @@
package bzulip
import (
"encoding/json"
"fmt"
"io/ioutil"
"strconv"
"strings"
"sync"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/version"
gzb "github.com/matterbridge/gozulipbot"
)
type Bzulip struct {
q *gzb.Queue
bot *gzb.Bot
streams map[int]string
*bridge.Config
sync.RWMutex
}
func New(cfg *bridge.Config) bridge.Bridger {
return &Bzulip{Config: cfg, streams: make(map[int]string)}
}
func (b *Bzulip) Connect() error {
bot := gzb.Bot{APIKey: b.GetString("token"), APIURL: b.GetString("server") + "/api/v1/", Email: b.GetString("login"), UserAgent: fmt.Sprintf("matterbridge/%s", version.Release)}
bot.Init()
q, err := bot.RegisterAll()
b.q = q
b.bot = &bot
if err != nil {
b.Log.Errorf("Connect() %#v", err)
return err
}
// init stream
b.getChannel(0)
b.Log.Info("Connection succeeded")
go b.handleQueue()
return nil
}
func (b *Bzulip) Disconnect() error {
return nil
}
func (b *Bzulip) JoinChannel(channel config.ChannelInfo) error {
return nil
}
func (b *Bzulip) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg)
// Delete message
if msg.Event == config.EventMsgDelete {
if msg.ID == "" {
return "", nil
}
_, err := b.bot.UpdateMessage(msg.ID, "")
return "", err
}
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.sendMessage(rmsg)
}
if len(msg.Extra["file"]) > 0 {
return b.handleUploadFile(&msg)
}
}
// edit the message if we have a msg ID
if msg.ID != "" {
_, err := b.bot.UpdateMessage(msg.ID, msg.Username+msg.Text)
return "", err
}
// Post normal message
return b.sendMessage(msg)
}
func (b *Bzulip) getChannel(id int) string {
if name, ok := b.streams[id]; ok {
return name
}
streams, err := b.bot.GetRawStreams()
if err != nil {
b.Log.Errorf("getChannel: %#v", err)
return ""
}
for _, stream := range streams.Streams {
b.streams[stream.StreamID] = stream.Name
}
if name, ok := b.streams[id]; ok {
return name
}
return ""
}
func (b *Bzulip) handleQueue() error {
for {
messages, err := b.q.GetEvents()
if err != nil {
switch err {
case gzb.BackoffError:
time.Sleep(time.Second * 5)
case gzb.NoJSONError:
b.Log.Error("Response wasn't JSON, server down or restarting? sleeping 10 seconds")
time.Sleep(time.Second * 10)
case gzb.BadEventQueueError:
b.Log.Info("got a bad event queue id error, reconnecting")
b.bot.Queues = nil
for {
b.q, err = b.bot.RegisterAll()
if err != nil {
b.Log.Errorf("reconnecting failed: %s. Sleeping 10 seconds", err)
time.Sleep(time.Second * 10)
}
break
}
case gzb.HeartbeatError:
b.Log.Debug("heartbeat received.")
default:
b.Log.Debugf("receiving error: %#v", err)
time.Sleep(time.Second * 10)
}
continue
}
for _, m := range messages {
b.Log.Debugf("== Receiving %#v", m)
// ignore our own messages
if m.SenderEmail == b.GetString("login") {
continue
}
avatarURL := m.AvatarURL
if !strings.HasPrefix(avatarURL, "http") {
avatarURL = b.GetString("server") + avatarURL
}
rmsg := config.Message{
Username: m.SenderFullName,
Text: m.Content,
Channel: b.getChannel(m.StreamID) + "/topic:" + m.Subject,
Account: b.Account,
UserID: strconv.Itoa(m.SenderID),
Avatar: avatarURL,
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
time.Sleep(time.Second * 3)
}
}
func (b *Bzulip) sendMessage(msg config.Message) (string, error) {
topic := ""
if strings.Contains(msg.Channel, "/topic:") {
res := strings.Split(msg.Channel, "/topic:")
topic = res[1]
msg.Channel = res[0]
}
m := gzb.Message{
Stream: msg.Channel,
Topic: topic,
Content: msg.Username + msg.Text,
}
resp, err := b.bot.Message(m)
if err != nil {
return "", err
}
if resp != nil {
defer resp.Body.Close()
res, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", err
}
var jr struct {
ID int `json:"id"`
}
err = json.Unmarshal(res, &jr)
if err != nil {
return "", err
}
return strconv.Itoa(jr.ID), nil
}
return "", nil
}
func (b *Bzulip) handleUploadFile(msg *config.Message) (string, error) {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.Comment != "" {
msg.Text += fi.Comment + ": "
}
if fi.URL != "" {
msg.Text = fi.URL
if fi.Comment != "" {
msg.Text = fi.Comment + ": " + fi.URL
}
}
_, err := b.sendMessage(*msg)
if err != nil {
return "", err
}
}
return "", nil
}

1803
changelog.md Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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
}

210
contrib/api.yaml Normal file
View File

@ -0,0 +1,210 @@
openapi: 3.0.0
info:
contact: {}
description: A read/write API for the Matterbridge chat bridge.
license:
name: Apache 2.0
url: 'https://github.com/42wim/matterbridge/blob/master/LICENSE'
title: Matterbridge API
version: "0.1.0-oas3"
paths:
/health:
get:
responses:
'200':
description: OK
content:
'*/*':
schema:
type: string
summary: Checks if the server is alive.
/message:
post:
responses:
'200':
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/config.OutgoingMessageResponse'
summary: Create a message
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/config.OutgoingMessage'
description: Message object to create
required: true
/messages:
get:
responses:
'200':
description: OK
content:
application/json:
schema:
items:
$ref: '#/components/schemas/config.IncomingMessage'
type: array
security:
- ApiKeyAuth: []
summary: List new messages
/stream:
get:
responses:
'200':
description: OK
content:
application/x-json-stream:
schema:
$ref: '#/components/schemas/config.IncomingMessage'
summary: Stream realtime messages
servers:
- url: /api
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
schemas:
config.IncomingMessage:
properties:
avatar:
description: URL to an avatar image
example: >-
https://secure.gravatar.com/avatar/1234567890abcdef1234567890abcdef.jpg
type: string
event:
description: >-
A specific matterbridge event. (see
https://github.com/42wim/matterbridge/blob/master/bridge/config/config.go#L16)
type: string
gateway:
description: Name of the gateway as configured in matterbridge.toml
example: mygateway
type: string
text:
description: Content of the message
example: 'Testing, testing, 1-2-3.'
type: string
username:
description: Human-readable username
example: alice
type: string
account:
description: Unique account name of format "[protocol].[slug]" as defined in matterbridge.toml
example: slack.myteam
type: string
channel:
description: Human-readable channel name of sending bridge
example: test-channel
type: string
id:
description: Unique ID of message on the gateway
example: slack 1541361213.030700
type: string
parent_id:
description: Unique ID of a parent message, if threaded
example: slack 1541361213.030700
type: string
protocol:
description: Chat protocol of the sending bridge
example: slack
type: string
timestamp:
description: Timestamp of the message
example: "1541361213.030700"
type: string
userid:
description: Userid on the sending bridge
example: U4MCXJKNC
type: string
extra:
description: Extra data that doesn't fit in other fields (eg base64 encoded files)
type: object
config.OutgoingMessage:
properties:
avatar:
description: URL to an avatar image
example: >-
https://secure.gravatar.com/avatar/1234567890abcdef1234567890abcdef.jpg
type: string
event:
description: >-
A specific matterbridge event. (see
https://github.com/42wim/matterbridge/blob/master/bridge/config/config.go#L16)
example: ""
type: string
gateway:
description: Name of the gateway as configured in matterbridge.toml
example: mygateway
type: string
text:
description: Content of the message
example: 'Testing, testing, 1-2-3.'
type: string
username:
description: Human-readable username
example: alice
type: string
type: object
required:
- gateway
- text
- username
config.OutgoingMessageResponse:
properties:
avatar:
description: URL to an avatar image
example: >-
https://secure.gravatar.com/avatar/1234567890abcdef1234567890abcdef.jpg
type: string
event:
description: >-
A specific matterbridge event. (see
https://github.com/42wim/matterbridge/blob/master/bridge/config/config.go#L16)
example: ""
type: string
gateway:
description: Name of the gateway as configured in matterbridge.toml
example: mygateway
type: string
text:
description: Content of the message
example: 'Testing, testing, 1-2-3.'
type: string
username:
description: Human-readable username
example: alice
type: string
account:
description: fixed api account
example: api.local
type: string
channel:
description: fixed api channel
example: api
type: string
id:
example: ""
type: string
parent_id:
example: ""
type: string
protocol:
description: fixed api protocol
example: api
type: string
timestamp:
description: Timestamp of the message
example: "1541361213.030700"
type: string
userid:
example: ""
type: string
extra:
example: null
type: object
type: object
security:
- bearerAuth: []

2
contrib/example.tengo Normal file
View File

@ -0,0 +1,2 @@
text := import("text")
msgText=text.re_replace("matterbridge",msgText,"matterbridge (https://github.com/42wim/matterbridge)")

View File

@ -0,0 +1,15 @@
fmt := import("fmt")
os := import("os")
times := import("times")
if msgText != "" && msgUsername != "system" {
os.chdir("/var/www/matterbridge")
file := os.open_file("inmessage.log", os.o_append|os.o_wronly|os.o_create, 0644)
file.write_string(fmt.sprintf(
"[%s] <%s> %s\n",
times.time_format(times.now(), times.format_rfc1123),
msgUsername,
msgText
))
file.close()
}

19
contrib/matterbridge.openrc Executable file
View File

@ -0,0 +1,19 @@
#!/sbin/openrc-run
# Copyright 2021-2022 Gentoo Authors
# Distributed under the terms of the GNU General Public License v2
command=/usr/bin/matterbridge
command_args="-conf ${MATTERBRIDGE_CONF:-/etc/matterbridge/bridge.toml} ${MATTERBRIDGE_ARGS}"
command_user="matterbridge:matterbridge"
pidfile="/run/${RC_SVCNAME}.pid"
command_background=1
output_log="/var/log/${RC_SVCNAME}.log"
error_log="${output_log}"
depend() {
need net
}
start_pre() {
checkpath -f "${output_log}" -o "${command_user}" || return 1
}

View File

@ -0,0 +1,11 @@
[Unit]
Description=matterbridge
After=network.target
[Service]
ExecStart=/usr/bin/matterbridge -conf /etc/matterbridge/bridge.toml
User=matterbridge
Group=matterbridge
[Install]
WantedBy=multi-user.target

View File

@ -0,0 +1,6 @@
text := import("text")
if outProtocol == "mumble" {
urlRE := text.re_compile(`(?is)((http|https):\/\/)?([a-z0-9-]+\.)?[a-z0-9-]+(\.[a-z]{2,6}){1,3}(\/[a-z0-9.,_\/~#&=;%+?-]*)?`)
msgText = urlRE.replace(msgText,`<a href="$0">$0</a>`)
}

View File

@ -0,0 +1,10 @@
text := import("text")
// if we're not sending to a discord bridge,
// then convert custom emoji tags into url's
if (inProtocol == "discord" && outProtocol != "discord") {
rePNG := text.re_compile(`<:.*?:([0-9]+)>`)
msgText=rePNG.replace(msgText,"https://cdn.discordapp.com/emojis/$1.png")
reGIF := text.re_compile(`<a:.*?:([0-9]+)>`)
msgText=reGIF.replace(msgText,"https://cdn.discordapp.com/emojis/$1.gif")
}

View File

@ -0,0 +1,14 @@
// See https://github.com/42wim/matterbridge/issues/881
// Generates a colored nick for each msgUsername, with example to filter specific codes
text := import("text")
fmt := import("fmt")
if outProtocol == "irc" {
// generate a color for a nick, make sure it isn't 0 or 15
colorCode := len(msgUsername)+bytes(msgUsername)[0]%14 + 2
// example if we want to use colorCode 3 when we have calculated colorcode 14
if colorCode == 14 {
colorCode = 3
}
msgUsername=fmt.sprintf("\x03%02d%s\x0F", colorCode, msgUsername)
}

View File

@ -0,0 +1,7 @@
// See https://github.com/42wim/matterbridge/issues/798
// if we're not sending to an irc bridge we strip the IRC colors
if outProtocol != "irc" {
re := text.re_compile(`\x03(?:\d{1,2}(?:,\d{1,2})?)?|[[:cntrl:]]`)
msgText=re.replace(msgText,"")
}

View File

@ -0,0 +1,16 @@
/*
This script will return the nick except with multi-character usernames
containing a zero-width space between the first and second character letter.
Single character usernames will be left untouched.
This is useful to prevent remote users from nickalerting
IRC users of the same name when the remote user speaks.
This result can be used in {TENGO} in RemoteNickFormat.
*/
result = nick
if len(nick) > 1 {
result = string(nick[0]) + "" + nick[1:]
}

View File

@ -0,0 +1,9 @@
/*
This script will return the current time in kitchen format if the protocol (of the remote bridge) isn't irc
See https://github.com/d5/tengo/blob/master/docs/stdlib-times.md
This result can be used in {TENGO} in RemoteNickFormat
*/
times := import("times")
if protocol != "irc" {
result=times.time_format(times.now(),times.format_kitchen)
}

10
docker/arm/Dockerfile Normal file
View File

@ -0,0 +1,10 @@
FROM alpine:edge as certs
RUN apk --update add ca-certificates
ARG VERSION=1.22.3
ADD https://github.com/42wim/matterbridge/releases/download/v${VERSION}/matterbridge-${VERSION}-linux-arm64 /bin/matterbridge
RUN chmod +x /bin/matterbridge
FROM scratch
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=certs /bin/matterbridge /bin/matterbridge
ENTRYPOINT ["/bin/matterbridge"]

5
gateway/bench.tengo Normal file
View File

@ -0,0 +1,5 @@
text := import("text")
if text.re_match("blah",msgText) {
msgText="replaced by this"
msgUsername="fakeuser"
}

11
gateway/bridgemap/api.go Normal file
View File

@ -0,0 +1,11 @@
// +build !noapi
package bridgemap
import (
"github.com/42wim/matterbridge/bridge/api"
)
func init() {
FullMap["api"] = api.New
}

View File

@ -0,0 +1,12 @@
// +build !nodiscord
package bridgemap
import (
bdiscord "github.com/42wim/matterbridge/bridge/discord"
)
func init() {
FullMap["discord"] = bdiscord.New
UserTypingSupport["discord"] = struct{}{}
}

View File

@ -0,0 +1,12 @@
//go:build !noharmony
// +build !noharmony
package bridgemap
import (
bharmony "github.com/42wim/matterbridge/bridge/harmony"
)
func init() {
FullMap["harmony"] = bharmony.New
}

11
gateway/bridgemap/birc.go Normal file
View File

@ -0,0 +1,11 @@
// +build !noirc
package bridgemap
import (
birc "github.com/42wim/matterbridge/bridge/irc"
)
func init() {
FullMap["irc"] = birc.New
}

View File

@ -0,0 +1,11 @@
// +build !nokeybase
package bridgemap
import (
bkeybase "github.com/42wim/matterbridge/bridge/keybase"
)
func init() {
FullMap["keybase"] = bkeybase.New
}

View File

@ -0,0 +1,11 @@
// +build !nomatrix
package bridgemap
import (
bmatrix "github.com/42wim/matterbridge/bridge/matrix"
)
func init() {
FullMap["matrix"] = bmatrix.New
}

View File

@ -0,0 +1,11 @@
// +build !nomattermost
package bridgemap
import (
bmattermost "github.com/42wim/matterbridge/bridge/mattermost"
)
func init() {
FullMap["mattermost"] = bmattermost.New
}

View File

@ -0,0 +1,11 @@
// +build !nomsteams
package bridgemap
import (
bmsteams "github.com/42wim/matterbridge/bridge/msteams"
)
func init() {
FullMap["msteams"] = bmsteams.New
}

View File

@ -0,0 +1,11 @@
// +build !nomumble
package bridgemap
import (
bmumble "github.com/42wim/matterbridge/bridge/mumble"
)
func init() {
FullMap["mumble"] = bmumble.New
}

View File

@ -0,0 +1,11 @@
// +build !nonctalk
package bridgemap
import (
btalk "github.com/42wim/matterbridge/bridge/nctalk"
)
func init() {
FullMap["nctalk"] = btalk.New
}

View File

@ -0,0 +1,10 @@
package bridgemap
import (
"github.com/42wim/matterbridge/bridge"
)
var (
FullMap = map[string]bridge.Factory{}
UserTypingSupport = map[string]struct{}{}
)

Some files were not shown because too many files have changed in this diff Show More