Compare commits

..

513 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
6620 changed files with 6631399 additions and 237069 deletions

2
.dockerignore Normal file
View File

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

View File

@ -1,6 +1,7 @@
---
name: Bug report
about: Create a report to help us improve. (Check the FAQ on the wiki first)
labels: bug
---

View File

@ -1,6 +1,7 @@
---
name: Feature request
about: Suggest an idea for this project
labels: enhancement
---

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

3
.gitignore vendored
View File

@ -4,3 +4,6 @@
# Exclude configuration file
matterbridge.toml
# Exclude IDE Files
.vscode

View File

@ -7,7 +7,7 @@ run:
# concurrency: 4
# timeout for analysis, e.g. 30s, 5m, default is 1m
deadline: 2m
deadline: 5m
# exit code when at least one issue was found, default is 1
issues-exit-code: 1
@ -23,7 +23,7 @@ run:
# 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:
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
@ -91,7 +91,6 @@ linters-settings:
# Correct spellings using locale preferences for US or UK.
# Default is to use a neutral variety of English.
# Setting locale to US will correct the British spelling of 'colour' to 'color'.
locale: US
lll:
# max line length, lines longer will be reported. Default is 120.
# '\t' is counted as 1 character by default, and can be changed with the tab-width option
@ -175,8 +174,47 @@ linters:
- 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.

View File

@ -18,8 +18,11 @@ builds:
- arm
- arm64
- 386
goarm:
- 6
- 7
ldflags:
- -s -w -X main.githash={{.ShortCommit}}
- -s -w -X github.com/42wim/matterbridge/version.GitHash={{.ShortCommit}}
archives:
-

View File

@ -1,56 +0,0 @@
language: go
go_import_path: github.com/42wim/matterbridge
# We have everything vendored so this helps TravisCI not run `go get ...`.
install: true
git:
depth: 200
notifications:
email: false
branches:
only:
- master
- /.*/
jobs:
include:
- stage: lint
# Run linting in one Go environment only.
script: ./ci/lint.sh
go: 1.13.x
env:
- GO111MODULE=on
- GOLANGCI_VERSION="v1.21.0"
- stage: test
# Run tests in a combination of Go environments.
script: ./ci/test.sh
go: 1.12.x
env:
- GO111MODULE=off
- script: ./ci/test.sh
go: 1.12.x
env:
- GO111MODULE=on
- script: ./ci/test.sh
go: 1.13.x
env:
- GO111MODULE=on
- REPORT_COVERAGE=1
- BINDEPLOY=1
before_deploy: /bin/bash ci/bintray.sh
deploy:
on:
all_branches: true
condition: $BINDEPLOY = 1
provider: bintray
edge:
branch: v1.8.47
file: ci/deploy.json
user: 42wim
key:
secure: "CeXXe6JOmt7HYR81MdWLua0ltQHhDdkIeRGBFbgd7hkb1wi8eF9DgpAcQrTso8NIlHNZmSAP46uhFgsRvkuezzX0ygalZ7DCJyAyn3sAMEh+UQSHV1WGThRehTtidqRGjetzsIGSwdrJOWil+XTfbO1Z8DGzfakhSuAZka8CM4BAoe3YeP9rYK8h+84x0GHfczvsLtXZ3mWLvQuwe4pK6+ItBCUg0ae7O7ZUpWHy0xQQkkWztY/6RAzXfaG7DuGjIw+20fhx3WOXRNpHCtZ6Bc3qERCpk0s1HhlQWlrN9wDaFTBWYwlvSnNgvxxMbNXJ6RrRJ0l0bA7FUswYwyroxhzrGLdzWDg8dHaQkypocngdalfhpsnoO9j3ApJhomUFJ3UoEq5nOGRUrKn8MPi+dP0zE4kNQ3e4VNa1ufNrvfpWolMg3xh8OXuhQdD5wIM5zFAbRJLqWSCVAjPq4DDPecmvXBOlIial7oa312lN5qnBnUjvAcxszZ+FUyDHT1Grxzna4tMwxY9obPzZUzm7359AOCCwIQFVB8GLqD2nwIstcXS0zGRz+fhviPipHuBa02q5bGUZwmkvrSNab0s8Jo7pCrel2Rz3nWPKaiCfq2WjbW1CLheSMkOQrjsdUd1hhbqNWFPUjJPInTc77NAKCfm5runv5uyowRLh4NNd0sI="

View File

@ -1,11 +1,14 @@
FROM alpine:edge
ENTRYPOINT ["/bin/matterbridge"]
FROM alpine AS builder
COPY . /go/src/github.com/42wim/matterbridge
RUN apk update && apk add go git gcc musl-dev ca-certificates mailcap \
&& cd /go/src/github.com/42wim/matterbridge \
&& export GOPATH=/go \
&& go get \
&& go build -x -ldflags "-X main.githash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge \
&& rm -rf /go \
&& apk del --purge git go gcc musl-dev
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"]

254
README.md
View File

@ -9,26 +9,26 @@ Letting people be where they want to be.<br />
<sup>
[Discord][mb-discord] |
[Gitter][mb-gitter] |
[IRC][mb-irc] |
[Discord][mb-discord] |
[Keybase][mb-keybase] |
[Matrix][mb-matrix] |
[Slack][mb-slack] |
[Mattermost][mb-mattermost] |
[MSTeams][mb-msteams] |
[Rocket.Chat][mb-rocketchat] |
[XMPP][mb-xmpp] |
[Slack][mb-slack] |
[Telegram][mb-telegram] |
[Twitch][mb-twitch] |
[WhatsApp][mb-whatsapp] |
[XMPP][mb-xmpp] |
[Zulip][mb-zulip] |
[Telegram][mb-telegram] |
[Keybase][mb-keybase] |
And more...
</sup>
---
[![Download stable](https://img.shields.io/github/release/42wim/matterbridge.svg?label=download%20stable)](https://github.com/42wim/matterbridge/releases/latest)
[![Download dev](https://img.shields.io/bintray/v/42wim/nightly/Matterbridge.svg?label=download%20dev&colorB=007ec6)](https://bintray.com/42wim/nightly/Matterbridge/_latestVersion)
[![Maintainability](https://api.codeclimate.com/v1/badges/82dff70ef2ba85a6173a/maintainability)](https://codeclimate.com/github/42wim/matterbridge/maintainability)
[![Test Coverage](https://api.codeclimate.com/v1/badges/82dff70ef2ba85a6173a/test_coverage)](https://codeclimate.com/github/42wim/matterbridge/test_coverage)<br />
@ -58,20 +58,22 @@ And more...
- [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)
- [Changelog](#changelog)
- [FAQ](#faq)
- [Related projects](#related-projects)
- [Articles](#articles)
- [Thanks](#thanks)
- [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
@ -86,29 +88,47 @@ And more...
### Natively supported
- [Mattermost](https://github.com/mattermost/mattermost-server/) 4.x, 5.x
- [IRC](http://www.mirc.com/servers.html)
- [XMPP](https://xmpp.org)
- [Gitter](https://gitter.im)
- [Slack](https://slack.com)
- [Discord](https://discordapp.com)
- [Telegram](https://telegram.org)
- [Rocket.chat](https://rocket.chat)
- [Matrix](https://matrix.org)
- [Steam](https://store.steampowered.com/)
- [Twitch](https://twitch.tv)
- [Ssh-chat](https://github.com/shazow/ssh-chat)
- [WhatsApp](https://www.whatsapp.com/)
- [Zulip](https://zulipchat.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)
- [Facebook messenger](https://github.com/VictorNine/fbridge)
- [Discourse](https://github.com/DeclanHoare/matterbabble)
- [Counter-Strike, half-life and more](https://forums.alliedmods.net/showthread.php?t=319430)
- [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
@ -117,61 +137,115 @@ More info and examples on the [wiki](https://github.com/42wim/matterbridge/wiki/
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 Server chat)
- [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]
- [Discord][mb-discord]
- [Keybase][mb-keybase]
- [Matrix][mb-matrix]
- [Slack][mb-slack]
- [Mattermost][mb-mattermost]
- [Rocket.Chat][mb-rocketchat]
- [XMPP][mb-xmpp] (matterbridge@conference.jabber.de)
- [Twitch][mb-twitch]
- [Zulip][mb-zulip]
- [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
See <https://github.com/42wim/matterbridge/wiki>
## Installing / upgrading
### Binaries
- Latest stable release [v1.16.5](https://github.com/42wim/matterbridge/releases/latest)
- Development releases (follows master) can be downloaded [here](https://dl.bintray.com/42wim/nightly/)
- 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) and 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.
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.12+ is required. Make sure you have [Go](https://golang.org/doc/install) properly installed.
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
```
go get github.com/42wim/matterbridge
To install the latest dev run:
```bash
go install github.com/42wim/matterbridge@master
```
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
```
## Building with whatsapp (beta) multidevice support
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
```
If you're low on memory and don't need msteams:
```bash
go install -tags nomsteams,whatsappmulti github.com/42wim/matterbridge@master
```
You should now have matterbridge binary in the ~/go/bin directory:
```bash
$ ls ~/go/bin/
matterbridge
```
@ -196,8 +270,8 @@ All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for
```toml
[irc]
[irc.freenode]
Server="irc.freenode.net:6667"
[irc.libera]
Server="irc.libera.chat:6667"
Nick="yourbotname"
[mattermost]
@ -213,7 +287,7 @@ All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for
name="mygateway"
enable=true
[[gateway.inout]]
account="irc.freenode"
account="irc.libera"
channel="#testing"
[[gateway.inout]]
@ -254,7 +328,7 @@ RemoteNickFormat="[{PROTOCOL}/{BRIDGE}] <{NICK}> "
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")
@ -270,6 +344,10 @@ Usage of ./matterbridge:
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)
@ -291,18 +369,26 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
- [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
## 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/
- http://bencey.co.nz/2018/09/17/bridge/
- https://www.algoo.fr/blog/2018/01/19/recouvrez-votre-liberte-en-quittant-slack-pour-un-mattermost-auto-heberge/
- https://kopano.com/blog/matterbridge-bridging-mattermost-chat/
- https://www.stitcher.com/s/?eid=52382713
- https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/
- <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
@ -315,36 +401,44 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
Matterbridge wouldn't exist without these libraries:
- discord - https://github.com/bwmarrin/discordgo
- echo - https://github.com/labstack/echo
- gitter - https://github.com/sromku/go-gitter
- gops - https://github.com/google/gops
- gozulipbot - https://github.com/ifo/gozulipbot
- irc - https://github.com/lrstanley/girc
- mattermost - https://github.com/mattermost/mattermost-server
- matrix - https://github.com/matrix-org/gomatrix
- sshchat - https://github.com/shazow/ssh-chat
- slack - https://github.com/nlopes/slack
- steam - https://github.com/Philipp15b/go-steam
- telegram - https://github.com/go-telegram-bot-api/telegram-bot-api
- xmpp - https://github.com/mattn/go-xmpp
- whatsapp - https://github.com/Rhymen/go-whatsapp/
- zulip - https://github.com/ifo/gozulipbot
- tengo - https://github.com/d5/tengo
- keybase - https://github.com/keybase/go-keybase-chat-bot
- 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-gitter]: https://gitter.im/42wim/matterbridge
[mb-irc]: https://webchat.freenode.net/?channels=matterbridgechat
[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-slack]: https://join.slack.com/matterbridgechat/shared_invite/MjEwODMxNjU1NDMwLTE0OTk2MTU3NTMtMzZkZmRiNDZhOA
[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-xmpp]: https://inverse.chat/
[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-keybase]: https://keybase.io
[mb-xmpp]: https://inverse.chat/
[mb-zulip]: https://matterbridge.zulipchat.com/register/
[mb-telegram]: https://t.me/Matterbridge

View File

@ -1,22 +1,28 @@
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/zfjagann/golang-ring"
"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 {
@ -32,6 +38,32 @@ func New(cfg *bridge.Config) bridge.Bridger {
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"))
@ -41,9 +73,17 @@ func New(cfg *bridge.Config) bridge.Bridger {
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") == "" {
@ -58,13 +98,13 @@ func New(cfg *bridge.Config) bridge.Bridger {
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) {
@ -74,7 +114,14 @@ func (b *API) Send(msg config.Message) (string, error) {
if msg.Event == config.EventMsgDelete {
return "", nil
}
b.Messages.Enqueue(&msg)
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
}
@ -93,6 +140,36 @@ func (b *API) handlePostMessage(c echo.Context) error {
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)
@ -106,25 +183,63 @@ func (b *API) handleMessages(c echo.Context) error {
return nil
}
func (b *API) handleStream(c echo.Context) error {
c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
c.Response().WriteHeader(http.StatusOK)
greet := config.Message{
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 {
msg := b.Messages.Dequeue()
if msg != nil {
if err := json.NewEncoder(c.Response()).Encode(msg); err != nil {
return err
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()
}
c.Response().Flush()
time.Sleep(100 * time.Millisecond)
case <-c.Request().Context().Done():
return nil
}
time.Sleep(200 * time.Millisecond)
}
}
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
}

View File

@ -4,6 +4,7 @@ import (
"log"
"strings"
"sync"
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/sirupsen/logrus"
@ -74,6 +75,7 @@ func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map
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
@ -84,8 +86,16 @@ func (b *Bridge) joinChannels(channels map[string]config.ChannelInfo, exists map
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.Account + "." + key)
val, ok := b.Config.GetBool(b.GetConfigKey(key))
if !ok {
val, _ = b.Config.GetBool("general." + key)
}
@ -93,7 +103,7 @@ func (b *Bridge) GetBool(key string) bool {
}
func (b *Bridge) GetInt(key string) int {
val, ok := b.Config.GetInt(b.Account + "." + key)
val, ok := b.Config.GetInt(b.GetConfigKey(key))
if !ok {
val, _ = b.Config.GetInt("general." + key)
}
@ -101,7 +111,7 @@ func (b *Bridge) GetInt(key string) int {
}
func (b *Bridge) GetString(key string) string {
val, ok := b.Config.GetString(b.Account + "." + key)
val, ok := b.Config.GetString(b.GetConfigKey(key))
if !ok {
val, _ = b.Config.GetString("general." + key)
}
@ -109,7 +119,7 @@ func (b *Bridge) GetString(key string) string {
}
func (b *Bridge) GetStringSlice(key string) []string {
val, ok := b.Config.GetStringSlice(b.Account + "." + key)
val, ok := b.Config.GetStringSlice(b.GetConfigKey(key))
if !ok {
val, _ = b.Config.GetStringSlice("general." + key)
}
@ -117,7 +127,7 @@ func (b *Bridge) GetStringSlice(key string) []string {
}
func (b *Bridge) GetStringSlice2D(key string) [][]string {
val, ok := b.Config.GetStringSlice2D(b.Account + "." + key)
val, ok := b.Config.GetStringSlice2D(b.GetConfigKey(key))
if !ok {
val, _ = b.Config.GetStringSlice2D("general." + key)
}

View File

@ -3,6 +3,8 @@ package config
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"strings"
"sync"
"time"
@ -21,11 +23,15 @@ const (
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"`
@ -42,14 +48,23 @@ type Message struct {
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
Name string
Data *[]byte
Comment string
URL string
Size int64
Avatar bool
SHA string
NativeID string
}
type ChannelInfo struct {
@ -72,35 +87,43 @@ type ChannelMember struct {
type ChannelMembers []ChannelMember
type Protocol struct {
AuthCode string // steam
BindAddress string // mattermost, slack // DEPRECATED
Buffer int // api
Charset string // irc
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
IconURL string // mattermost, slack
IgnoreFailureOnStart bool // general
IgnoreNicks string // all protocols
IgnoreMessages string // all protocols
Jid string // xmpp
Label string // all protocols
Login string // mattermost, matrix
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
@ -110,7 +133,7 @@ type Protocol struct {
NicksPerRow int // mattermost, slack
NoHomeServerSuffix bool // matrix
NoSendJoinPart bool // all protocols
NoTLS bool // mattermost
NoTLS bool // mattermost, xmpp
Password string // IRC,mattermost,XMPP,matrix
PrefixMessagesWithNick bool // mattemost, slack
PreserveThreading bool // slack
@ -118,12 +141,14 @@ type Protocol struct {
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
Server string // IRC,mattermost,XMPP,discord,matrix
SessionFile string // msteams,whatsapp
ShowJoinPart bool // all protocols
ShowTopicChange bool // slack
ShowUserTyping bool // slack
@ -131,19 +156,24 @@ type Protocol struct {
SkipTLSVerify bool // IRC, mattermost
SkipVersionCheck bool // mattermost
StripNick bool // all protocols
StripMarkdown bool // irc
SyncTopic bool // slack
TengoModifyMessage string // general
Team string // mattermost, keybase
Token string // gitter, slack, discord, api
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
UseUserName bool // discord, matrix, mattermost
UseInsecureURL bool // telegram
UserName string // IRC
VerboseJoinPart bool // IRC
WebhookBindAddress string // mattermost, slack
WebhookURL string // mattermost, slack
@ -201,6 +231,7 @@ type BridgeValues struct {
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
@ -210,6 +241,7 @@ type BridgeValues struct {
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)
@ -235,7 +267,17 @@ func NewConfig(rootLogger *logrus.Logger, cfgfile string) Config {
logger.Fatalf("Failed to read configuration file: %#v", err)
}
mycfg := newConfigFromString(logger, input)
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
}
@ -246,14 +288,26 @@ func NewConfig(rootLogger *logrus.Logger, cfgfile string) Config {
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)
return newConfigFromString(logger, input, "toml")
}
func newConfigFromString(logger *logrus.Entry, input []byte) *config {
viper.SetConfigType("toml")
func newConfigFromString(logger *logrus.Entry, input []byte, cfgtype string) *config {
viper.SetConfigType(cfgtype)
viper.SetEnvPrefix("matterbridge")
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_"))
viper.AutomaticEnv()
@ -281,6 +335,12 @@ 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()
@ -340,6 +400,11 @@ type TestConfig struct {
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 {

View File

@ -2,29 +2,31 @@ package bdiscord
import (
"bytes"
"errors"
"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
const (
MessageLength = 1950
cFileUpload = "file_upload"
)
type Bdiscord struct {
*bridge.Config
c *discordgo.Session
nick string
guildID string
webhookID string
webhookToken string
canEditWebhooks bool
nick string
userID string
guildID string
channelsMutex sync.RWMutex
channels []*discordgo.Channel
@ -33,30 +35,39 @@ type Bdiscord struct {
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 {
b := &Bdiscord{Config: cfg}
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)
if b.GetString("WebhookURL") != "" {
b.Log.Debug("Configuring Discord Incoming Webhook")
b.webhookID, b.webhookToken = b.splitURL(b.GetString("WebhookURL"))
b.useAutoWebhooks = b.GetBool("AutoWebhooks")
if b.useAutoWebhooks {
b.Log.Debug("Using automatic webhooks")
}
return b
}
func (b *Bdiscord) Connect() error {
var err error
var guildFound bool
token := b.GetString("Token")
b.Log.Info("Connecting")
if b.GetString("WebhookURL") == "" {
b.Log.Info("Connecting using token")
} else {
b.Log.Info("Connecting using webhookurl (for posting) and token")
}
if !strings.HasPrefix(b.GetString("Token"), "Bot ") {
token = "Bot " + b.GetString("Token")
}
@ -70,19 +81,16 @@ func (b *Bdiscord) Connect() error {
return err
}
b.Log.Info("Connection succeeded")
b.c.AddHandler(b.messageCreate)
b.c.AddHandler(b.messageTyping)
b.c.AddHandler(b.memberUpdate)
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)
// 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, "", "")
guilds, err := b.c.UserGuilds(100, "", "", false)
if err != nil {
return err
}
@ -92,55 +100,108 @@ func (b *Bdiscord) Connect() error {
}
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 {
if guild.Name == serverName || guild.ID == serverName {
b.channels, err = b.c.GuildChannels(guild.ID)
if err != nil {
break
}
b.guildID = guild.ID
guildFound = true
// 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 !guildFound {
msg := fmt.Sprintf("Server \"%s\" not found", b.GetString("Server"))
err = errors.New(msg)
b.Log.Error(msg)
b.Log.Info("Possible values:")
for _, guild := range guilds {
b.Log.Infof("Server=\"%s\" # Server name", guild.Name)
b.Log.Infof("Server=\"%s\" # Server ID", guild.ID)
}
}
if err != nil {
// 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
}
b.channelsMutex.RLock()
if b.GetString("WebhookURL") == "" {
for _, channel := range b.channels {
b.Log.Debugf("found channel %#v", channel)
}
} else {
b.canEditWebhooks = true
for _, channel := range b.channels {
b.Log.Debugf("found channel %#v; verifying PermissionManageWebhooks", channel)
perms, permsErr := b.c.State.UserChannelPermissions(userinfo.ID, channel.ID)
manageWebhooks := discordgo.PermissionManageWebhooks
if permsErr != nil || perms&manageWebhooks != manageWebhooks {
b.Log.Warnf("Can't manage webhooks in channel \"%s\"", channel.Name)
b.canEditWebhooks = false
// 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
}
if b.canEditWebhooks {
b.Log.Info("Can manage webhooks; will edit channel for global webhook on send")
} else {
b.Log.Warn("Can't manage webhooks; won't edit channel for global webhook on send")
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
}
}
b.channelsMutex.RUnlock()
// Obtaining guild members and initializing nickname mapping.
b.membersMutex.Lock()
@ -161,6 +222,19 @@ func (b *Bdiscord) Connect() error {
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
}
@ -197,76 +271,22 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
msg.Text = "_" + msg.Text + "_"
}
// use initial webhook configured for the entire Discord account
isGlobalWebhook := true
wID := b.webhookID
wToken := b.webhookToken
// check if have a channel specific webhook
b.channelsMutex.RLock()
if ci, ok := b.channelInfoMap[msg.Channel+b.Account]; ok {
if ci.Options.WebhookURL != "" {
wID, wToken = b.splitURL(ci.Options.WebhookURL)
isGlobalWebhook = false
}
// Handle prefix hint for unthreaded messages.
if msg.ParentNotFound() {
msg.ParentID = ""
}
b.channelsMutex.RUnlock()
// Use webhook to send the message
if wID != "" && msg.Event != config.EventMsgDelete {
// skip events
if msg.Event != "" && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange {
return "", nil
}
// If we are editing a message, delete the old message
if msg.ID != "" {
b.Log.Debugf("Deleting edited webhook message")
err := b.c.ChannelMessageDelete(channelID, msg.ID)
if err != nil {
b.Log.Errorf("Could not delete edited webhook message: %s", err)
}
}
b.Log.Debugf("Broadcasting using Webhook")
// skip empty messages
if msg.Text == "" && (msg.Extra == nil || len(msg.Extra["file"]) == 0) {
b.Log.Debugf("Skipping empty message %#v", msg)
return "", nil
}
msg.Text = helper.ClipMessage(msg.Text, MessageLength)
msg.Text = b.replaceUserMentions(msg.Text)
// discord username must be [0..32] max
if len(msg.Username) > 32 {
msg.Username = msg.Username[0:32]
}
// if we have a global webhook for this Discord account, and permission
// to modify webhooks (previously verified), then set its channel to
// the message channel before using it
// TODO: this isn't necessary if the last message from this webhook was
// sent to the current channel
if isGlobalWebhook && b.canEditWebhooks {
b.Log.Debugf("Setting webhook channel to \"%s\"", msg.Channel)
_, err := b.c.WebhookEdit(wID, "", "", channelID)
if err != nil {
b.Log.Errorf("Could not set webhook channel: %s", err)
return "", err
}
}
b.Log.Debugf("Processing webhook sending for message %#v", msg)
msg, err := b.webhookSend(&msg, wID, wToken)
if err != nil {
b.Log.Errorf("Could not broadcast via webook for message %#v: %s", msg, err)
return "", err
}
if msg == nil {
return "", nil
}
return msg.ID, nil
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
@ -278,80 +298,87 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
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) {
rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength)
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)
return b.handleUploadFile(msg, channelID)
}
}
msg.Text = helper.ClipMessage(msg.Text, MessageLength)
msg.Text = b.replaceUserMentions(msg.Text)
// Edit message
if msg.ID != "" {
_, err := b.c.ChannelMessageEdit(channelID, msg.ID, msg.Username+msg.Text)
return msg.ID, err
}
// Post normal message
res, err := b.c.ChannelMessageSend(channelID, msg.Username+msg.Text)
if err != nil {
return "", err
}
return res.ID, nil
}
// useWebhook returns true if we have a webhook defined somewhere
func (b *Bdiscord) useWebhook() bool {
if b.GetString("WebhookURL") != "" {
return true
}
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
for _, channel := range b.channelInfoMap {
if channel.Options.WebhookURL != "" {
return true
// 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))")
}
}
return false
}
// isWebhookID returns true if the specified id is used in a defined webhook
func (b *Bdiscord) isWebhookID(id string) bool {
if b.GetString("WebhookURL") != "" {
wID, _ := b.splitURL(b.GetString("WebhookURL"))
if wID == id {
return true
}
}
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
for _, channel := range b.channelInfoMap {
if channel.Options.WebhookURL != "" {
wID, _ := b.splitURL(channel.Options.WebhookURL)
if wID == id {
return true
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
}
return false
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) {
var err error
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
file := discordgo.File{
@ -360,72 +387,19 @@ func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (stri
Reader: bytes.NewReader(*fi.Data),
}
m := discordgo.MessageSend{
Content: msg.Username + fi.Comment,
Files: []*discordgo.File{&file},
Content: msg.Username + fi.Comment,
Files: []*discordgo.File{&file},
AllowedMentions: b.getAllowedMentions(),
}
_, err = b.c.ChannelMessageSendComplex(channelID, &m)
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
}
// 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, webhookID, token string) (*discordgo.Message, error) {
var (
res *discordgo.Message
err error
)
// WebhookParams can have either `Content` or `File`.
// We can't send empty messages.
if msg.Text != "" {
res, err = b.c.WebhookExecute(
webhookID,
token,
true,
&discordgo.WebhookParams{
Content: msg.Text,
Username: msg.Username,
AvatarURL: msg.Avatar,
},
)
if err != nil {
b.Log.Errorf("Could not send text (%s) for message %#v: %s", msg.Text, msg, err)
}
}
if msg.Extra != nil {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
file := discordgo.File{
Name: fi.Name,
ContentType: "",
Reader: bytes.NewReader(*fi.Data),
}
content := ""
if msg.Text == "" {
content = fi.Comment
}
_, e2 := b.c.WebhookExecute(
webhookID,
token,
false,
&discordgo.WebhookParams{
Username: msg.Username,
AvatarURL: msg.Avatar,
File: &file,
Content: content,
},
)
if e2 != nil {
b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, e2)
}
}
}
return res, err
}

View File

@ -3,9 +3,14 @@ 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)
@ -16,6 +21,10 @@ func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelet
// 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,
@ -31,22 +40,39 @@ func (b *Bdiscord) messageDeleteBulk(s *discordgo.Session, m *discordgo.MessageD
}
}
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 != "" {
if m.Message.EditedTimestamp != nil {
b.Log.Debugf("Sending edit message")
m.Content += b.GetString("EditSuffix")
msg := &discordgo.MessageCreate{
@ -57,6 +83,10 @@ func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdat
}
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
@ -64,7 +94,7 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
return
}
// if using webhooks, do not relay if it's ours
if b.useWebhook() && m.Author.Bot && b.isWebhookID(m.Author.ID) {
if m.Author.Bot && b.transmitter.HasWebhook(m.Author.ID) {
return
}
@ -77,8 +107,9 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
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 != "" {
b.Log.Debugf("== Receiving event %#v", m.Message)
m.Message.Content = b.replaceChannelMentions(m.Message.Content)
rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c)
if err != nil {
@ -90,12 +121,12 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
// set channel name
rmsg.Channel = b.getChannelName(m.ChannelID)
// set username
if !b.GetBool("UseUserName") {
fromWebhook := m.WebhookID != ""
if !fromWebhook && !b.GetBool("UseUserName") {
rmsg.Username = b.getNick(m.Author, m.GuildID)
} else {
rmsg.Username = m.Author.Username
if b.GetBool("UseDiscriminator") {
if !fromWebhook && b.GetBool("UseDiscriminator") {
rmsg.Username += "#" + m.Author.Discriminator
}
}
@ -103,7 +134,7 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
// if we have embedded content add it to text
if b.GetBool("ShowEmbeds") && m.Message.Embeds != nil {
for _, embed := range m.Message.Embeds {
rmsg.Text = rmsg.Text + "embed: " + embed.Title + " - " + embed.Description + " - " + embed.URL + "\n"
rmsg.Text += handleEmbed(embed)
}
}
@ -119,12 +150,24 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
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)
}
@ -152,6 +195,13 @@ func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUp
}
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
@ -173,6 +223,13 @@ func (b *Bdiscord) memberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd)
}
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
@ -192,3 +249,33 @@ func (b *Bdiscord) memberRemove(s *discordgo.Session, m *discordgo.GuildMemberRe
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)
}
}

View File

@ -9,6 +9,30 @@ import (
"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()
@ -44,7 +68,7 @@ func (b *Bdiscord) getGuildMemberByNick(nick string) (*discordgo.Member, error)
b.membersMutex.RLock()
defer b.membersMutex.RUnlock()
if member, ok := b.nickMemberMap[nick]; ok {
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
@ -137,6 +161,7 @@ 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 {
@ -182,15 +207,20 @@ func (b *Bdiscord) replaceUserMentions(text string) string {
return userMentionRE.ReplaceAllStringFunc(text, replaceUserMentionFunc)
}
func replaceEmotes(text string) string {
return emoteRE.ReplaceAllString(text, "$1")
}
func (b *Bdiscord) replaceAction(text string) (string, bool) {
if strings.HasPrefix(text, "_") && strings.HasSuffix(text, "_") {
return text[1 : len(text)-1], true
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) {
func (b *Bdiscord) splitURL(url string) (string, string, bool) {
const (
expectedWebhookSplitCount = 7
webhookIdxID = 5
@ -198,9 +228,9 @@ func (b *Bdiscord) splitURL(url string) (string, string) {
)
webhookURLSplit := strings.Split(url, "/")
if len(webhookURLSplit) != expectedWebhookSplitCount {
b.Log.Fatalf("%s is no correct discord WebhookURL", url)
return "", "", false
}
return webhookURLSplit[webhookIdxID], webhookURLSplit[webhookIdxToken]
return webhookURLSplit[webhookIdxID], webhookURLSplit[webhookIdxToken], true
}
func enumerateUsernames(s string) []string {

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
}

View File

@ -1,182 +0,0 @@
package bgitter
import (
"fmt"
"strings"
"github.com/42wim/go-gitter"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
)
type Bgitter struct {
c *gitter.Gitter
User *gitter.User
Users []gitter.User
Rooms []gitter.Room
*bridge.Config
}
func New(cfg *bridge.Config) bridge.Bridger {
return &Bgitter{Config: cfg}
}
func (b *Bgitter) Connect() error {
var err error
b.Log.Info("Connecting")
b.c = gitter.New(b.GetString("Token"))
b.User, err = b.c.GetUser()
if err != nil {
return err
}
b.Rooms, err = b.c.GetRooms()
if err != nil {
return err
}
b.Log.Info("Connection succeeded")
return nil
}
func (b *Bgitter) Disconnect() error {
return nil
}
func (b *Bgitter) JoinChannel(channel config.ChannelInfo) error {
roomID, err := b.c.GetRoomId(channel.Name)
if err != nil {
return fmt.Errorf("Could not find roomID for %v. Please create the room on gitter.im", channel.Name)
}
room, err := b.c.GetRoom(roomID)
if err != nil {
return err
}
b.Rooms = append(b.Rooms, *room)
user, err := b.c.GetUser()
if err != nil {
return err
}
_, err = b.c.JoinRoom(roomID, user.ID)
if err != nil {
return err
}
users, _ := b.c.GetUsersInRoom(roomID)
b.Users = append(b.Users, users...)
stream := b.c.Stream(roomID)
go b.c.Listen(stream)
go func(stream *gitter.Stream, room string) {
for event := range stream.Event {
switch ev := event.Data.(type) {
case *gitter.MessageReceived:
// ignore message sent from ourselves
if ev.Message.From.ID != b.User.ID {
b.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Message.From.Username, b.Account)
rmsg := config.Message{Username: ev.Message.From.Username, Text: ev.Message.Text, Channel: room,
Account: b.Account, Avatar: b.getAvatar(ev.Message.From.Username), UserID: ev.Message.From.ID,
ID: ev.Message.ID}
if strings.HasPrefix(ev.Message.Text, "@"+ev.Message.From.Username) {
rmsg.Event = config.EventUserAction
rmsg.Text = strings.Replace(rmsg.Text, "@"+ev.Message.From.Username+" ", "", -1)
}
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}
case *gitter.GitterConnectionClosed:
b.Log.Errorf("connection with gitter closed for room %s", room)
}
}
}(stream, room.URI)
return nil
}
func (b *Bgitter) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg)
roomID := b.getRoomID(msg.Channel)
if roomID == "" {
b.Log.Errorf("Could not find roomID for %v", msg.Channel)
return "", nil
}
// Delete message
if msg.Event == config.EventMsgDelete {
if msg.ID == "" {
return "", nil
}
// gitter has no delete message api so we edit message to ""
_, err := b.c.UpdateMessage(roomID, msg.ID, "")
if err != nil {
return "", err
}
return "", nil
}
// Upload a file (in gitter case send the upload URL because gitter has no native upload support)
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.c.SendMessage(roomID, rmsg.Username+rmsg.Text)
}
if len(msg.Extra["file"]) > 0 {
return b.handleUploadFile(&msg, roomID)
}
}
// Edit message
if msg.ID != "" {
b.Log.Debugf("updating message with id %s", msg.ID)
_, err := b.c.UpdateMessage(roomID, msg.ID, msg.Username+msg.Text)
if err != nil {
return "", err
}
return "", nil
}
// Post normal message
resp, err := b.c.SendMessage(roomID, msg.Username+msg.Text)
if err != nil {
return "", err
}
return resp.ID, nil
}
func (b *Bgitter) getRoomID(channel string) string {
for _, v := range b.Rooms {
if v.URI == channel {
return v.ID
}
}
return ""
}
func (b *Bgitter) getAvatar(user string) string {
var avatar string
if b.Users != nil {
for _, u := range b.Users {
if user == u.Username {
return u.AvatarURLSmall
}
}
}
return avatar
}
func (b *Bgitter) handleUploadFile(msg *config.Message, roomID string) (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.c.SendMessage(roomID, msg.Username+msg.Text)
if err != nil {
return "", err
}
}
return "", 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
}

View File

@ -48,6 +48,30 @@ func DownloadFileAuth(url string, auth string) (*[]byte, error) {
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.
@ -55,11 +79,19 @@ func DownloadFileAuth(url string, auth string) (*[]byte, error) {
// TODO: The current implementation has the inconvenient that it disregards
// word boundaries when splitting but this is hard to solve without potentially
// breaking formatting and other stylistic effects.
func GetSubLines(message string, maxLineLength int) []string {
const clippingMessage = " <clipped message>"
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
@ -142,17 +174,23 @@ func HandleDownloadSize(logger *logrus.Entry, msg *config.Message, name string,
// 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,
Name: name,
Data: data,
URL: url,
Comment: comment,
Avatar: avatar,
NativeID: id,
})
}
@ -166,21 +204,61 @@ func RemoveEmptyNewLines(msg string) string {
// 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) string {
const clippingMessage = " <clipped message>"
func ClipMessage(text string, length int, clippingMessage string) string {
if clippingMessage == "" {
clippingMessage = " <clipped message>"
}
if len(text) > length {
text = text[:length-len(clippingMessage)]
if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError {
text = text[:len(text)-size]
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
extensions := parser.HardLineBreak | parser.NoIntraEmphasis | parser.FencedCode
markdownParser := parser.NewWithExtensions(extensions)
renderer := html.NewRenderer(html.RendererOptions{
Flags: 0,
@ -192,7 +270,7 @@ func ParseMarkdown(input string) string {
return res
}
// ConvertWebPToPNG convert input data (which should be WebP format to PNG format)
// 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)

View File

@ -10,98 +10,105 @@ import (
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"},
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.",
},
"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."},
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!",
},
"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!",
},
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.",
},
},
"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.",
},
"Message ending with new-line.": {
input: "Newline ending\n",
splitOutput: []string{"Newline ending"},
nonSplitOutput: []string{"Newline ending"},
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.",
},
"Long message containing UTF-8 multi-byte runes": {
input: "不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說",
splitOutput: []string{
"不布人個我此而及單石業喜資富下 <clipped message>",
"我河下日沒一我臺空達的常景便物 <clipped message>",
"沒為……子大我別名解成?生賣的 <clipped message>",
"全直黑,我自我結毛分洲了世當, <clipped message>",
"是政福那是東;斯說",
},
nonSplitOutput: []string{"不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說"},
},
"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)
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)
nonSplitLines := GetSubLines(testcase.input, 0, "")
assert.Equalf(t, testcase.nonSplitOutput, nonSplitLines, "'%s' testcase should give expected lines without splitting.", testname)
}
}
@ -110,17 +117,122 @@ 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, 0644)
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
}

View File

@ -10,7 +10,6 @@ import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/dfordsoft/golib/ic"
"github.com/lrstanley/girc"
"github.com/paulrosania/go-charset/charset"
"github.com/saintfish/chardet"
@ -24,12 +23,12 @@ func (b *Birc) handleCharset(msg *config.Message) error {
if b.GetString("Charset") != "" {
switch b.GetString("Charset") {
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
msg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), msg.Text)
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 from utf-8 conversion failed: %s", err)
b.Log.Errorf("charset to utf-8 conversion failed: %s", err)
return err
}
fmt.Fprint(w, msg.Text)
@ -54,12 +53,12 @@ func (b *Birc) handleFiles(msg *config.Message) bool {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.Comment != "" {
msg.Text += fi.Comment + ": "
msg.Text += fi.Comment + " : "
}
if fi.URL != "" {
msg.Text = fi.URL
if fi.Comment != "" {
msg.Text = fi.Comment + ": " + fi.URL
msg.Text = fi.Comment + " : " + fi.URL
}
}
b.Local <- config.Message{Text: msg.Text, Username: msg.Username, Channel: msg.Channel, Event: msg.Event}
@ -67,6 +66,20 @@ func (b *Birc) handleFiles(msg *config.Message) bool {
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)
@ -109,14 +122,25 @@ func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) {
i := b.i
b.Nick = event.Params[0]
i.Handlers.Add("PRIVMSG", b.handlePrivMsg)
i.Handlers.Add("CTCP_ACTION", b.handlePrivMsg)
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.Add(girc.NOTICE, b.handleNotice)
i.Handlers.Add("JOIN", b.handleJoinPart)
i.Handlers.Add("PART", b.handleJoinPart)
i.Handlers.Add("QUIT", b.handleJoinPart)
i.Handlers.Add("KICK", b.handleJoinPart)
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() {
@ -170,14 +194,30 @@ 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}
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 event.IsAction() {
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()
@ -200,7 +240,7 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
}
switch mycharset {
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
rmsg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), rmsg.Text)
rmsg.Text = toUTF8(b.GetString("Charset"), rmsg.Text)
default:
r, err := charset.NewReader(mycharset, strings.NewReader(rmsg.Text))
if err != nil {
@ -217,6 +257,7 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
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)
}

View File

@ -2,8 +2,10 @@ package birc
import (
"crypto/tls"
"errors"
"fmt"
"hash/crc32"
"io/ioutil"
"net"
"sort"
"strconv"
@ -14,6 +16,7 @@ import (
"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
@ -28,6 +31,7 @@ type Birc struct {
Local chan config.Message // local queue for flood control
FirstConnection, authDone bool
MessageDelay, MessageQueue, MessageLength int
channels map[string]bool
*bridge.Config
}
@ -38,6 +42,8 @@ func New(cfg *bridge.Config) bridge.Bridger {
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 {
@ -67,6 +73,10 @@ func (b *Birc) Command(msg *config.Message) string {
}
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"))
@ -110,6 +120,7 @@ func (b *Birc) Disconnect() error {
}
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 {
@ -156,10 +167,14 @@ func (b *Birc) Send(msg config.Message) (string, error) {
}
var msgLines []string
if b.GetBool("StripMarkdown") {
msg.Text = stripmd.Strip(msg.Text)
}
if b.GetBool("MessageSplit") {
msgLines = helper.GetSubLines(msg.Text, b.MessageLength)
msgLines = helper.GetSubLines(msg.Text, b.MessageLength, b.GetString("MessageClipped"))
} else {
msgLines = helper.GetSubLines(msg.Text, 0)
msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
}
for i := range msgLines {
if len(b.Local) >= b.MessageQueue {
@ -167,12 +182,8 @@ func (b *Birc) Send(msg config.Message) (string, error) {
return "", nil
}
b.Local <- config.Message{
Text: msgLines[i],
Username: msg.Username,
Channel: msg.Channel,
Event: msg.Event,
}
msg.Text = msgLines[i]
b.Local <- msg
}
return "", nil
}
@ -199,22 +210,58 @@ func (b *Birc) doConnect() {
}
}
// 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
if b.GetBool("Colornicks") {
checksum := crc32.ChecksumIEEE([]byte(msg.Username))
colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes
username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username)
}
if msg.Event == config.EventUserAction {
b.i.Cmd.Action(msg.Channel, username+msg.Text)
// 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 {
b.Log.Debugf("Sending to channel %s", msg.Channel)
b.i.Cmd.Message(msg.Channel, username+msg.Text)
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)
}
}
}
}
@ -229,8 +276,11 @@ func (b *Birc) getClient() (*girc.Client, error) {
if err != nil {
return nil, err
}
user := b.GetString("UserName")
if user == "" {
user = b.GetString("Nick")
}
// fix strict user handling of girc
user := b.GetString("Nick")
for !girc.IsValidUser(user) {
if len(user) == 1 || len(user) == 0 {
user = "matterbridge"
@ -238,6 +288,27 @@ func (b *Birc) getClient() (*girc.Client, error) {
}
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,
@ -245,10 +316,15 @@ func (b *Birc) getClient() (*girc.Client, error) {
Port: port,
Nick: b.GetString("Nick"),
User: user,
Name: b.GetString("Nick"),
Name: realName,
SSL: b.GetBool("UseTLS"),
TLSConfig: &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, //nolint:gosec
PingDelay: time.Minute,
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
}
@ -258,12 +334,16 @@ func (b *Birc) endNames(client *girc.Client, event girc.Event) {
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.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.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)
@ -274,7 +354,7 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool {
b.Nick = b.i.GetNick()
// freenode doesn't send 001 as first reply
if event.Command == "NOTICE" {
if event.Command == "NOTICE" && len(event.Params) != 2 {
return true
}
// don't forward queries to the bot
@ -282,7 +362,18 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool {
return true
}
// don't forward message from ourself
if event.Source.Name == b.Nick {
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
@ -302,3 +393,23 @@ func (b *Birc) storeNames(client *girc.Client, event girc.Event) {
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

@ -4,7 +4,7 @@ import (
"strconv"
"github.com/42wim/matterbridge/bridge/config"
"github.com/keybase/go-keybase-chat-bot/kbchat"
"github.com/keybase/go-keybase-chat-bot/kbchat/types/chat1"
)
func (b *Bkeybase) handleKeybase() {
@ -20,7 +20,7 @@ func (b *Bkeybase) handleKeybase() {
b.Log.Errorf("failed to read message: %s", err.Error())
}
if msg.Message.Content.Type != "text" {
if msg.Message.Content.TypeName != "text" {
continue
}
@ -34,7 +34,7 @@ func (b *Bkeybase) handleKeybase() {
}()
}
func (b *Bkeybase) handleMessage(msg kbchat.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
@ -45,10 +45,10 @@ func (b *Bkeybase) handleMessage(msg kbchat.Message) {
// TODO download avatar
// Create our message
rmsg := config.Message{Username: msg.Sender.Username, Text: msg.Content.Text.Body, UserID: msg.Sender.Uid, Channel: msg.Channel.TopicName, ID: strconv.Itoa(msg.MsgID), Account: b.Account}
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.Type != "text" {
if msg.Content.TypeName != "text" {
b.Log.Errorf("message is not text")
return
}

View File

@ -90,16 +90,17 @@ func (b *Bkeybase) Send(msg config.Message) (string, error) {
return "", err
}
_, _ = b.kbc.SendAttachmentByTeam(b.team, fpath, fcaption, &b.channel)
_, _ = b.kbc.SendAttachmentByTeam(b.team, &b.channel, fpath, fcaption)
}
return "", nil
}
// Send regular message
resp, err := b.kbc.SendMessageByTeamName(b.team, msg.Username+msg.Text, &b.channel)
text := msg.Username + msg.Text
resp, err := b.kbc.SendMessageByTeamName(b.team, &b.channel, text)
if err != nil {
return "", err
}
return strconv.Itoa(resp.Result.MsgID), 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
}
}
}

View File

@ -3,11 +3,11 @@ package bmatrix
import (
"bytes"
"fmt"
"html"
"mime"
"regexp"
"strings"
"sync"
"time"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
@ -15,40 +15,108 @@ import (
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
RoomMap map[string]string
mc *matrix.Client
UserID string
NicknameMap map[string]NicknameCacheEntry
RoomMap map[string]string
rateMutex sync.RWMutex
sync.RWMutex
htmlTag *regexp.Regexp
*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.htmlTag = regexp.MustCompile("</.*?>")
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"))
b.mc, err = matrix.NewClient(b.GetString("Server"), "", "")
if err != nil {
return err
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")
}
resp, err := b.mc.Login(&matrix.ReqLogin{
Type: "m.login.password",
User: b.GetString("Login"),
Password: b.GetString("Password"),
})
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
}
@ -58,14 +126,18 @@ func (b *Bmatrix) Disconnect() error {
}
func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error {
resp, err := b.mc.JoinRoom(channel.Name, "", nil)
if err != nil {
return err
}
b.Lock()
b.RoomMap[resp.RoomID] = channel.Name
b.Unlock()
return err
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) {
@ -74,17 +146,61 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
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: msg.Username + msg.Text,
MsgType: "m.emote",
Body: body,
FormattedBody: formattedBody,
Format: "org.matrix.custom.html",
}
resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m)
if err != nil {
return "", err
if b.GetBool("HTMLDisable") {
m.Format = ""
m.FormattedBody = ""
}
return resp.EventID, err
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
@ -92,17 +208,34 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
if msg.ID == "" {
return "", nil
}
resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{})
if err != nil {
return "", err
}
return resp.EventID, err
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) {
if _, err := b.mc.SendText(channel, rmsg.Username+rmsg.Text); err != nil {
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)
}
}
@ -113,47 +246,162 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
}
// Edit message if we have an ID
// matrix has no editing support
if msg.ID != "" {
rmsg := EditedMessage{
TextMessage: matrix.TextMessage{
Body: body,
MsgType: "m.text",
Format: "org.matrix.custom.html",
FormattedBody: formattedBody,
},
}
// Use notices to send join/leave events
if msg.Event == config.EventJoinLeave {
resp, err := b.mc.SendNotice(channel, msg.Username+msg.Text)
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
}
username := html.EscapeString(msg.Username)
// check if we have a </tag>. if we have, we don't escape HTML. #696
if b.htmlTag.MatchString(msg.Username) {
username = msg.Username
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)
resp, err := b.mc.SendHTML(channel, msg.Username+msg.Text, username+helper.ParseMarkdown(msg.Text))
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) getRoomID(channel string) string {
b.RLock()
defer b.RUnlock()
for ID, name := range b.RoomMap {
if name == channel {
return ID
}
}
return ""
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)
}
@ -161,6 +409,77 @@ func (b *Bmatrix) handlematrix() {
}()
}
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 {
@ -172,16 +491,14 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
return
}
// TODO download avatar
// Create our message
rmsg := config.Message{Username: ev.Sender[1:], Channel: channel, Account: b.Account, UserID: ev.Sender, ID: ev.ID}
// 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
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
@ -199,11 +516,28 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
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)
@ -214,6 +548,11 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
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())
}
}
}
@ -288,27 +627,30 @@ func (b *Bmatrix) handleUploadFiles(msg *config.Message, channel string) (string
// 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])
if !(strings.Contains(mtype, "image") || strings.Contains(mtype, "video") ||
strings.Contains(mtype, "application") || strings.Contains(mtype, "audio")) {
return
}
if fi.Comment != "" {
_, err := b.mc.SendText(channel, msg.Username+fi.Comment)
if err != nil {
b.Log.Errorf("file comment failed: %#v", err)
}
} else {
// image and video uploads send no username, we have to do this ourself here #715
_, err := b.mc.SendText(channel, msg.Username)
if err != nil {
b.Log.Errorf("file comment failed: %#v", err)
}
// 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)
res, err := b.mc.UploadToContentRepo(content, mtype, int64(len(*fi.Data)))
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
@ -317,44 +659,60 @@ func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *conf
switch {
case strings.Contains(mtype, "video"):
b.Log.Debugf("sendVideo %s", res.ContentURI)
_, err = b.mc.SendVideo(channel, fi.Name, 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.mc.SendImage(channel, fi.Name, 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, "application"):
b.Log.Debugf("sendFile %s", res.ContentURI)
_, err = b.mc.SendFile(channel, fi.Name, res.ContentURI, mtype, uint(len(*fi.Data)))
if err != nil {
b.Log.Errorf("sendFile failed: %#v", err)
}
case strings.Contains(mtype, "audio"):
b.Log.Debugf("sendAudio %s", res.ContentURI)
_, err = b.mc.SendAudio(channel, fi.Name, res.ContentURI, mtype, uint(len(*fi.Data)))
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)
}
// skipMessages returns true if this message should not be handled
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
}

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

@ -1,10 +1,12 @@
package bmattermost
import (
"context"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterclient"
"github.com/mattermost/mattermost-server/model"
"github.com/matterbridge/matterclient"
"github.com/mattermost/mattermost/server/public/model"
)
// handleDownloadAvatar downloads the avatar of userid from channel
@ -21,12 +23,17 @@ func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
Extra: make(map[string][]interface{}),
}
if _, ok := b.avatarMap[userid]; !ok {
data, resp := b.mc.Client.GetProfileImage(userid, "")
if resp.Error != nil {
b.Log.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error)
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)
err = helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General)
if err != nil {
b.Log.Error(err)
return
@ -36,20 +43,20 @@ func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
}
}
// handleDownloadFile handles file download
//nolint:wrapcheck
func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error {
url, _ := b.mc.Client.GetFileLink(id)
finfo, resp := b.mc.Client.GetFileInfo(id)
if resp.Error != nil {
return resp.Error
}
err := helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General)
url, _, _ := b.mc.Client.GetFileLink(context.TODO(), id)
finfo, _, err := b.mc.Client.GetFileInfo(context.TODO(), id)
if err != nil {
return err
}
data, resp := b.mc.Client.DownloadFile(id, true)
if resp.Error != nil {
return resp.Error
err = helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General)
if err != nil {
return err
}
data, _, 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
@ -86,18 +93,24 @@ func (b *Bmattermost) handleMatter() {
}
}
//nolint:cyclop
func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
for message := range b.mc.MessageChan {
b.Log.Debugf("%#v", message.Raw.Data)
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, message.Channel)
b.handleDownloadAvatar(message.UserID, channelName)
}
b.Log.Debugf("== Receiving event %#v", message)
@ -105,10 +118,10 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
rmsg := &config.Message{
Username: message.Username,
UserID: message.UserID,
Channel: message.Channel,
Channel: channelName,
Text: message.Text,
ID: message.Post.Id,
ParentID: message.Post.ParentId,
ParentID: message.Post.RootId, // ParentID is obsolete with mattermost
Extra: make(map[string][]interface{}),
}
@ -116,11 +129,11 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
b.handleProps(rmsg, message)
// create a text for bridges that don't support native editing
if message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED && !b.GetBool("EditDisable") {
if message.Raw.EventType() == model.WebsocketEventPostEdited && !b.GetBool("EditDisable") {
rmsg.Text = message.Text + b.GetString("EditSuffix")
}
if message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED {
if message.Raw.EventType() == model.WebsocketEventPostDeleted {
rmsg.Event = config.EventMsgDelete
}
@ -132,8 +145,10 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
}
// Use nickname instead of username if defined
if nick := b.mc.GetNickName(rmsg.UserID); nick != "" {
rmsg.Username = nick
if !b.GetBool("useusername") {
if nick := b.mc.GetNickName(rmsg.UserID); nick != "" {
rmsg.Username = nick
}
}
messages <- rmsg
@ -144,6 +159,7 @@ 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,
@ -153,11 +169,10 @@ func (b *Bmattermost) handleMatterHook(messages chan *config.Message) {
}
}
// handleUploadFile handles native upload of files
func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
var err error
var res, id string
channelID := b.mc.GetChannelId(msg.Channel, b.TeamID)
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)
@ -173,6 +188,7 @@ func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
return res, err
}
//nolint:forcetypeassert
func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Message) {
props := message.Post.Props
if props == nil {
@ -183,16 +199,18 @@ func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Me
}
if _, ok := props["attachments"].([]interface{}); ok {
rmsg.Extra["attachments"] = props["attachments"].([]interface{})
if rmsg.Text == "" {
for _, attachment := range rmsg.Extra["attachments"] {
attach := attachment.(map[string]interface{})
if attach["text"].(string) != "" {
rmsg.Text += attach["text"].(string)
continue
}
if attach["fallback"].(string) != "" {
rmsg.Text += attach["fallback"].(string)
}
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

@ -1,13 +1,14 @@
package bmattermost
import (
"net/http"
"strings"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterclient"
"github.com/42wim/matterbridge/matterhook"
"github.com/mattermost/mattermost-server/model"
"github.com/matterbridge/matterclient"
"github.com/mattermost/mattermost/server/public/model"
)
func (b *Bmattermost) doConnectWebhookBind() error {
@ -15,8 +16,10 @@ func (b *Bmattermost) doConnectWebhookBind() error {
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")})
matterhook.Config{
InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
BindAddress: b.GetString("WebhookBindAddress"),
})
case b.GetString("Token") != "":
b.Log.Info("Connecting using token (sending)")
err := b.apiLogin()
@ -32,8 +35,10 @@ func (b *Bmattermost) doConnectWebhookBind() error {
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")})
matterhook.Config{
InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
BindAddress: b.GetString("WebhookBindAddress"),
})
}
return nil
}
@ -41,8 +46,10 @@ func (b *Bmattermost) doConnectWebhookBind() error {
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})
matterhook.Config{
InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
DisableServer: true,
})
if b.GetString("Token") != "" {
b.Log.Info("Connecting using token (receiving)")
err := b.apiLogin()
@ -59,13 +66,14 @@ func (b *Bmattermost) doConnectWebhookURL() error {
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"))
b.mc = matterclient.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server"), "")
if b.GetBool("debug") {
b.mc.SetLogLevel("debug")
}
@ -73,14 +81,13 @@ func (b *Bmattermost) apiLogin() error {
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"))
err := b.mc.Login()
if err != nil {
if err := b.mc.Login(); err != nil {
return err
}
b.Log.Info("Connection succeeded")
b.TeamID = b.mc.GetTeamId()
go b.mc.WsReceiver()
go b.mc.StatusLoop()
b.TeamID = b.mc.GetTeamID()
return nil
}
@ -113,6 +120,7 @@ func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
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) {
@ -136,7 +144,7 @@ func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
msg.Text += fi.URL
msg.Text += " " + fi.URL
}
}
}
@ -163,19 +171,37 @@ func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
}
// skipMessages returns true if this message should not be handled
//
//nolint:gocyclo,cyclop
func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
// Handle join/leave
if message.Type == "system_join_leave" ||
message.Type == "system_join_channel" ||
message.Type == "system_leave_channel" {
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: message.Channel,
Channel: channelName,
Account: b.Account,
Event: config.EventJoinLeave,
}
@ -183,7 +209,7 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
}
// Handle edited messages
if (message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED) && b.GetBool("EditDisable") {
if (message.Raw.EventType() == model.WebsocketEventPostEdited) && b.GetBool("EditDisable") {
return true
}
@ -196,13 +222,14 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
// Ignore messages sent from matterbridge
if message.Post.Props != nil {
if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok {
b.Log.Debugf("sent by matterbridge, ignoring")
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
}
@ -212,14 +239,56 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
}
// ignore messages from other teams than ours
if message.Raw.Data["team_id"].(string) != b.TeamID {
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.Event == "posted" || message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED ||
message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED) {
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

@ -1,31 +1,44 @@
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/matterclient"
"github.com/42wim/matterbridge/matterhook"
"github.com/matterbridge/matterclient"
"github.com/rs/xid"
)
type Bmattermost struct {
mh *matterhook.Client
mc *matterclient.MMClient
mc *matterclient.Client
v6 bool
uuid string
TeamID string
*bridge.Config
avatarMap map[string]string
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)}
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
}
@ -37,6 +50,13 @@ 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
@ -60,6 +80,7 @@ func (b *Bmattermost) Connect() error {
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
@ -81,14 +102,21 @@ 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.mc.GetChannelId(channel.Name, b.TeamID)
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
}
@ -118,19 +146,31 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
if msg.ID == "" {
return "", nil
}
return msg.ID, b.mc.DeleteMessage(msg.ID)
}
// Handle prefix hint for unthreaded messages.
if msg.ParentID == "msg-parent-not-found" {
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.mc.GetChannelId(rmsg.Channel, b.TeamID), rmsg.Username+rmsg.Text, msg.ParentID); err != nil {
if _, err := b.mc.PostMessage(b.getChannelID(rmsg.Channel), rmsg.Username+rmsg.Text, msg.ParentID); err != nil {
b.Log.Errorf("PostMessage failed: %s", err)
}
}
@ -150,5 +190,5 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
}
// Post normal message
return b.mc.PostMessage(b.mc.GetChannelId(msg.Channel, b.TeamID), msg.Text, msg.ParentID)
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

@ -1,7 +1,11 @@
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() {
@ -38,8 +42,26 @@ func (b *Brocketchat) handleRocketHook(messages chan *config.Message) {
}
}
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
@ -58,11 +80,51 @@ func (b *Brocketchat) handleRocketClient(messages chan *config.Message) {
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
}
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)

View File

@ -135,6 +135,7 @@ func (b *Brocketchat) uploadFile(fi *config.FileInfo, channel string) error {
if err != nil {
return err
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err

View File

@ -29,6 +29,12 @@ type Brocketchat struct {
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 {

View File

@ -1,18 +1,22 @@
package bslack
import (
"errors"
"fmt"
"html"
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/nlopes/slack"
"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) != "" {
if b.GetString(incomingWebhookConfig) != "" && b.GetString(tokenConfig) == "" {
b.Log.Debugf("Choosing webhooks based receiving")
go b.handleMatterHook(messages)
} else {
@ -23,7 +27,8 @@ func (b *Bslack) handleSlack() {
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 {
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)
@ -53,7 +58,9 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) {
continue
}
rmsg, err := b.handleTypingEvent(ev)
if err != nil {
if err == ErrEventIgnored {
continue
} else if err != nil {
b.Log.Errorf("%#v", err)
continue
}
@ -70,6 +77,13 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) {
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:
@ -87,8 +101,10 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) {
b.Log.Errorf("Connection failed %#v %#v", ev.Error(), ev.ErrorObj)
case *slack.MemberJoinedChannelEvent:
b.users.populateUser(ev.User)
case *slack.HelloEvent, *slack.LatencyReport:
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)
}
@ -124,11 +140,11 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
}
}
// Skip any messages that we made ourselves or from 'slackbot' (see #527).
if ev.Username == sSlackBotUser ||
(b.rtm != nil && ev.Username == b.si.User.Name) ||
(len(ev.Attachments) > 0 && ev.Attachments[0].CallbackID == "matterbridge_"+b.uuid) {
return true
// 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 {
@ -143,6 +159,16 @@ func (b *Bslack) skipMessageEvent(ev *slack.MessageEvent) bool {
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 {
@ -204,6 +230,26 @@ func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, er
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:
@ -236,6 +282,13 @@ func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message)
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 {
@ -244,12 +297,15 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message)
// See if we have some text in the attachments.
if rmsg.Text == "" {
for _, attach := range ev.Attachments {
for i, attach := range ev.Attachments {
if attach.Text != "" {
if attach.Title != "" {
rmsg.Text = attach.Title + "\n"
rmsg.Text = getMessageTitle(&ev.Attachments[i])
}
rmsg.Text += attach.Text
if attach.Footer != "" {
rmsg.Text += "\n\n" + attach.Footer
}
} else {
rmsg.Text = attach.Fallback
}
@ -263,6 +319,8 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message)
// 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)
}
@ -270,6 +328,9 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message)
}
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
@ -309,7 +370,7 @@ func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File, retr
// that the comment is not duplicated.
comment := rmsg.Text
rmsg.Text = ""
helper.HandleDownloadData(b.Log, rmsg, file.Name, comment, file.URLPrivateDownload, data, b.General)
helper.HandleDownloadData2(b.Log, rmsg, file.Name, file.ID, comment, file.URLPrivateDownload, data, b.General)
return nil
}

View File

@ -7,8 +7,8 @@ import (
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/nlopes/slack"
"github.com/sirupsen/logrus"
"github.com/slack-go/slack"
)
// populateReceivedMessage shapes the initial Matterbridge message that we will forward to the
@ -87,6 +87,9 @@ func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *confi
if user.Profile.DisplayName != "" {
rmsg.Username = user.Profile.DisplayName
}
if b.GetBool("UseFullName") && user.Profile.RealName != "" {
rmsg.Username = user.Profile.RealName
}
return nil
}
@ -98,7 +101,9 @@ func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config
var err error
var bot *slack.Bot
for {
bot, err = b.rtm.GetBotInfo(ev.BotID)
bot, err = b.rtm.GetBotInfo(slack.GetBotInfoParameters{
Bot: ev.BotID,
})
if err == nil {
break
}
@ -124,7 +129,7 @@ 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(`<(.*?)(\|.*?)?>`)
urlRE = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
codeFenceRE = regexp.MustCompile(`(?m)^` + "```" + `\w+$`)
topicOrPurposeRE = regexp.MustCompile(`(?s)(@.+) (cleared|set)(?: the)? channel (topic|purpose)(?:: (.*))?`)
)
@ -178,14 +183,7 @@ func (b *Bslack) replaceVariable(text string) string {
// @see https://api.slack.com/docs/message-formatting#linking_to_urls
func (b *Bslack) replaceURL(text string) string {
for _, r := range urlRE.FindAllStringSubmatch(text, -1) {
if len(strings.TrimSpace(r[2])) == 1 { // A display text separator was found, but the text was blank
text = strings.Replace(text, r[0], "", 1)
} else {
text = strings.Replace(text, r[0], r[1], 1)
}
}
return text
return urlRE.ReplaceAllString(text, "[${2}](${1})")
}
func (b *Bslack) replaceb0rkedMarkDown(text string) string {

View File

@ -5,7 +5,7 @@ import (
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/matterhook"
"github.com/nlopes/slack"
"github.com/slack-go/slack"
)
type BLegacy struct {

View File

@ -13,8 +13,8 @@ import (
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterhook"
lru "github.com/hashicorp/golang-lru"
"github.com/nlopes/slack"
"github.com/rs/xid"
"github.com/slack-go/slack"
)
type Bslack struct {
@ -36,24 +36,25 @@ type Bslack struct {
}
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"
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"
@ -64,6 +65,7 @@ const (
editSuffixConfig = "EditSuffix"
iconURLConfig = "iconurl"
noSendJoinConfig = "nosendjoinpart"
messageLength = 3000
)
func New(cfg *bridge.Config) bridge.Bridger {
@ -155,7 +157,7 @@ func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
// try to join a channel when in legacy
if b.legacy {
_, err := b.sc.JoinChannel(channel.Name)
_, _, _, err := b.sc.JoinConversation(channel.Name)
if err != nil {
switch err.Error() {
case "name_taken", "restricted_action":
@ -194,6 +196,7 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
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
@ -202,7 +205,7 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
}
// Use webhook to send the message
if b.GetString(outgoingWebhookConfig) != "" {
if b.GetString(outgoingWebhookConfig) != "" && b.GetString(tokenConfig) == "" {
return "", b.sendWebhook(msg)
}
return b.sendRTM(msg)
@ -297,7 +300,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
}
// Handle prefix hint for unthreaded messages.
if msg.ParentID == "msg-parent-not-found" {
if msg.ParentNotFound() {
msg.ParentID = ""
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
}
@ -318,7 +321,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
}
// Upload a file if it exists.
if msg.Extra != nil {
if len(msg.Extra) > 0 {
extraMsgs := helper.HandleExtra(&msg, b.General)
for i := range extraMsgs {
rmsg := &extraMsgs[i]
@ -329,7 +332,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
}
}
// Upload files if necessary (from Slack, Telegram or Mattermost).
b.uploadFile(&msg, channelInfo.ID)
return b.uploadFile(&msg, channelInfo.ID)
}
// Post message.
@ -408,7 +411,6 @@ func (b *Bslack) editMessage(msg *config.Message, channelInfo *slack.Channel) (b
}
messageOptions := b.prepareMessageOptions(msg)
for {
messageOptions = append(messageOptions, slack.MsgOptionText(msg.Text, false))
_, _, _, err := b.rtm.UpdateMessage(channelInfo.ID, msg.ID, messageOptions...)
if err == nil {
return true, nil
@ -427,11 +429,6 @@ func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (s
return "", nil
}
messageOptions := b.prepareMessageOptions(msg)
messageOptions = append(
messageOptions,
slack.MsgOptionText(msg.Text, false),
slack.MsgOptionEnableLinkUnfurl(),
)
for {
_, id, err := b.rtm.PostMessage(channelInfo.ID, messageOptions...)
if err == nil {
@ -446,7 +443,8 @@ func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (s
}
// uploadFile handles native upload of files
func (b *Bslack) uploadFile(msg *config.Message, channelID string) {
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 {
@ -463,7 +461,7 @@ func (b *Bslack) uploadFile(msg *config.Message, channelID 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)
initialComment += fmt.Sprintf(" with comment: %s", fi.Comment)
}
res, err := b.sc.UploadFile(slack.FileUploadParameters{
Reader: bytes.NewReader(*fi.Data),
@ -474,13 +472,22 @@ func (b *Bslack) uploadFile(msg *config.Message, channelID string) {
})
if err != nil {
b.Log.Errorf("uploadfile %#v", err)
return
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 {
@ -497,8 +504,6 @@ func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption {
}
var attachments []slack.Attachment
// add a callback ID so we can see we created it
attachments = append(attachments, slack.Attachment{CallbackID: "matterbridge_" + b.uuid})
// add file attachments
attachments = append(attachments, b.createAttach(msg.Extra)...)
// add slack attachments (from another slack bridge)
@ -509,6 +514,19 @@ func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption {
}
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

View File

@ -8,8 +8,8 @@ import (
"time"
"github.com/42wim/matterbridge/bridge/config"
"github.com/nlopes/slack"
"github.com/sirupsen/logrus"
"github.com/slack-go/slack"
)
const minimumRefreshInterval = 10 * time.Second
@ -113,6 +113,12 @@ func (b *users) populateUser(userID string) {
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) {
@ -283,8 +289,9 @@ func (b *channels) populateChannels(wait bool) {
// We only retrieve public and private channels, not IMs
// and MPIMs as those do not have a channel name.
queryParams := &slack.GetConversationsParameters{
ExcludeArchived: "true",
ExcludeArchived: true,
Types: []string{"public_channel,private_channel"},
Limit: 1000,
}
for {
channels, nextCursor, err := b.sc.GetConversations(queryParams)

View File

@ -1,22 +1,41 @@
package btelegram
import (
"fmt"
"html"
"regexp"
"path/filepath"
"strconv"
"strings"
"unicode/utf16"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
"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 {
message = posted
rmsg.Text = message.Text
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
@ -39,32 +58,54 @@ func (b *Btelegram) handleGroups(rmsg *config.Message, message *tgbotapi.Message
// handleForwarded handles forwarded messages
func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Message) {
if message.ForwardFrom != nil {
usernameForward := ""
if b.GetBool("UseFirstName") {
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 = message.ForwardFrom.UserName
if usernameForward == "" {
usernameForward = message.ForwardFrom.FirstName
}
}
if usernameForward == "" {
usernameForward = unknownUser
}
rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text
}
if usernameForward == "" {
usernameForward = unknownUser
}
rmsg.Text = "Forwarded from " + usernameForward + ": " + rmsg.Text
}
// handleQuoting handles quoting of previous messages
func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Message) {
if message.ReplyToMessage != nil {
// 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 == "" {
@ -76,7 +117,11 @@ func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Messag
usernameReply = unknownUser
}
if !b.GetBool("QuoteDisable") {
rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, message.ReplyToMessage.Text)
quote := message.ReplyToMessage.Text
if quote == "" {
quote = message.ReplyToMessage.Caption
}
rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, quote)
}
}
}
@ -84,10 +129,15 @@ func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Messag
// handleUsername handles the correct setting of the username
func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) {
if message.From != nil {
rmsg.UserID = strconv.Itoa(message.From.ID)
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 == "" {
@ -95,11 +145,40 @@ func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Messa
}
}
// only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" {
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
@ -112,10 +191,16 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
if update.Message == nil && update.ChannelPost == nil &&
update.EditedMessage == nil && update.EditedChannelPost == nil {
b.Log.Error("Getting nil messages, this shouldn't happen.")
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{})}
@ -134,6 +219,19 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
// set the ID's from the channel or group message
rmsg.ID = strconv.Itoa(message.MessageID)
rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)
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)
@ -150,14 +248,12 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
// quote the previous message
b.handleQuoting(&rmsg, message)
// handle entities (adding URLs)
b.handleEntities(&rmsg, message)
if rmsg.Text != "" || len(rmsg.Extra) > 0 {
rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
// 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.Itoa(message.From.ID), b.General)
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)
@ -167,54 +263,132 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
}
}
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 int, channel string) {
rmsg := config.Message{Username: "system",
Text: "avatar",
Channel: channel,
Account: b.Account,
UserID: strconv.Itoa(userid),
Event: config.EventAvatarDownload,
Extra: make(map[string][]interface{})}
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.Itoa(userid)]; !ok {
photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1})
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.Errorf("Userprofile download failed for %#v %s", userid, err)
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
}
}
if len(photos.Photos) > 0 {
photo := photos.Photos[0][0]
url := b.getFileDirectURL(photo.FileID)
name := strconv.Itoa(userid) + ".png"
b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)
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)
}
}
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) 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 := 0
size := int64(0)
var url, name, text string
switch {
case message.Sticker != nil:
text, name, url = b.getDownloadInfo(message.Sticker.FileID, ".webp", true)
size = message.Sticker.FileSize
size = int64(message.Sticker.FileSize)
case message.Voice != nil:
text, name, url = b.getDownloadInfo(message.Voice.FileID, ".ogg", true)
size = message.Voice.FileSize
@ -230,8 +404,8 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa
name = message.Document.FileName
text = " " + message.Document.FileName + " : " + url
case message.Photo != nil:
photos := *message.Photo
size = photos[len(photos)-1].FileSize
photos := message.Photo
size = int64(photos[len(photos)-1].FileSize)
text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true)
}
@ -254,15 +428,18 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa
if err != nil {
return err
}
if strings.HasSuffix(name, ".webp") && b.GetBool("MediaConvertWebPToPNG") {
b.Log.Debugf("WebP to PNG conversion enabled, converting %s", name)
err := helper.ConvertWebPToPNG(data)
if err != nil {
b.Log.Errorf("conversion failed: %s", err)
} else {
name = strings.Replace(name, ".webp", ".png", 1)
}
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
}
@ -274,7 +451,7 @@ func (b *Btelegram) getDownloadInfo(id string, suffix string, urlpart bool) (str
urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
}
if suffix != "" && !strings.HasSuffix(name, suffix) {
if suffix != "" && !strings.HasSuffix(name, suffix) && !strings.HasSuffix(name, ".webm") {
name += suffix
}
text := " " + url
@ -286,11 +463,15 @@ func (b *Btelegram) handleDelete(msg *config.Message, chatid int64) (string, err
if msg.ID == "" {
return "", nil
}
msgid, err := strconv.Atoi(msg.ID)
if err != nil {
return "", err
}
_, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid})
cfg := tgbotapi.NewDeleteMessage(chatid, msgid)
_, err = b.c.Request(cfg)
return "", err
}
@ -312,6 +493,9 @@ func (b *Btelegram) handleEdit(msg *config.Message, chatid int64) (string, error
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")
@ -325,31 +509,57 @@ func (b *Btelegram) handleEdit(msg *config.Message, chatid int64) (string, error
}
// handleUploadFile handles native upload of files
func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64) string {
var c tgbotapi.Chattable
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,
}
re := regexp.MustCompile(".(jpg|png)$")
if re.MatchString(fi.Name) {
c = tgbotapi.NewPhotoUpload(chatid, file)
} else {
c = tgbotapi.NewDocumentUpload(chatid, file)
if b.GetString("MessageFormat") == HTMLFormat {
fi.Comment = makeHTML(html.EscapeString(fi.Comment))
}
_, err := b.c.Send(c)
if err != nil {
b.Log.Errorf("file upload failed: %#v", err)
}
if fi.Comment != "" {
if _, err := b.sendMessage(chatid, msg.Username, fi.Comment); err != nil {
b.Log.Errorf("posting file comment %s failed: %s", fi.Comment, err)
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 ""
return b.sendMediaFiles(msg, chatid, threadid, parentID, media)
}
func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string {
@ -357,7 +567,7 @@ func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string
if format == "" {
format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
}
quoteMessagelength := len(quoteMessage)
quoteMessagelength := len([]rune(quoteMessage))
if b.GetInt("QuoteLengthLimit") != 0 && quoteMessagelength >= b.GetInt("QuoteLengthLimit") {
runes := []rune(quoteMessage)
quoteMessage = string(runes[0:b.GetInt("QuoteLengthLimit")])
@ -376,21 +586,61 @@ func (b *Btelegram) handleEntities(rmsg *config.Message, message *tgbotapi.Messa
if message.Entities == nil {
return
}
// for now only do URL replacements
for _, e := range *message.Entities {
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 e.Offset+e.Length > len(utfEncodedString) {
b.Log.Errorf("entity length is too long %d > %d", e.Offset+e.Length, len(utfEncodedString))
if offset+e.Length > len(utfEncodedString) {
b.Log.Errorf("entity length is too long %d > %d", offset+e.Length, len(utfEncodedString))
continue
}
link := utf16.Decode(utfEncodedString[e.Offset : e.Offset+e.Length])
rmsg.Text = strings.Replace(rmsg.Text, string(link), url.String(), 1)
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
}
}
}

View File

@ -2,7 +2,6 @@ package btelegram
import (
"bytes"
"html"
"github.com/russross/blackfriday"
)
@ -24,10 +23,16 @@ func (options *customHTML) Paragraph(out *bytes.Buffer, text func() bool) {
func (options *customHTML) BlockCode(out *bytes.Buffer, text []byte, lang string) {
out.WriteString("<pre>")
out.WriteString(html.EscapeString(string(text)))
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)
}
@ -42,6 +47,10 @@ func (options *customHTML) BlockQuote(out *bytes.Buffer, text []byte) {
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)
}

View File

@ -1,20 +1,23 @@
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/go-telegram-bot-api/telegram-bot-api"
tgbotapi "github.com/matterbridge/telegram-bot-api/v6"
)
const (
unknownUser = "unknown"
HTMLFormat = "HTML"
HTMLNick = "htmlnick"
MarkdownV2 = "MarkdownV2"
)
type Btelegram struct {
@ -24,6 +27,16 @@ type Btelegram struct {
}
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)}
}
@ -37,11 +50,7 @@ func (b *Btelegram) Connect() error {
}
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates, err := b.c.GetUpdatesChan(u)
if err != nil {
b.Log.Debugf("%#v", err)
return err
}
updates := b.c.GetUpdatesChan(u)
b.Log.Info("Connection succeeded")
go b.handleRecv(updates)
return nil
@ -55,11 +64,63 @@ 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)
// get the chatid
chatid, err := strconv.ParseInt(msg.Channel, 10, 64)
chatid, topicid, err := b.getIds(msg.Channel)
if err != nil {
return "", err
}
@ -70,7 +131,7 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
}
if b.GetString("MessageFormat") == HTMLFormat {
msg.Text = makeHTML(msg.Text)
msg.Text = makeHTML(html.EscapeString(msg.Text))
}
// Delete message
@ -78,16 +139,27 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
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, rmsg.Username, rmsg.Text); msgErr != nil {
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 {
b.handleUploadFile(&msg, chatid)
return b.handleUploadFile(&msg, chatid, topicid, parentID)
}
}
@ -101,7 +173,7 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
// 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, msg.Username, msg.Text)
return b.sendMessage(chatid, topicid, msg.Username, msg.Text, parentID)
}
return "", nil
@ -115,23 +187,13 @@ func (b *Btelegram) getFileDirectURL(id string) string {
return res
}
func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, error) {
func (b *Btelegram) sendMessage(chatid int64, topicid int, username, text string, parentID int) (string, error) {
m := tgbotapi.NewMessage(chatid, "")
m.Text = username + text
if b.GetString("MessageFormat") == HTMLFormat {
b.Log.Debug("Using mode HTML")
m.ParseMode = tgbotapi.ModeHTML
m.Text, m.ParseMode = TGGetParseMode(b, username, text)
if topicid != 0 {
m.BaseChat.MessageThreadID = topicid
}
if b.GetString("MessageFormat") == "Markdown" {
b.Log.Debug("Using mode markdown")
m.ParseMode = tgbotapi.ModeMarkdown
}
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
b.Log.Debug("Using mode HTML - nick only")
m.Text = username + html.EscapeString(text)
m.ParseMode = tgbotapi.ModeHTML
}
m.ReplyToMessageID = parentID
m.DisableWebPagePreview = b.GetBool("DisableWebPagePreview")
res, err := b.c.Send(m)
@ -141,6 +203,37 @@ func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, er
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,

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

View File

@ -1,3 +1,4 @@
// nolint:goconst
package bwhatsapp
import (
@ -9,6 +10,7 @@ import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/Rhymen/go-whatsapp"
"github.com/jpillora/backoff"
)
/*
@ -22,15 +24,55 @@ Check:
// HandleError received from WhatsApp
func (b *Bwhatsapp) HandleError(err error) {
// ignore received invalid data errors. https://github.com/42wim/matterbridge/issues/843
if strings.Contains(err.Error(), "error processing data: received invalid data") {
// 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
}
b.Log.Errorf("%v", err) // TODO implement proper handling? at least respond to different error types
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 { // || !strings.Contains(strings.ToLower(message.Text), "@echo") {
if message.Info.FromMe {
return
}
// whatsapp sends last messages to show context , cut them
@ -38,13 +80,13 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
return
}
messageTime := time.Unix(int64(message.Info.Timestamp), 0) // TODO check how behaves between timezones
groupJID := message.Info.RemoteJid
senderJID := message.Info.SenderJid
if len(senderJID) == 0 {
// TODO workaround till https://github.com/Rhymen/go-whatsapp/issues/86 resolved
senderJID = *message.Info.Source.Participant
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
@ -65,108 +107,276 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
if mention == "" {
mention = "someone"
}
message.Text = strings.Replace(message.Text, "@"+numberAndSuffix[0], "@"+mention, 1)
}
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
rmsg := config.Message{
UserID: senderJID,
Username: senderName,
Text: message.Text,
Timestamp: messageTime,
Channel: groupJID,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
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
// Event string `json:"event"`
// Gateway string // will be added during message processing
ID: message.Info.Id}
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 { // || !strings.Contains(strings.ToLower(message.Text), "@echo") {
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
return
}
// whatsapp sends last messages to show context , cut them
if message.Info.Timestamp < b.startedAt {
return
}
messageTime := time.Unix(int64(message.Info.Timestamp), 0) // TODO check how behaves between timezones
groupJID := message.Info.RemoteJid
senderJID := message.Info.SenderJid
// if len(senderJid) == 0 {
// // TODO workaround till https://github.com/Rhymen/go-whatsapp/issues/86 resolved
// senderJid = *message.Info.Source.Participant
// }
if len(message.Info.SenderJid) == 0 && 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)
senderName := b.getSenderName(message.Info.SenderJid)
if senderName == "" {
senderName = "Someone" // don't expose telephone number
}
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
rmsg := config.Message{
UserID: senderJID,
Username: senderName,
Timestamp: messageTime,
Channel: groupJID,
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
// ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string
// Event string `json:"event"`
// Gateway string // will be added during message processing
ID: message.Info.Id}
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
}
// Download and unencrypt content
data, err := message.Download()
fileExt, err := mime.ExtensionsByType(message.Type)
if err != nil {
b.Log.Errorf("%v", err)
b.Log.Errorf("Mimetype detection error: %s", err)
return
}
// Get file extension by mimetype
fileExt, err := mime.ExtensionsByType(message.Type)
if err != nil {
b.Log.Errorf("%v", 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("<= Image downloaded and unencrypted")
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("<= Image Message is %#v", rmsg)
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) HandleVideoMessage(message whatsapp.VideoMessage) {
// fmt.Println(message) // TODO implement
//}
//
//func (b *Bwhatsapp) HandleJsonMessage(message string) {
// fmt.Println(message) // TODO implement
//}
// TODO HandleRawMessage
// TODO HandleAudioMessage
// 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
}

View File

@ -6,22 +6,24 @@ import (
"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"`
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)
}
@ -44,13 +46,12 @@ func (b *Bwhatsapp) readSession() (whatsapp.Session, error) {
if err != nil {
return session, err
}
defer file.Close()
decoder := gob.NewDecoder(file)
err = decoder.Decode(&session)
if err != nil {
return session, err
}
return session, nil
return session, decoder.Decode(&session)
}
func (b *Bwhatsapp) writeSession(session whatsapp.Session) error {
@ -65,11 +66,31 @@ func (b *Bwhatsapp) writeSession(session whatsapp.Session) error {
if err != nil {
return err
}
defer file.Close()
encoder := gob.NewEncoder(file)
err = encoder.Encode(session)
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 {
@ -80,8 +101,32 @@ func (b *Bwhatsapp) getSenderName(senderJid string) string {
// 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
return sender.Notify
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 ""
}
@ -89,6 +134,7 @@ func (b *Bwhatsapp) getSenderNotify(senderJid string) string {
if sender, exists := b.users[senderJid]; exists {
return sender.Notify
}
return ""
}
@ -97,11 +143,20 @@ func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*ProfilePicInfo, error) {
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")
}

View File

@ -28,7 +28,6 @@ const (
type Bwhatsapp struct {
*bridge.Config
// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L18-L21
session *whatsapp.Session
conn *whatsapp.Conn
startedAt uint64
@ -40,6 +39,12 @@ type Bwhatsapp struct {
// 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")
}
@ -50,21 +55,17 @@ func New(cfg *bridge.Config) bridge.Bridger {
users: make(map[string]whatsapp.Contact),
userAvatars: make(map[string]string),
}
return b
}
// Connect to WhatsApp. Required implementation of the Bridger interface
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
func (b *Bwhatsapp) Connect() error {
b.RLock() // TODO do we need locking for Whatsapp?
defer b.RUnlock()
number := b.GetString(cfgNumber)
if number == "" {
return errors.New("WhatsApp's telephone Number need to be configured")
return errors.New("whatsapp's telephone number need to be configured")
}
// https://github.com/Rhymen/go-whatsapp#creating-a-connection
b.Log.Debugln("Connecting to WhatsApp..")
conn, err := whatsapp.NewConn(20 * time.Second)
if err != nil {
@ -77,35 +78,18 @@ func (b *Bwhatsapp) Connect() error {
b.Log.Debugln("WhatsApp connection successful")
// load existing session in order to keep it between restarts
if b.session == nil {
var session whatsapp.Session
session, err = b.readSession()
if err == nil {
b.Log.Debugln("Restoring WhatsApp session..")
// https://github.com/Rhymen/go-whatsapp#restore
session, err = b.conn.RestoreWithSession(session)
if err != nil {
// TODO return or continue to normal login?
// restore session connection timed out (I couldn't get over it without logging in again)
return errors.New("failed to restore session: " + err.Error())
}
b.session = &session
b.Log.Debugln("Session restored successfully!")
} else {
b.Log.Warn(err.Error())
}
b.session, err = b.restoreSession()
if err != nil {
b.Log.Warn(err.Error())
}
// login to a new session
if b.session == nil {
err = b.Login()
if err != nil {
if err = b.Login(); err != nil {
return err
}
}
b.startedAt = uint64(time.Now().Unix())
_, err = b.conn.Contacts()
@ -113,6 +97,13 @@ func (b *Bwhatsapp) Connect() error {
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" {
@ -129,12 +120,13 @@ func (b *Bwhatsapp) Connect() error {
info, err := b.GetProfilePicThumb(jid)
if err != nil {
b.Log.Warnf("Could not get profile photo of %s: %v", jid, err)
} else {
// TODO any race conditions here?
b.Lock()
b.userAvatars[jid] = info.URL
b.Unlock()
}
}
b.Log.Debug("Finished getting avatars..")
}()
@ -151,8 +143,10 @@ func (b *Bwhatsapp) Login() error {
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)
@ -163,74 +157,62 @@ func (b *Bwhatsapp) Login() error {
fmt.Fprintf(os.Stderr, "error saving session: %v\n", err)
}
// TODO change connection strings to configured ones longClientName:"github.com/rhymen/go-whatsapp", shortClientName:"go-whatsapp"}" prefix=whatsapp
// TODO get also a nice logo
// TODO notification about unplugged and dead battery
// conn.Info: Wid, Pushname, Connected, Battery, Plugged
return nil
}
// Disconnect is called while reconnecting to the bridge
// TODO 42wim Documentation would be helpful on when reconnects happen and what should be done in this function
// Required implementation of the Bridger interface
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
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
}
func isGroupJid(identifier string) bool {
return strings.HasSuffix(identifier, "@g.us") || strings.HasSuffix(identifier, "@temp")
}
// 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)
}
} else {
// 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
// TODO sort
// copy b;
//sort.Slice(people, func(i, j int) bool {
// return people[i].Age > people[j].Age
//})
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)
return nil
}
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)
// 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)
}
}
return nil
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
@ -304,22 +286,23 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
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
// TODO 42wim Doesn't the app get clogged with a ton of IDs after some time of running?
// WhatsApp allows to set any ID so in that case we could use external IDs and don't do mapping
// but external IDs are not set
return "", nil
}
// TODO delete message on WhatsApp https://github.com/Rhymen/go-whatsapp/issues/100
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)
msg.Text += " (edited)"
// TODO handle edit as a message reply with updated text
if b.GetString("editsuffix") != "" {
msg.Text += b.GetString("EditSuffix")
} else {
msg.Text += " (edited)"
}
}
// Handle Upload a file
@ -349,16 +332,7 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
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
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

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

View File

@ -1,8 +1,12 @@
package bxmpp
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strings"
"sync"
"time"
@ -10,6 +14,7 @@ import (
"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"
@ -23,12 +28,24 @@ type Bxmpp struct {
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,
xmppMap: make(map[string]string),
Config: cfg,
StanzaIDs: stanzaIDs,
OriginIDs: originIDs,
xmppMap: make(map[string]string),
avatarAvailability: make(map[string]bool),
avatarMap: make(map[string]string),
}
}
@ -67,17 +84,35 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
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 _, err := b.xc.Send(xmpp.Chat{
Type: "groupchat",
Remote: rmsg.Channel + "@" + b.GetString("Muc"),
Text: rmsg.Username + rmsg.Text,
}); err != nil {
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.")
}
}
@ -86,13 +121,31 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
}
}
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 != "" {
msgID = msg.ID
msgReplaceID = msg.ID
}
// Post normal message.
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",
@ -100,34 +153,71 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
Text: msg.Username + msg.Text,
ID: msgID,
ReplaceID: msgReplaceID,
ReplyID: replyID,
}); err != nil {
return "", err
}
return msgID, nil
}
func (b *Bxmpp) createXMPP() error {
if !strings.Contains(b.GetString("Jid"), "@") {
return fmt.Errorf("the Jid %s doesn't contain an @", b.GetString("Jid"))
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: strings.Split(b.GetString("Jid"), "@")[1],
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: true,
StartTLS: !b.GetBool("NoTLS"),
TLSConfig: tc,
Debug: b.GetBool("debug"),
Logger: b.Log.Writer(),
Session: true,
Status: "",
StatusMessage: "",
Resource: "",
InsecureAllowUnencryptedAuth: false,
InsecureAllowUnencryptedAuth: b.GetBool("NoTLS"),
}
var err error
b.xc, err = options.NewClient()
@ -210,7 +300,13 @@ func (b *Bxmpp) handleXMPP() error {
for {
m, err := b.xc.Recv()
if err != nil {
return err
// An error together with AvatarData is non-fatal
switch m.(type) {
case xmpp.AvatarData:
continue
default:
return err
}
}
switch v := m.(type) {
@ -218,6 +314,11 @@ func (b *Bxmpp) handleXMPP() error {
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
@ -228,16 +329,34 @@ func (b *Bxmpp) handleXMPP() error {
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,
}
@ -253,6 +372,10 @@ func (b *Bxmpp) handleXMPP() error {
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.
}
@ -306,7 +429,7 @@ func (b *Bxmpp) handleUploadFile(msg *config.Message) error {
func (b *Bxmpp) parseNick(remote string) string {
s := strings.Split(remote, "@")
if len(s) > 0 {
if len(s) > 1 {
s = strings.Split(s[1], "/")
if len(s) == 2 {
return s[1] // nick
@ -345,6 +468,11 @@ func (b *Bxmpp) skipMessage(message xmpp.Chat) bool {
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
}

View File

@ -2,6 +2,7 @@ package bzulip
import (
"encoding/json"
"fmt"
"io/ioutil"
"strconv"
"strings"
@ -11,6 +12,7 @@ import (
"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"
)
@ -27,7 +29,7 @@ func New(cfg *bridge.Config) bridge.Bridger {
}
func (b *Bzulip) Connect() error {
bot := gzb.Bot{APIKey: b.GetString("token"), APIURL: b.GetString("server") + "/api/v1/", Email: b.GetString("login")}
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
@ -104,29 +106,31 @@ func (b *Bzulip) getChannel(id int) string {
func (b *Bzulip) handleQueue() error {
for {
messages, err := b.q.GetEvents()
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)
}
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 {
@ -135,19 +139,25 @@ func (b *Bzulip) handleQueue() error {
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: m.AvatarURL,
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
b.q.LastEventID = m.ID
}
time.Sleep(time.Second * 3)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +0,0 @@
#!/usr/bin/env bash
set -u -e -x -o pipefail
go version | grep go1.13 || exit
VERSION=$(git describe --tags)
mkdir ci/binaries
GOOS=windows GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-windows-amd64.exe
GOOS=linux GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-linux-amd64
GOOS=linux GOARCH=arm go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-linux-arm
GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o ci/binaries/matterbridge-$VERSION-darwin-amd64
cd ci
cat > deploy.json <<EOF
{
"package": {
"name": "Matterbridge",
"repo": "nightly",
"subject": "42wim"
},
"version": {
"name": "$VERSION"
},
"files":
[
{"includePattern": "ci/binaries/(.*)", "uploadPattern":"\$1"}
],
"publish": true
}
EOF

View File

@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -u -e -x -o pipefail
if [[ -n "${GOLANGCI_VERSION-}" ]]; then
# Retrieve the golangci-lint linter binary.
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | bash -s -- -b ${GOPATH}/bin ${GOLANGCI_VERSION}
fi
# Run the linter.
golangci-lint run
# if [[ "${GO111MODULE-off}" == "on" ]]; then
# # If Go modules are active then check that dependencies are correctly maintained.
# go mod tidy
# go mod vendor
# git diff --exit-code --quiet || (echo "Please run 'go mod tidy' to clean up the 'go.mod' and 'go.sum' files."; false)
# fi

View File

@ -1,17 +0,0 @@
#!/usr/bin/env bash
set -u -e -x -o pipefail
if [[ -n "${REPORT_COVERAGE+cover}" ]]; then
# Retrieve and prepare CodeClimate's test coverage reporter.
curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
chmod +x ./cc-test-reporter
./cc-test-reporter before-build
fi
# Run all the tests with the race detector and generate coverage.
go test -v -race -coverprofile c.out ./...
if [[ -n "${REPORT_COVERAGE+cover}" && "${TRAVIS_SECURE_ENV_VARS}" == "true" ]]; then
# Upload test coverage to CodeClimate.
./cc-test-reporter after-build
fi

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,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

@ -1,9 +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
ARG VERSION=1.12.3
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
ADD https://github.com/42wim/matterbridge/releases/download/v${VERSION}/matterbridge-linux-arm /bin/matterbridge
RUN chmod +x /bin/matterbridge
COPY --from=certs /bin/matterbridge /bin/matterbridge
ENTRYPOINT ["/bin/matterbridge"]

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

@ -2,45 +2,9 @@ package bridgemap
import (
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/api"
bdiscord "github.com/42wim/matterbridge/bridge/discord"
bgitter "github.com/42wim/matterbridge/bridge/gitter"
birc "github.com/42wim/matterbridge/bridge/irc"
bkeybase "github.com/42wim/matterbridge/bridge/keybase"
bmatrix "github.com/42wim/matterbridge/bridge/matrix"
bmattermost "github.com/42wim/matterbridge/bridge/mattermost"
brocketchat "github.com/42wim/matterbridge/bridge/rocketchat"
bslack "github.com/42wim/matterbridge/bridge/slack"
bsshchat "github.com/42wim/matterbridge/bridge/sshchat"
bsteam "github.com/42wim/matterbridge/bridge/steam"
btelegram "github.com/42wim/matterbridge/bridge/telegram"
bwhatsapp "github.com/42wim/matterbridge/bridge/whatsapp"
bxmpp "github.com/42wim/matterbridge/bridge/xmpp"
bzulip "github.com/42wim/matterbridge/bridge/zulip"
)
var (
FullMap = map[string]bridge.Factory{
"api": api.New,
"discord": bdiscord.New,
"gitter": bgitter.New,
"irc": birc.New,
"mattermost": bmattermost.New,
"matrix": bmatrix.New,
"rocketchat": brocketchat.New,
"slack-legacy": bslack.NewLegacy,
"slack": bslack.New,
"sshchat": bsshchat.New,
"steam": bsteam.New,
"telegram": btelegram.New,
"whatsapp": bwhatsapp.New,
"xmpp": bxmpp.New,
"zulip": bzulip.New,
"keybase": bkeybase.New,
}
UserTypingSupport = map[string]struct{}{
"slack": {},
"discord": {},
}
FullMap = map[string]bridge.Factory{}
UserTypingSupport = map[string]struct{}{}
)

View File

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

View File

@ -0,0 +1,13 @@
// +build !noslack
package bridgemap
import (
bslack "github.com/42wim/matterbridge/bridge/slack"
)
func init() {
FullMap["slack-legacy"] = bslack.NewLegacy
FullMap["slack"] = bslack.New
UserTypingSupport["slack"] = struct{}{}
}

View File

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

View File

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

View File

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

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

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

View File

@ -0,0 +1,12 @@
// +build !nowhatsapp
// +build !whatsappmulti
package bridgemap
import (
bwhatsapp "github.com/42wim/matterbridge/bridge/whatsapp"
)
func init() {
FullMap["whatsapp"] = bwhatsapp.New
}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package gateway
import (
"fmt"
"io/ioutil"
"os"
"regexp"
@ -13,7 +14,7 @@ import (
"github.com/d5/tengo/v2"
"github.com/d5/tengo/v2/stdlib"
lru "github.com/hashicorp/golang-lru"
"github.com/matterbridge/emoji"
"github.com/kyokomi/emoji/v2"
"github.com/sirupsen/logrus"
)
@ -65,7 +66,7 @@ func New(rootLogger *logrus.Logger, cfg *config.Gateway, r *Router) *Gateway {
func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string {
ID := protocol + " " + mID
if gw.Messages.Contains(ID) {
return mID
return ID
}
// If not keyed, iterate through cache for downstream, and infer upstream.
@ -74,7 +75,7 @@ func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string {
ids := v.([]*BrMsgID)
for _, downstreamMsgObj := range ids {
if ID == downstreamMsgObj.ID {
return strings.Replace(mid.(string), protocol+" ", "", 1)
return mid.(string)
}
}
}
@ -108,7 +109,7 @@ func (gw *Gateway) AddBridge(cfg *config.Bridge) error {
func (gw *Gateway) checkConfig(cfg *config.Bridge) {
match := false
for _, key := range gw.Router.Config.Viper().AllKeys() {
if strings.HasPrefix(key, cfg.Account) {
if strings.HasPrefix(key, strings.ToLower(cfg.Account)) {
match = true
break
}
@ -126,7 +127,7 @@ func (gw *Gateway) AddConfig(cfg *config.Gateway) error {
gw.logger.Errorf("mapChannels() failed: %s", err)
}
for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) {
br := br //scopelint
br := br // scopelint
err := gw.AddBridge(&br)
if err != nil {
return err
@ -298,16 +299,31 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
igNicks := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreNicks"))
igMessages := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreMessages"))
if gw.ignoreTextEmpty(msg) || gw.ignoreText(msg.Username, igNicks) || gw.ignoreText(msg.Text, igMessages) {
if gw.ignoreTextEmpty(msg) || gw.ignoreText(msg.Username, igNicks) || gw.ignoreText(msg.Text, igMessages) || gw.ignoreFilesComment(msg.Extra, igMessages) {
return true
}
return false
}
// ignoreFilesComment returns true if we need to ignore a file with matched comment.
func (gw *Gateway) ignoreFilesComment(extra map[string][]interface{}, igMessages []string) bool {
if extra == nil {
return false
}
for _, f := range extra["file"] {
fi, ok := f.(config.FileInfo)
if !ok {
continue
}
if gw.ignoreText(fi.Comment, igMessages) {
return true
}
}
return false
}
func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) string {
br := gw.Bridges[msg.Account]
msg.Protocol = br.Protocol
if dest.GetBool("StripNick") {
re := regexp.MustCompile("[^a-zA-Z0-9]+")
msg.Username = re.ReplaceAllString(msg.Username, "")
@ -315,6 +331,7 @@ func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) stri
nick := dest.GetString("RemoteNickFormat")
// loop to replace nicks
br := gw.Bridges[msg.Account]
for _, outer := range br.GetStringSlice2D("ReplaceNicks") {
search := outer[0]
replace := outer[1]
@ -337,20 +354,21 @@ func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) stri
}
i++
}
nick = strings.Replace(nick, "{NOPINGNICK}", msg.Username[:i]+""+msg.Username[i:], -1)
nick = strings.ReplaceAll(nick, "{NOPINGNICK}", msg.Username[:i]+"\u200b"+msg.Username[i:])
}
nick = strings.Replace(nick, "{BRIDGE}", br.Name, -1)
nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1)
nick = strings.Replace(nick, "{GATEWAY}", gw.Name, -1)
nick = strings.Replace(nick, "{LABEL}", br.GetString("Label"), -1)
nick = strings.Replace(nick, "{NICK}", msg.Username, -1)
nick = strings.Replace(nick, "{CHANNEL}", msg.Channel, -1)
nick = strings.ReplaceAll(nick, "{BRIDGE}", br.Name)
nick = strings.ReplaceAll(nick, "{PROTOCOL}", br.Protocol)
nick = strings.ReplaceAll(nick, "{GATEWAY}", gw.Name)
nick = strings.ReplaceAll(nick, "{LABEL}", br.GetString("Label"))
nick = strings.ReplaceAll(nick, "{NICK}", msg.Username)
nick = strings.ReplaceAll(nick, "{USERID}", msg.UserID)
nick = strings.ReplaceAll(nick, "{CHANNEL}", msg.Channel)
tengoNick, err := gw.modifyUsernameTengo(msg, br)
if err != nil {
gw.logger.Errorf("modifyUsernameTengo error: %s", err)
}
nick = strings.Replace(nick, "{TENGO}", tengoNick, -1) //nolint:gocritic
nick = strings.ReplaceAll(nick, "{TENGO}", tengoNick)
return nick
}
@ -364,14 +382,28 @@ func (gw *Gateway) modifyAvatar(msg *config.Message, dest *bridge.Bridge) string
}
func (gw *Gateway) modifyMessage(msg *config.Message) {
if err := modifyMessageTengo(gw.BridgeValues().General.TengoModifyMessage, msg); err != nil {
if gw.BridgeValues().General.TengoModifyMessage != "" {
gw.logger.Warnf("General TengoModifyMessage=%s is deprecated and will be removed in v1.20.0, please move to Tengo InMessage=%s", gw.BridgeValues().General.TengoModifyMessage, gw.BridgeValues().General.TengoModifyMessage)
}
if err := modifyInMessageTengo(gw.BridgeValues().General.TengoModifyMessage, msg); err != nil {
gw.logger.Errorf("TengoModifyMessage failed: %s", err)
}
if err := modifyMessageTengo(gw.BridgeValues().Tengo.Message, msg); err != nil {
inMessage := gw.BridgeValues().Tengo.InMessage
if inMessage == "" {
inMessage = gw.BridgeValues().Tengo.Message
if inMessage != "" {
gw.logger.Warnf("Tengo Message=%s is deprecated and will be removed in v1.20.0, please move to Tengo InMessage=%s", inMessage, inMessage)
}
}
if err := modifyInMessageTengo(inMessage, msg); err != nil {
gw.logger.Errorf("Tengo.Message failed: %s", err)
}
// replace :emoji: to unicode
emoji.ReplacePadding = ""
msg.Text = emoji.Sprint(msg.Text)
br := gw.Bridges[msg.Account]
@ -417,44 +449,65 @@ func (gw *Gateway) SendMessage(
}
}
// Only send irc notices to irc
if msg.Event == config.EventNoticeIRC && dest.Protocol != "irc" {
return "", nil
}
// Too noisy to log like other events
debugSendMessage := ""
if msg.Event != config.EventUserTyping {
gw.logger.Debugf("=> Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, rmsg.Channel, dest.Account, channel.Name)
debugSendMessage = fmt.Sprintf("=> Sending %#v from %s (%s) to %s (%s)", msg, msg.Account, rmsg.Channel, dest.Account, channel.Name)
}
msg.Channel = channel.Name
msg.Avatar = gw.modifyAvatar(rmsg, dest)
msg.Username = gw.modifyUsername(rmsg, dest)
msg.ID = gw.getDestMsgID(rmsg.Protocol+" "+rmsg.ID, dest, channel)
// exclude file delete event as the msg ID here is the native file ID that needs to be deleted
if msg.Event != config.EventFileDelete {
msg.ID = gw.getDestMsgID(rmsg.Protocol+" "+rmsg.ID, dest, channel)
}
// for api we need originchannel as channel
if dest.Protocol == apiProtocol {
msg.Channel = rmsg.Channel
}
msg.ParentID = gw.getDestMsgID(rmsg.Protocol+" "+canonicalParentMsgID, dest, channel)
msg.ParentID = gw.getDestMsgID(canonicalParentMsgID, dest, channel)
if msg.ParentID == "" {
msg.ParentID = canonicalParentMsgID
msg.ParentID = strings.Replace(canonicalParentMsgID, dest.Protocol+" ", "", 1)
}
// if the parentID is still empty and we have a parentID set in the original message
// this means that we didn't find it in the cache so set it "msg-parent-not-found"
// this means that we didn't find it in the cache so set it to a "msg-parent-not-found" constant
if msg.ParentID == "" && rmsg.ParentID != "" {
msg.ParentID = "msg-parent-not-found"
msg.ParentID = config.ParentIDNotFound
}
err := gw.modifySendMessageTengo(rmsg, &msg, dest)
drop, err := gw.modifyOutMessageTengo(rmsg, &msg, dest)
if err != nil {
gw.logger.Errorf("modifySendMessageTengo: %s", err)
}
if drop {
gw.logger.Debugf("=> Tengo dropping %#v from %s (%s) to %s (%s)", msg, msg.Account, rmsg.Channel, dest.Account, channel.Name)
return "", nil
}
if debugSendMessage != "" {
gw.logger.Debug(debugSendMessage)
}
// if we are using mattermost plugin account, send messages to MattermostPlugin channel
// that can be picked up by the mattermost matterbridge plugin
if dest.Account == "mattermost.plugin" {
gw.Router.MattermostPlugin <- msg
}
defer func(t time.Time) {
gw.logger.Debugf("=> Send from %s (%s) to %s (%s) took %s", msg.Account, rmsg.Channel, dest.Account, channel.Name, time.Since(t))
}(time.Now())
mID, err := dest.Send(msg)
if err != nil {
return mID, err
@ -464,7 +517,7 @@ func (gw *Gateway) SendMessage(
if mID != "" {
gw.logger.Debugf("mID %s: %s", dest.Account, mID)
return mID, nil
//brMsgIDs = append(brMsgIDs, &BrMsgID{dest, dest.Protocol + " " + mID, channel.ID})
// brMsgIDs = append(brMsgIDs, &BrMsgID{dest, dest.Protocol + " " + mID, channel.ID})
}
return "", nil
}
@ -506,7 +559,7 @@ func getProtocol(msg *config.Message) string {
return p[0]
}
func modifyMessageTengo(filename string, msg *config.Message) error {
func modifyInMessageTengo(filename string, msg *config.Message) error {
if filename == "" {
return nil
}
@ -518,6 +571,7 @@ func modifyMessageTengo(filename string, msg *config.Message) error {
s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...))
_ = s.Add("msgText", msg.Text)
_ = s.Add("msgUsername", msg.Username)
_ = s.Add("msgUserID", msg.UserID)
_ = s.Add("msgAccount", msg.Account)
_ = s.Add("msgChannel", msg.Channel)
c, err := s.Compile()
@ -546,6 +600,7 @@ func (gw *Gateway) modifyUsernameTengo(msg *config.Message, br *bridge.Bridge) (
_ = s.Add("result", "")
_ = s.Add("msgText", msg.Text)
_ = s.Add("msgUsername", msg.Username)
_ = s.Add("msgUserID", msg.UserID)
_ = s.Add("nick", msg.Username)
_ = s.Add("msgAccount", msg.Account)
_ = s.Add("msgChannel", msg.Channel)
@ -565,22 +620,28 @@ func (gw *Gateway) modifyUsernameTengo(msg *config.Message, br *bridge.Bridge) (
return c.Get("result").String(), nil
}
func (gw *Gateway) modifySendMessageTengo(origmsg *config.Message, msg *config.Message, br *bridge.Bridge) error {
func (gw *Gateway) modifyOutMessageTengo(origmsg *config.Message, msg *config.Message, br *bridge.Bridge) (bool, error) {
filename := gw.BridgeValues().Tengo.OutMessage
var res []byte
var err error
var (
res []byte
err error
drop bool
)
if filename == "" {
res, err = internal.Asset("tengo/outmessage.tengo")
if err != nil {
return err
return drop, err
}
} else {
res, err = ioutil.ReadFile(filename)
if err != nil {
return err
return drop, err
}
}
s := tengo.NewScript(res)
s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...))
_ = s.Add("inAccount", origmsg.Account)
_ = s.Add("inProtocol", origmsg.Protocol)
@ -594,14 +655,20 @@ func (gw *Gateway) modifySendMessageTengo(origmsg *config.Message, msg *config.M
_ = s.Add("outEvent", msg.Event)
_ = s.Add("msgText", msg.Text)
_ = s.Add("msgUsername", msg.Username)
_ = s.Add("msgUserID", msg.UserID)
_ = s.Add("msgDrop", drop)
c, err := s.Compile()
if err != nil {
return err
return drop, err
}
if err := c.Run(); err != nil {
return err
return drop, err
}
drop = c.Get("msgDrop").Bool()
msg.Text = c.Get("msgText").String()
msg.Username = c.Get("msgUsername").String()
return nil
return drop, nil
}

View File

@ -18,8 +18,6 @@ var testconfig = []byte(`
server=""
[mattermost.test]
server=""
[gitter.42wim]
server=""
[discord.test]
server=""
[slack.test]
@ -33,11 +31,6 @@ server=""
account = "irc.freenode"
channel = "#wimtesting"
[[gateway.inout]]
account="gitter.42wim"
channel="42wim/testroom"
#channel="matterbridge/Lobby"
[[gateway.inout]]
account = "discord.test"
channel = "general"
@ -52,8 +45,6 @@ var testconfig2 = []byte(`
server=""
[mattermost.test]
server=""
[gitter.42wim]
server=""
[discord.test]
server=""
[slack.test]
@ -67,10 +58,6 @@ server=""
account = "irc.freenode"
channel = "#wimtesting"
[[gateway.in]]
account="gitter.42wim"
channel="42wim/testroom"
[[gateway.inout]]
account = "discord.test"
channel = "general"
@ -86,10 +73,6 @@ server=""
account = "irc.freenode"
channel = "#wimtesting2"
[[gateway.out]]
account="gitter.42wim"
channel="42wim/testroom"
[[gateway.out]]
account = "discord.test"
channel = "general2"
@ -184,31 +167,18 @@ func maketestRouter(input []byte) *Router {
}
return r
}
func TestNewRouter(t *testing.T) {
r := maketestRouter(testconfig)
assert.Equal(t, 1, len(r.Gateways))
assert.Equal(t, 4, len(r.Gateways["bridge1"].Bridges))
assert.Equal(t, 4, len(r.Gateways["bridge1"].Channels))
assert.Equal(t, 3, len(r.Gateways["bridge1"].Bridges))
assert.Equal(t, 3, len(r.Gateways["bridge1"].Channels))
r = maketestRouter(testconfig2)
assert.Equal(t, 2, len(r.Gateways))
assert.Equal(t, 4, len(r.Gateways["bridge1"].Bridges))
assert.Equal(t, 3, len(r.Gateways["bridge2"].Bridges))
assert.Equal(t, 4, len(r.Gateways["bridge1"].Channels))
assert.Equal(t, 3, len(r.Gateways["bridge2"].Channels))
assert.Equal(t, &config.ChannelInfo{
Name: "42wim/testroom",
Direction: "out",
ID: "42wim/testroomgitter.42wim",
Account: "gitter.42wim",
SameChannel: map[string]bool{"bridge2": false},
}, r.Gateways["bridge2"].Channels["42wim/testroomgitter.42wim"])
assert.Equal(t, &config.ChannelInfo{
Name: "42wim/testroom",
Direction: "in",
ID: "42wim/testroomgitter.42wim",
Account: "gitter.42wim",
SameChannel: map[string]bool{"bridge1": false},
}, r.Gateways["bridge1"].Channels["42wim/testroomgitter.42wim"])
assert.Equal(t, 3, len(r.Gateways["bridge1"].Bridges))
assert.Equal(t, 2, len(r.Gateways["bridge2"].Bridges))
assert.Equal(t, 3, len(r.Gateways["bridge1"].Channels))
assert.Equal(t, 2, len(r.Gateways["bridge2"].Channels))
assert.Equal(t, &config.ChannelInfo{
Name: "general",
Direction: "inout",
@ -241,8 +211,6 @@ func TestGetDestChannel(t *testing.T) {
SameChannel: map[string]bool{"bridge1": false},
Options: config.ChannelOptions{Key: ""},
}}, r.Gateways["bridge1"].getDestChannel(msg, *br))
case "gitter.42wim":
assert.Equal(t, []config.ChannelInfo(nil), r.Gateways["bridge1"].getDestChannel(msg, *br))
case "irc.freenode":
assert.Equal(t, []config.ChannelInfo(nil), r.Gateways["bridge1"].getDestChannel(msg, *br))
}
@ -420,6 +388,7 @@ func (s *ignoreTestSuite) SetupSuite() {
logger.SetOutput(ioutil.Discard)
s.gw = &Gateway{logger: logrus.NewEntry(logger)}
}
func (s *ignoreTestSuite) TestIgnoreTextEmpty() {
extraFile := make(map[string][]interface{})
extraAttach := make(map[string][]interface{})
@ -461,7 +430,6 @@ func (s *ignoreTestSuite) TestIgnoreTextEmpty() {
output := s.gw.ignoreTextEmpty(testcase.input)
s.Assert().Equalf(testcase.output, output, "case '%s' failed", testname)
}
}
func (s *ignoreTestSuite) TestIgnoreTexts() {
@ -533,7 +501,7 @@ func (s *ignoreTestSuite) TestIgnoreNicks() {
func BenchmarkTengo(b *testing.B) {
msg := &config.Message{Username: "user", Text: "blah testing", Account: "protocol.account", Channel: "mychannel"}
for n := 0; n < b.N; n++ {
err := modifyMessageTengo("bench.tengo", msg)
err := modifyInMessageTengo("bench.tengo", msg)
if err != nil {
return
}

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