Compare commits

...

327 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
4980 changed files with 6201271 additions and 153102 deletions

View File

@ -5,27 +5,28 @@ jobs:
name: golangci-lint name: golangci-lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v3
with: with:
fetch-depth: 20 fetch-depth: 20
- name: Run golangci-lint - name: Run golangci-lint
uses: golangci/golangci-lint-action@v2 uses: golangci/golangci-lint-action@v3
with: with:
version: v1.29 version: latest
args: "-v --new-from-rev HEAD~5" args: "-v --new-from-rev HEAD~5 --timeout=5m"
test-build-upload: test-build-upload:
strategy: strategy:
matrix: matrix:
go-version: [1.14.x, 1.15.x] go-version: [1.22.x]
platform: [ubuntu-latest] platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
- name: Install Go - name: Install Go
uses: actions/setup-go@v2 uses: actions/setup-go@v3
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
stable: false
- name: Checkout code - name: Checkout code
uses: actions/checkout@v2 uses: actions/checkout@v3
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Test - name: Test
@ -34,24 +35,24 @@ jobs:
run: | run: |
mkdir -p output/{win,lin,arm,mac} mkdir -p output/{win,lin,arm,mac}
VERSION=$(git describe --tags) VERSION=$(git describe --tags)
GOOS=linux GOARCH=amd64 go build -mod=vendor -ldflags "-s -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o output/lin/matterbridge-$VERSION-linux-amd64 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
GOOS=windows GOARCH=amd64 go build -mod=vendor -ldflags "-s -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o output/win/matterbridge-$VERSION-windows-amd64.exe 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
GOOS=darwin GOARCH=amd64 go build -mod=vendor -ldflags "-s -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o output/mac/matterbridge-$VERSION-darwin-amd64 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 - name: Upload linux 64-bit
if: startsWith(matrix.go-version,'1.15') if: startsWith(matrix.go-version,'1.22')
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: matterbridge-linux-64bit name: matterbridge-linux-64bit
path: output/lin path: output/lin
- name: Upload windows 64-bit - name: Upload windows 64-bit
if: startsWith(matrix.go-version,'1.15') if: startsWith(matrix.go-version,'1.22')
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: matterbridge-windows-64bit name: matterbridge-windows-64bit
path: output/win path: output/win
- name: Upload darwin 64-bit - name: Upload darwin 64-bit
if: startsWith(matrix.go-version,'1.15') if: startsWith(matrix.go-version,'1.22')
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v3
with: with:
name: matterbridge-darwin-64bit name: matterbridge-darwin-64bit
path: output/mac 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 # Exclude configuration file
matterbridge.toml matterbridge.toml
# Exclude IDE Files
.vscode

View File

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

View File

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

View File

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

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

134
README.md
View File

@ -58,20 +58,22 @@ And more...
- [Binaries](#binaries) - [Binaries](#binaries)
- [Packages](#packages) - [Packages](#packages)
- [Building](#building) - [Building](#building)
- [Building with whatsapp (beta) multidevice support](#building-with-whatsapp-beta-multidevice-support)
- [Configuration](#configuration) - [Configuration](#configuration)
- [Basic configuration](#basic-configuration) - [Basic configuration](#basic-configuration)
- [Settings](#settings) - [Settings](#settings)
- [Advanced configuration](#advanced-configuration) - [Advanced configuration](#advanced-configuration)
- [Examples](#examples) - [Examples](#examples)
- [Bridge mattermost (off-topic) - irc (#testing)](#bridge-mattermost-off-topic---irc-testing) - [Bridge mattermost (off-topic) - irc (#testing)](#bridge-mattermost-off-topic---irc-testing)
- [Bridge slack (#general) - discord (general)](#bridge-slack-general---discord-general) - [Bridge slack (#general) - discord (general)](#bridge-slack-general---discord-general)
- [Running](#running) - [Running](#running)
- [Docker](#docker) - [Docker](#docker)
- [Changelog](#changelog) - [Systemd](#systemd)
- [FAQ](#faq) - [Changelog](#changelog)
- [Related projects](#related-projects) - [FAQ](#faq)
- [Articles](#articles) - [Related projects](#related-projects)
- [Thanks](#thanks) - [Articles / Tutorials](#articles--tutorials)
- [Thanks](#thanks)
## Features ## Features
@ -88,31 +90,45 @@ And more...
- [Discord](https://discordapp.com) - [Discord](https://discordapp.com)
- [Gitter](https://gitter.im) - [Gitter](https://gitter.im)
- [Harmony](https://harmonyapp.io)
- [IRC](http://www.mirc.com/servers.html) - [IRC](http://www.mirc.com/servers.html)
- [Keybase](https://keybase.io) - [Keybase](https://keybase.io)
- [Matrix](https://matrix.org) - [Matrix](https://matrix.org)
- [Mattermost](https://github.com/mattermost/mattermost-server/) 4.x, 5.x - [Mattermost](https://github.com/mattermost/mattermost-server/)
- [Microsoft Teams](https://teams.microsoft.com) - [Microsoft Teams](https://teams.microsoft.com)
- [Mumble](https://www.mumble.info/) - [Mumble](https://www.mumble.info/)
- [Nextcloud Talk](https://nextcloud.com/talk/) - [Nextcloud Talk](https://nextcloud.com/talk/)
- [Rocket.chat](https://rocket.chat) - [Rocket.chat](https://rocket.chat)
- [Slack](https://slack.com) - [Slack](https://slack.com)
- [Ssh-chat](https://github.com/shazow/ssh-chat) - [Ssh-chat](https://github.com/shazow/ssh-chat)
- [Steam](https://store.steampowered.com/) - ~~[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) - [Telegram](https://telegram.org)
- [Twitch](https://twitch.tv) - [Twitch](https://twitch.tv)
- [VK](https://vk.com/)
- [WhatsApp](https://www.whatsapp.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) - [XMPP](https://xmpp.org)
- [Zulip](https://zulipchat.com) - [Zulip](https://zulipchat.com)
### 3rd party via matterbridge api ### 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) - [Discourse](https://github.com/DeclanHoare/matterbabble)
- [Facebook messenger](https://github.com/powerjungle/fbridge-asyncio)
- [Facebook messenger](https://github.com/VictorNine/fbridge) - [Facebook messenger](https://github.com/VictorNine/fbridge)
- [Minecraft](https://github.com/elytra/MatterLink) - [Minecraft](https://github.com/elytra/MatterLink)
- [Reddit](https://github.com/bonehurtingjuice/mattereddit) - [Reddit](https://github.com/bonehurtingjuice/mattereddit)
- [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)
- [MatterAMXX](https://github.com/GabeIggy/MatterAMXX) - [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 ### API
@ -121,12 +137,19 @@ 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. 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) - [pyCord](https://github.com/NikkyAI/pyCord) (crossplatform chatbot)
- [Mattereddit](https://github.com/bonehurtingjuice/mattereddit) (Reddit chat support) - [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) - [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
- [matterbabble](https://github.com/DeclanHoare/matterbabble) (Discourse 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) - [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 ## Chat with us
@ -153,25 +176,71 @@ See <https://github.com/42wim/matterbridge/wiki>
### Binaries ### Binaries
- Latest stable release [v1.20.0](https://github.com/42wim/matterbridge/releases/latest) - 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. - 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 ### Packages
- [Overview](https://repology.org/metapackage/matterbridge/versions) - [Overview](https://repology.org/metapackage/matterbridge/versions)
- [snap](https://snapcraft.io/matterbridge) - [snap](https://snapcraft.io/matterbridge)
- [scoop](https://github.com/42wim/scoop-bucket)
## Building ## Building
Most people just want to use binaries, you can find those [here](https://github.com/42wim/matterbridge/releases/latest) 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: 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 ```bash
go get github.com/42wim/matterbridge go install 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: You should now have matterbridge binary in the ~/go/bin directory:
@ -201,8 +270,8 @@ All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for
```toml ```toml
[irc] [irc]
[irc.freenode] [irc.libera]
Server="irc.freenode.net:6667" Server="irc.libera.chat:6667"
Nick="yourbotname" Nick="yourbotname"
[mattermost] [mattermost]
@ -218,7 +287,7 @@ All possible [settings](https://github.com/42wim/matterbridge/wiki/Settings) for
name="mygateway" name="mygateway"
enable=true enable=true
[[gateway.inout]] [[gateway.inout]]
account="irc.freenode" account="irc.libera"
channel="#testing" channel="#testing"
[[gateway.inout]] [[gateway.inout]]
@ -275,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. 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 ## Changelog
See [changelog.md](https://github.com/42wim/matterbridge/blob/master/changelog.md) See [changelog.md](https://github.com/42wim/matterbridge/blob/master/changelog.md)
@ -296,8 +369,13 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
- [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support) - [fbridge](https://github.com/VictorNine/fbridge) (Facebook messenger support)
- [isla](https://github.com/alphachung/isla) (Bot for Discord-Telegram groups used alongside matterbridge) - [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) - [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) - [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://mattermost.com/blog/connect-irc-to-mattermost/>
@ -308,6 +386,9 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
- <https://www.stitcher.com/s/?eid=52382713> - <https://www.stitcher.com/s/?eid=52382713>
- <https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/> - <https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/>
- <https://userlinux.net/mattermost-and-matterbridge.html> - <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 ## Thanks
@ -322,10 +403,10 @@ Matterbridge wouldn't exist without these libraries:
- discord - <https://github.com/bwmarrin/discordgo> - discord - <https://github.com/bwmarrin/discordgo>
- echo - <https://github.com/labstack/echo> - echo - <https://github.com/labstack/echo>
- gitter - <https://github.com/sromku/go-gitter>
- gops - <https://github.com/google/gops> - gops - <https://github.com/google/gops>
- gozulipbot - <https://github.com/ifo/gozulipbot> - gozulipbot - <https://github.com/ifo/gozulipbot>
- gumble - <https://github.com/layeh/gumble> - gumble - <https://github.com/layeh/gumble>
- harmony - <https://github.com/harmony-development/shibshib>
- irc - <https://github.com/lrstanley/girc> - irc - <https://github.com/lrstanley/girc>
- keybase - <https://github.com/keybase/go-keybase-chat-bot> - keybase - <https://github.com/keybase/go-keybase-chat-bot>
- matrix - <https://github.com/matrix-org/gomatrix> - matrix - <https://github.com/matrix-org/gomatrix>
@ -333,12 +414,15 @@ Matterbridge wouldn't exist without these libraries:
- msgraph.go - <https://github.com/yaegashi/msgraph.go> - msgraph.go - <https://github.com/yaegashi/msgraph.go>
- mumble - <https://github.com/layeh/gumble> - mumble - <https://github.com/layeh/gumble>
- nctalk - <https://github.com/gary-kim/go-nc-talk> - nctalk - <https://github.com/gary-kim/go-nc-talk>
- rocketchat - <https://github.com/RocketChat/Rocket.Chat.Go.SDK>
- slack - <https://github.com/nlopes/slack> - slack - <https://github.com/nlopes/slack>
- sshchat - <https://github.com/shazow/ssh-chat> - sshchat - <https://github.com/shazow/ssh-chat>
- steam - <https://github.com/Philipp15b/go-steam> - steam - <https://github.com/Philipp15b/go-steam>
- telegram - <https://github.com/go-telegram-bot-api/telegram-bot-api> - telegram - <https://github.com/go-telegram-bot-api/telegram-bot-api>
- tengo - <https://github.com/d5/tengo> - tengo - <https://github.com/d5/tengo>
- vk - <https://github.com/SevereCloud/vksdk>
- whatsapp - <https://github.com/Rhymen/go-whatsapp> - whatsapp - <https://github.com/Rhymen/go-whatsapp>
- whatsapp - <https://github.com/tulir/whatsmeow>
- xmpp - <https://github.com/mattn/go-xmpp> - xmpp - <https://github.com/mattn/go-xmpp>
- zulip - <https://github.com/ifo/gozulipbot> - zulip - <https://github.com/ifo/gozulipbot>
@ -346,7 +430,7 @@ Matterbridge wouldn't exist without these libraries:
[mb-discord]: https://discord.gg/AkKPtrQ [mb-discord]: https://discord.gg/AkKPtrQ
[mb-gitter]: https://gitter.im/42wim/matterbridge [mb-gitter]: https://gitter.im/42wim/matterbridge
[mb-irc]: https://webchat.freenode.net/?channels=matterbridgechat [mb-irc]: https://web.libera.chat/#matterbridge
[mb-keybase]: https://keybase.io/team/matterbridge [mb-keybase]: https://keybase.io/team/matterbridge
[mb-matrix]: https://riot.im/app/#/room/#matterbridge:matrix.org [mb-matrix]: https://riot.im/app/#/room/#matterbridge:matrix.org
[mb-mattermost]: https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e [mb-mattermost]: https://framateam.org/signup_user_complete/?id=tfqm33ggop8x3qgu4boeieta6e

View File

@ -1,17 +1,20 @@
package api package api
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"net/http" "net/http"
"strings"
"sync" "sync"
"time" "time"
"gopkg.in/olahol/melody.v1" "github.com/olahol/melody"
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware" "github.com/labstack/echo/v4/middleware"
"github.com/mitchellh/mapstructure"
ring "github.com/zfjagann/golang-ring" ring "github.com/zfjagann/golang-ring"
) )
@ -137,6 +140,36 @@ func (b *API) handlePostMessage(c echo.Context) error {
message.Account = b.Account message.Account = b.Account
message.ID = "" message.ID = ""
message.Timestamp = time.Now() 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.Log.Debugf("Sending message from %s on %s to gateway", message.Username, "api")
b.Remote <- message b.Remote <- message
return c.JSON(http.StatusOK, message) return c.JSON(http.StatusOK, message)
@ -166,15 +199,20 @@ func (b *API) handleStream(c echo.Context) error {
} }
c.Response().Flush() c.Response().Flush()
for { for {
select {
// TODO: this causes issues, messages should be broadcasted to all connected clients // TODO: this causes issues, messages should be broadcasted to all connected clients
msg := b.Messages.Dequeue() default:
if msg != nil { msg := b.Messages.Dequeue()
if err := json.NewEncoder(c.Response()).Encode(msg); err != nil { if msg != nil {
return err 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)
} }
} }

View File

@ -23,12 +23,15 @@ const (
EventRejoinChannels = "rejoin_channels" EventRejoinChannels = "rejoin_channels"
EventUserAction = "user_action" EventUserAction = "user_action"
EventMsgDelete = "msg_delete" EventMsgDelete = "msg_delete"
EventFileDelete = "file_delete"
EventAPIConnected = "api_connected" EventAPIConnected = "api_connected"
EventUserTyping = "user_typing" EventUserTyping = "user_typing"
EventGetChannelMembers = "get_channel_members" EventGetChannelMembers = "get_channel_members"
EventNoticeIRC = "notice_irc" EventNoticeIRC = "notice_irc"
) )
const ParentIDNotFound = "msg-parent-not-found"
type Message struct { type Message struct {
Text string `json:"text"` Text string `json:"text"`
Channel string `json:"channel"` Channel string `json:"channel"`
@ -45,14 +48,23 @@ type Message struct {
Extra map[string][]interface{} 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 { type FileInfo struct {
Name string Name string
Data *[]byte Data *[]byte
Comment string Comment string
URL string URL string
Size int64 Size int64
Avatar bool Avatar bool
SHA string SHA string
NativeID string
} }
type ChannelInfo struct { type ChannelInfo struct {
@ -75,27 +87,28 @@ type ChannelMember struct {
type ChannelMembers []ChannelMember type ChannelMembers []ChannelMember
type Protocol struct { type Protocol struct {
AuthCode string // steam AllowMention []string // discord
BindAddress string // mattermost, slack // DEPRECATED AuthCode string // steam
Buffer int // api BindAddress string // mattermost, slack // DEPRECATED
Charset string // irc Buffer int // api
ClientID string // msteams Charset string // irc
ColorNicks bool // only irc for now ClientID string // msteams
Debug bool // general ColorNicks bool // only irc for now
DebugLevel int // only for irc now Debug bool // general
DisableWebPagePreview bool // telegram DebugLevel int // only for irc now
EditSuffix string // mattermost, slack, discord, telegram, gitter DisableWebPagePreview bool // telegram
EditDisable bool // mattermost, slack, discord, telegram, gitter EditSuffix string // mattermost, slack, discord, telegram, gitter
HTMLDisable bool // matrix EditDisable bool // mattermost, slack, discord, telegram, gitter
IconURL string // mattermost, slack HTMLDisable bool // matrix
IgnoreFailureOnStart bool // general IconURL string // mattermost, slack
IgnoreNicks string // all protocols IgnoreFailureOnStart bool // general
IgnoreMessages string // all protocols IgnoreNicks string // all protocols
Jid string // xmpp IgnoreMessages string // all protocols
JoinDelay string // all protocols Jid string // xmpp
Label string // all protocols JoinDelay string // all protocols
Login string // mattermost, matrix Label string // all protocols
LogFile string // general Login string // mattermost, matrix
LogFile string // general
MediaDownloadBlackList []string MediaDownloadBlackList []string
MediaDownloadPath string // Basically MediaServerUpload, but instead of uploading it, just write it to a file on the same server. MediaDownloadPath string // Basically MediaServerUpload, but instead of uploading it, just write it to a file on the same server.
MediaDownloadSize int // all protocols MediaDownloadSize int // all protocols
@ -108,7 +121,9 @@ type Protocol struct {
MessageLength int // IRC, max length of a message allowed MessageLength int // IRC, max length of a message allowed
MessageQueue int // IRC, size of message queue for flood control MessageQueue int // IRC, size of message queue for flood control
MessageSplit bool // IRC, split long messages with newlines on MessageLength instead of clipping 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 Muc string // xmpp
MxID string // matrix
Name string // all protocols Name string // all protocols
Nick string // all protocols Nick string // all protocols
NickFormatter string // mattermost, slack NickFormatter string // mattermost, slack
@ -126,12 +141,13 @@ type Protocol struct {
QuoteDisable bool // telegram QuoteDisable bool // telegram
QuoteFormat string // telegram QuoteFormat string // telegram
QuoteLengthLimit int // telegram QuoteLengthLimit int // telegram
RealName string // IRC
RejoinDelay int // IRC RejoinDelay int // IRC
ReplaceMessages [][]string // all protocols ReplaceMessages [][]string // all protocols
ReplaceNicks [][]string // all protocols ReplaceNicks [][]string // all protocols
RemoteNickFormat string // all protocols RemoteNickFormat string // all protocols
RunCommands []string // IRC RunCommands []string // IRC
Server string // IRC,mattermost,XMPP,discord Server string // IRC,mattermost,XMPP,discord,matrix
SessionFile string // msteams,whatsapp SessionFile string // msteams,whatsapp
ShowJoinPart bool // all protocols ShowJoinPart bool // all protocols
ShowTopicChange bool // slack ShowTopicChange bool // slack
@ -146,7 +162,7 @@ type Protocol struct {
Team string // mattermost, keybase Team string // mattermost, keybase
TeamID string // msteams TeamID string // msteams
TenantID string // msteams TenantID string // msteams
Token string // gitter, slack, discord, api Token string // gitter, slack, discord, api, matrix
Topic string // zulip Topic string // zulip
URL string // mattermost, slack // DEPRECATED URL string // mattermost, slack // DEPRECATED
UseAPI bool // mattermost, slack UseAPI bool // mattermost, slack
@ -155,8 +171,9 @@ type Protocol struct {
UseTLS bool // IRC UseTLS bool // IRC
UseDiscriminator bool // discord UseDiscriminator bool // discord
UseFirstName bool // telegram UseFirstName bool // telegram
UseUserName bool // discord, matrix UseUserName bool // discord, matrix, mattermost
UseInsecureURL bool // telegram UseInsecureURL bool // telegram
UserName string // IRC
VerboseJoinPart bool // IRC VerboseJoinPart bool // IRC
WebhookBindAddress string // mattermost, slack WebhookBindAddress string // mattermost, slack
WebhookURL string // mattermost, slack WebhookURL string // mattermost, slack

View File

@ -2,30 +2,31 @@ package bdiscord
import ( import (
"bytes" "bytes"
"errors"
"fmt" "fmt"
"strings" "strings"
"sync" "sync"
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/discord/transmitter"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
"github.com/matterbridge/discordgo" "github.com/bwmarrin/discordgo"
lru "github.com/hashicorp/golang-lru"
) )
const MessageLength = 1950 const (
MessageLength = 1950
cFileUpload = "file_upload"
)
type Bdiscord struct { type Bdiscord struct {
*bridge.Config *bridge.Config
c *discordgo.Session c *discordgo.Session
nick string nick string
userID string userID string
guildID string guildID string
webhookID string
webhookToken string
canEditWebhooks bool
channelsMutex sync.RWMutex channelsMutex sync.RWMutex
channels []*discordgo.Channel channels []*discordgo.Channel
@ -34,30 +35,39 @@ type Bdiscord struct {
membersMutex sync.RWMutex membersMutex sync.RWMutex
userMemberMap map[string]*discordgo.Member userMemberMap map[string]*discordgo.Member
nickMemberMap 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 { 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.userMemberMap = make(map[string]*discordgo.Member)
b.nickMemberMap = make(map[string]*discordgo.Member) b.nickMemberMap = make(map[string]*discordgo.Member)
b.channelInfoMap = make(map[string]*config.ChannelInfo) b.channelInfoMap = make(map[string]*config.ChannelInfo)
if b.GetString("WebhookURL") != "" {
b.Log.Debug("Configuring Discord Incoming Webhook") b.useAutoWebhooks = b.GetBool("AutoWebhooks")
b.webhookID, b.webhookToken = b.splitURL(b.GetString("WebhookURL")) if b.useAutoWebhooks {
b.Log.Debug("Using automatic webhooks")
} }
return b return b
} }
func (b *Bdiscord) Connect() error { func (b *Bdiscord) Connect() error {
var err error var err error
var guildFound bool
token := b.GetString("Token") token := b.GetString("Token")
b.Log.Info("Connecting") 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 ") { if !strings.HasPrefix(b.GetString("Token"), "Bot ") {
token = "Bot " + b.GetString("Token") token = "Bot " + b.GetString("Token")
} }
@ -71,19 +81,16 @@ func (b *Bdiscord) Connect() error {
return err return err
} }
b.Log.Info("Connection succeeded") b.Log.Info("Connection succeeded")
b.c.AddHandler(b.messageCreate) // Add privileged intent for guild member tracking. This is needed to track nicks
b.c.AddHandler(b.messageTyping) // for display names and @mention translation
b.c.AddHandler(b.memberUpdate) b.c.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged |
b.c.AddHandler(b.messageUpdate) discordgo.IntentsGuildMembers)
b.c.AddHandler(b.messageDelete)
b.c.AddHandler(b.messageDeleteBulk)
b.c.AddHandler(b.memberAdd)
b.c.AddHandler(b.memberRemove)
err = b.c.Open() err = b.c.Open()
if err != nil { if err != nil {
return err return err
} }
guilds, err := b.c.UserGuilds(100, "", "") guilds, err := b.c.UserGuilds(100, "", "", false)
if err != nil { if err != nil {
return err return err
} }
@ -94,66 +101,107 @@ func (b *Bdiscord) Connect() error {
serverName := strings.Replace(b.GetString("Server"), "ID:", "", -1) serverName := strings.Replace(b.GetString("Server"), "ID:", "", -1)
b.nick = userinfo.Username b.nick = userinfo.Username
b.userID = userinfo.ID b.userID = userinfo.ID
// Try and find this account's guild, and populate channels
b.channelsMutex.Lock() b.channelsMutex.Lock()
for _, guild := range guilds { for _, guild := range guilds {
if guild.Name == serverName || guild.ID == serverName { // Skip, if the server name does not match the visible name or the ID
b.channels, err = b.c.GuildChannels(guild.ID) if guild.Name != serverName && guild.ID != serverName {
if err != nil { continue
break
}
b.guildID = guild.ID
guildFound = true
} }
// 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() b.channelsMutex.Unlock()
if !guildFound {
msg := fmt.Sprintf("Server \"%s\" not found", b.GetString("Server")) // If we couldn't find a guild, we print extra debug information and return a nice error
err = errors.New(msg) if b.guildID == "" {
b.Log.Error(msg) err = fmt.Errorf("could not find Discord server %#v", b.GetString("Server"))
b.Log.Info("Possible values:") b.Log.Error(err.Error())
// Print all of the possible server values
b.Log.Info("Possible server values:")
for _, guild := range guilds { for _, guild := range guilds {
b.Log.Infof("Server=\"%s\" # Server name", guild.Name) b.Log.Infof("\t- Server=%#v # by name", guild.Name)
b.Log.Infof("Server=\"%s\" # Server ID", guild.ID) b.Log.Infof("\t- Server=%#v # by ID", guild.ID)
} }
}
if err != nil { // If there are no results, we should say that
if len(guilds) == 0 {
b.Log.Info("\t- (none found)")
}
return err return err
} }
b.channelsMutex.RLock() // Legacy note: WebhookURL used to have an actual webhook URL that we would edit,
if b.GetString("WebhookURL") == "" { // but we stopped doing that due to Discord making rate limits more aggressive.
for _, channel := range b.channels { //
b.Log.Debugf("found channel %#v", channel) // Even older: the same WebhookURL used to be used by every channel, which is usually unexpected.
} // This is no longer possible.
} else { if b.GetString("WebhookURL") != "" {
manageWebhooks := discordgo.PermissionManageWebhooks message := "The global WebhookURL setting has been removed. "
var channelsDenied []string message += "You can get similar \"webhook editing\" behaviour by replacing this line with `AutoWebhooks=true`. "
for _, info := range b.Channels { message += "If you rely on the old-OLD (non-editing) behaviour, can move the WebhookURL to specific channel sections."
id := b.getChannelID(info.Name) // note(qaisjp): this readlocks channelsMutex b.Log.Errorln(message)
b.Log.Debugf("Verifying PermissionManageWebhooks for %s with ID %s", info.ID, id) return fmt.Errorf("use of removed WebhookURL setting")
}
perms, permsErr := b.c.UserChannelPermissions(userinfo.ID, id)
if permsErr != nil { if b.GetInt("debuglevel") == 2 {
b.Log.Warnf("Failed to check PermissionManageWebhooks in channel \"%s\": %s", info.Name, permsErr.Error()) b.Log.Debug("enabling even more discord debug")
} else if perms&manageWebhooks == manageWebhooks { b.c.Debug = true
continue }
}
channelsDenied = append(channelsDenied, fmt.Sprintf("%#v", info.Name)) // Initialise webhook management
} b.transmitter = transmitter.New(b.c, b.guildID, "matterbridge", b.useAutoWebhooks)
b.transmitter.Log = b.Log
b.canEditWebhooks = len(channelsDenied) == 0
b.canEditWebhooks = false var webhookChannelIDs []string
b.Log.Info("Webhook editing is disabled because of ratelimit issues") for _, channel := range b.Channels {
/* channelID := b.getChannelID(channel.Name) // note(qaisjp): this readlocks channelsMutex
if b.canEditWebhooks {
b.Log.Info("Can manage webhooks; will edit channel for global webhook on send") // If a WebhookURL was not explicitly provided for this channel,
} else { // there are two options: just a regular bot message (ugly) or this is should be webhook sent
b.Log.Warn("Can't manage webhooks; won't edit channel for global webhook on send") if channel.Options.WebhookURL == "" {
b.Log.Warn("Can't manage webhooks in channels: ", strings.Join(channelsDenied, ", ")) // If it should be webhook sent, we should enforce this via the transmitter
} if b.useAutoWebhooks {
*/ webhookChannelIDs = append(webhookChannelIDs, channelID)
}
continue
}
whID, whToken, ok := b.splitURL(channel.Options.WebhookURL)
if !ok {
return fmt.Errorf("failed to parse WebhookURL %#v for channel %#v", channel.Options.WebhookURL, channel.ID)
}
b.transmitter.AddWebhook(channelID, &discordgo.Webhook{
ID: whID,
Token: whToken,
GuildID: b.guildID,
ChannelID: channelID,
})
}
if b.useAutoWebhooks {
err = b.transmitter.RefreshGuildWebhooks(webhookChannelIDs)
if err != nil {
b.Log.WithError(err).Println("transmitter could not refresh guild webhooks")
return err
}
} }
b.channelsMutex.RUnlock()
// Obtaining guild members and initializing nickname mapping. // Obtaining guild members and initializing nickname mapping.
b.membersMutex.Lock() b.membersMutex.Lock()
@ -174,6 +222,19 @@ func (b *Bdiscord) Connect() error {
b.nickMemberMap[member.Nick] = member 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 return nil
} }
@ -210,80 +271,22 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
msg.Text = "_" + msg.Text + "_" msg.Text = "_" + msg.Text + "_"
} }
// use initial webhook configured for the entire Discord account // Handle prefix hint for unthreaded messages.
isGlobalWebhook := true if msg.ParentNotFound() {
wID := b.webhookID msg.ParentID = ""
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
}
} }
b.channelsMutex.RUnlock()
// Use webhook to send the message // Use webhook to send the message
if wID != "" && msg.Event != config.EventMsgDelete { useWebhooks := b.shouldMessageUseWebhooks(&msg)
// skip events if useWebhooks && msg.Event != config.EventMsgDelete && msg.ParentID == "" {
if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave && msg.Event != config.EventTopicChange { return b.handleEventWebhook(&msg, channelID)
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
}
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 msg.ID != "" {
b.Log.Debugf("Editing webhook message")
uri := discordgo.EndpointWebhookToken(wID, wToken) + "/messages/" + msg.ID
_, err := b.c.RequestWithBucketID("PATCH", uri, discordgo.WebhookParams{
Content: msg.Text,
Username: msg.Username,
}, discordgo.EndpointWebhookToken("", ""))
if err == nil {
return msg.ID, nil
}
b.Log.Errorf("Could not edit webhook message: %s", err)
}
b.Log.Debugf("Broadcasting using Webhook")
// 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.
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
} }
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)") b.Log.Debugf("Broadcasting using token (API)")
// Delete message // Delete message
@ -295,80 +298,87 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
return "", err 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 // Upload a file if it exists
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) { for _, rmsg := range helper.HandleExtra(msg, b.General) {
rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength) // 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 { if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil {
b.Log.Errorf("Could not send message %#v: %s", rmsg, err) b.Log.Errorf("Could not send message %#v: %s", rmsg, err)
} }
} }
// check if we have files to upload (from slack, telegram or mattermost) // check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 { 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 // Edit message
if msg.ID != "" { if msg.ID != "" {
_, err := b.c.ChannelMessageEdit(channelID, msg.ID, msg.Username+msg.Text) // Exploit that a discord message ID is actually just a large number, and we encode a list of IDs by separating them with ";".
return msg.ID, err msgIds := strings.Split(msg.ID, ";")
} msgParts := helper.ClipOrSplitMessage(b.replaceUserMentions(msg.Text), MessageLength, b.GetString("MessageClipped"), len(msgIds))
for len(msgParts) < len(msgIds) {
// Post normal message msgParts = append(msgParts, "((obsoleted by edit))")
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
} }
} for i := range msgParts {
return false // 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
// isWebhookID returns true if the specified id is used in a defined webhook _, err := b.c.ChannelMessageEdit(channelID, msgIds[i], msg.Username+msgParts[i])
func (b *Bdiscord) isWebhookID(id string) bool { if err != nil {
if b.GetString("WebhookURL") != "" { return "", err
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
} }
} }
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 // handleUploadFile handles native upload of files
func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (string, error) { func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (string, error) {
var err error
for _, f := range msg.Extra["file"] { for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo) fi := f.(config.FileInfo)
file := discordgo.File{ file := discordgo.File{
@ -377,93 +387,19 @@ func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (stri
Reader: bytes.NewReader(*fi.Data), Reader: bytes.NewReader(*fi.Data),
} }
m := discordgo.MessageSend{ m := discordgo.MessageSend{
Content: msg.Username + fi.Comment, Content: msg.Username + fi.Comment,
Files: []*discordgo.File{&file}, Files: []*discordgo.File{&file},
AllowedMentions: b.getAllowedMentions(),
} }
_, err = b.c.ChannelMessageSendComplex(channelID, &m) res, err := b.c.ChannelMessageSendComplex(channelID, &m)
if err != nil { if err != nil {
return "", fmt.Errorf("file upload failed: %s", err) 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 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
)
// If avatar is unset, check if UseLocalAvatar contains the message's
// account or protocol, and if so, try to find a local avatar
if msg.Avatar == "" {
for _, val := range b.GetStringSlice("UseLocalAvatar") {
if msg.Protocol == val || msg.Account == val {
if avatar := b.findAvatar(msg); avatar != "" {
msg.Avatar = avatar
}
break
}
}
}
// 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
}
func (b *Bdiscord) findAvatar(m *config.Message) string {
member, err := b.getGuildMemberByNick(m.Username)
if err != nil {
return ""
}
return member.User.AvatarURL("")
}

View File

@ -2,10 +2,15 @@ package bdiscord
import ( import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/matterbridge/discordgo" "github.com/bwmarrin/discordgo"
"github.com/davecgh/go-spew/spew"
) )
func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) { //nolint:unparam 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 := config.Message{Account: b.Account, ID: m.ID, Event: config.EventMsgDelete, Text: config.EventMsgDelete}
rmsg.Channel = b.getChannelName(m.ChannelID) 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 // 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 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 { for _, msgID := range m.Messages {
rmsg := config.Message{ rmsg := config.Message{
Account: b.Account, Account: b.Account,
@ -31,7 +40,15 @@ 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) { 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") { if !b.GetBool("ShowUserTyping") {
return return
} }
@ -47,11 +64,15 @@ func (b *Bdiscord) messageTyping(s *discordgo.Session, m *discordgo.TypingStart)
} }
func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdate) { //nolint:unparam 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") { if b.GetBool("EditDisable") {
return return
} }
// only when message is actually edited // only when message is actually edited
if m.Message.EditedTimestamp != "" { if m.Message.EditedTimestamp != nil {
b.Log.Debugf("Sending edit message") b.Log.Debugf("Sending edit message")
m.Content += b.GetString("EditSuffix") m.Content += b.GetString("EditSuffix")
msg := &discordgo.MessageCreate{ msg := &discordgo.MessageCreate{
@ -62,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 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 var err error
// not relay our own messages // not relay our own messages
@ -69,7 +94,7 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
return return
} }
// if using webhooks, do not relay if it's ours // 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 return
} }
@ -82,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} 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 != "" { if m.Content != "" {
b.Log.Debugf("== Receiving event %#v", m.Message)
m.Message.Content = b.replaceChannelMentions(m.Message.Content) m.Message.Content = b.replaceChannelMentions(m.Message.Content)
rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c) rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c)
if err != nil { if err != nil {
@ -127,12 +153,21 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
// Replace emotes // Replace emotes
rmsg.Text = replaceEmotes(rmsg.Text) 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("<= Sending message from %s on %s to gateway", m.Author.Username, b.Account)
b.Log.Debugf("<= Message is %#v", rmsg) b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg b.Remote <- rmsg
} }
func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) { 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 { if m.Member == nil {
b.Log.Warnf("Received member update with no member information: %#v", m) b.Log.Warnf("Received member update with no member information: %#v", m)
} }
@ -160,6 +195,13 @@ func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUp
} }
func (b *Bdiscord) memberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd) { 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 { if m.Member == nil {
b.Log.Warnf("Received member update with no member information: %#v", m) b.Log.Warnf("Received member update with no member information: %#v", m)
return return
@ -181,6 +223,13 @@ func (b *Bdiscord) memberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd)
} }
func (b *Bdiscord) memberRemove(s *discordgo.Session, m *discordgo.GuildMemberRemove) { 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 { if m.Member == nil {
b.Log.Warnf("Received member update with no member information: %#v", m) b.Log.Warnf("Received member update with no member information: %#v", m)
return return

View File

@ -3,7 +3,7 @@ package bdiscord
import ( import (
"testing" "testing"
"github.com/matterbridge/discordgo" "github.com/bwmarrin/discordgo"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )

View File

@ -6,9 +6,33 @@ import (
"strings" "strings"
"unicode" "unicode"
"github.com/matterbridge/discordgo" "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 { func (b *Bdiscord) getNick(user *discordgo.User, guildID string) string {
b.membersMutex.RLock() b.membersMutex.RLock()
defer b.membersMutex.RUnlock() defer b.membersMutex.RUnlock()
@ -44,7 +68,7 @@ func (b *Bdiscord) getGuildMemberByNick(nick string) (*discordgo.Member, error)
b.membersMutex.RLock() b.membersMutex.RLock()
defer b.membersMutex.RUnlock() defer b.membersMutex.RUnlock()
if member, ok := b.nickMemberMap[nick]; ok { if member, ok := b.nickMemberMap[strings.TrimSpace(nick)]; ok {
return member, nil return member, nil
} }
return nil, errors.New("Couldn't find guild member with nick " + nick) // This will most likely get ignored by the caller return nil, errors.New("Couldn't find guild member with nick " + nick) // This will most likely get ignored by the caller
@ -196,7 +220,7 @@ func (b *Bdiscord) replaceAction(text string) (string, bool) {
} }
// splitURL splits a webhookURL and returns the ID and token. // 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 ( const (
expectedWebhookSplitCount = 7 expectedWebhookSplitCount = 7
webhookIdxID = 5 webhookIdxID = 5
@ -204,9 +228,9 @@ func (b *Bdiscord) splitURL(url string) (string, string) {
) )
webhookURLSplit := strings.Split(url, "/") webhookURLSplit := strings.Split(url, "/")
if len(webhookURLSplit) != expectedWebhookSplitCount { 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 { 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

@ -5,10 +5,7 @@ import (
"fmt" "fmt"
"image/png" "image/png"
"io" "io"
"io/ioutil"
"net/http" "net/http"
"os"
"os/exec"
"regexp" "regexp"
"strings" "strings"
"time" "time"
@ -51,6 +48,30 @@ func DownloadFileAuth(url string, auth string) (*[]byte, error) {
return &data, nil 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 // GetSubLines splits messages in newline-delimited lines. If maxLineLength is
// specified as non-zero GetSubLines will also clip long lines to the maximum // specified as non-zero GetSubLines will also clip long lines to the maximum
// length and insert a warning marker that the line was clipped. // length and insert a warning marker that the line was clipped.
@ -58,11 +79,19 @@ func DownloadFileAuth(url string, auth string) (*[]byte, error) {
// TODO: The current implementation has the inconvenient that it disregards // TODO: The current implementation has the inconvenient that it disregards
// word boundaries when splitting but this is hard to solve without potentially // word boundaries when splitting but this is hard to solve without potentially
// breaking formatting and other stylistic effects. // breaking formatting and other stylistic effects.
func GetSubLines(message string, maxLineLength int) []string { func GetSubLines(message string, maxLineLength int, clippingMessage string) []string {
const clippingMessage = " <clipped message>" if clippingMessage == "" {
clippingMessage = " <clipped message>"
}
var lines []string var lines []string
for _, line := range strings.Split(strings.TrimSpace(message), "\n") { 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 { if maxLineLength == 0 || len([]byte(line)) <= maxLineLength {
lines = append(lines, line) lines = append(lines, line)
continue continue
@ -145,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. // 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) { 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 var avatar bool
logger.Debugf("Download OK %#v %#v", name, len(*data)) logger.Debugf("Download OK %#v %#v", name, len(*data))
if msg.Event == config.EventAvatarDownload { if msg.Event == config.EventAvatarDownload {
avatar = true avatar = true
} }
msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{ msg.Extra["file"] = append(msg.Extra["file"], config.FileInfo{
Name: name, Name: name,
Data: data, Data: data,
URL: url, URL: url,
Comment: comment, Comment: comment,
Avatar: avatar, Avatar: avatar,
NativeID: id,
}) })
} }
@ -169,21 +204,61 @@ func RemoveEmptyNewLines(msg string) string {
// ClipMessage trims a message to the specified length if it exceeds it and adds a warning // ClipMessage trims a message to the specified length if it exceeds it and adds a warning
// to the message in case it does so. // to the message in case it does so.
func ClipMessage(text string, length int) string { func ClipMessage(text string, length int, clippingMessage string) string {
const clippingMessage = " <clipped message>" if clippingMessage == "" {
clippingMessage = " <clipped message>"
}
if len(text) > length { if len(text) > length {
text = text[:length-len(clippingMessage)] text = text[:length-len(clippingMessage)]
if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError { for len(text) > 0 {
text = text[:len(text)-size] 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 text += clippingMessage
} }
return text 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 // ParseMarkdown takes in an input string as markdown and parses it to html
func ParseMarkdown(input string) string { func ParseMarkdown(input string) string {
extensions := parser.HardLineBreak | parser.NoIntraEmphasis extensions := parser.HardLineBreak | parser.NoIntraEmphasis | parser.FencedCode
markdownParser := parser.NewWithExtensions(extensions) markdownParser := parser.NewWithExtensions(extensions)
renderer := html.NewRenderer(html.RendererOptions{ renderer := html.NewRenderer(html.RendererOptions{
Flags: 0, Flags: 0,
@ -210,49 +285,3 @@ func ConvertWebPToPNG(data *[]byte) error {
*data = w.Bytes() *data = w.Bytes()
return nil return nil
} }
// 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:
tmpFile, err := ioutil.TempFile(os.TempDir(), "matterbridge-lottie-*.tgs")
if err != nil {
return err
}
tmpFileName := tmpFile.Name()
defer func() {
if removeErr := os.Remove(tmpFileName); removeErr != nil {
logger.Errorf("Could not delete temporary file %s: %v", tmpFileName, removeErr)
}
}()
if _, writeErr := tmpFile.Write(*data); writeErr != nil {
return writeErr
}
// Must close before calling lottie to avoid data races:
if closeErr := tmpFile.Close(); closeErr != nil {
return closeErr
}
// Call lottie to transform:
cmd := exec.Command("lottie_convert.py", "--input-format", "lottie", "--output-format", outputFormat, tmpFileName, "/dev/stdout")
cmd.Stderr = nil
// NB: lottie writes progress into to stderr in all cases.
stdout, stderr := cmd.Output()
if stderr != nil {
// 'stderr' already contains some parts of Stderr, because it was set to 'nil'.
return stderr
}
*data = stdout
return nil
}

View File

@ -10,98 +10,105 @@ import (
const testLineLength = 64 const testLineLength = 64
var ( var lineSplittingTestCases = map[string]struct {
lineSplittingTestCases = map[string]struct { input string
input string splitOutput []string
splitOutput []string nonSplitOutput []string
nonSplitOutput []string }{
}{ "Short single-line message": {
"Short single-line message": { input: "short",
input: "short", splitOutput: []string{"short"},
splitOutput: []string{"short"}, nonSplitOutput: []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": { nonSplitOutput: []string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."},
input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", },
splitOutput: []string{ "Short multi-line message": {
"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>", input: "I\ncan't\nget\nno\nsatisfaction!",
"cing elit, sed do eiusmod tempor incididunt ut <clipped message>", splitOutput: []string{
" labore et dolore magna aliqua.", "I",
}, "can't",
nonSplitOutput: []string{"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."}, "get",
"no",
"satisfaction!",
}, },
"Short multi-line message": { nonSplitOutput: []string{
input: "I\ncan't\nget\nno\nsatisfaction!", "I",
splitOutput: []string{ "can't",
"I", "get",
"can't", "no",
"get", "satisfaction!",
"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" + "Long multi-line message": {
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" + input: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\n" +
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n" + "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" +
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\n" +
splitOutput: []string{ "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
"Lorem ipsum dolor sit amet, consectetur adipis <clipped message>", splitOutput: []string{
"cing elit, sed do eiusmod tempor incididunt ut <clipped message>", "Lorem ipsum dolor sit amet, consectetur adipis <clipped message>",
" labore et dolore magna aliqua.", "cing elit, sed do eiusmod tempor incididunt ut <clipped message>",
"Ut enim ad minim veniam, quis nostrud exercita <clipped message>", " labore et dolore magna aliqua.",
"tion ullamco laboris nisi ut aliquip ex ea com <clipped message>", "Ut enim ad minim veniam, quis nostrud exercita <clipped message>",
"modo consequat.", "tion ullamco laboris nisi ut aliquip ex ea com <clipped message>",
"Duis aute irure dolor in reprehenderit in volu <clipped message>", "modo consequat.",
"ptate velit esse cillum dolore eu fugiat nulla <clipped message>", "Duis aute irure dolor in reprehenderit in volu <clipped message>",
" pariatur.", "ptate velit esse cillum dolore eu fugiat nulla <clipped message>",
"Excepteur sint occaecat cupidatat non proident <clipped message>", " pariatur.",
", sunt in culpa qui officia deserunt mollit an <clipped message>", "Excepteur sint occaecat cupidatat non proident <clipped message>",
"im id est laborum.", ", sunt in culpa qui officia deserunt mollit an <clipped message>",
}, "im id est laborum.",
nonSplitOutput: []string{
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
"Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
"Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
"Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
},
}, },
"Message ending with new-line.": { nonSplitOutput: []string{
input: "Newline ending\n", "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
splitOutput: []string{"Newline ending"}, "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
nonSplitOutput: []string{"Newline ending"}, "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: "不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說", "Message ending with new-line.": {
splitOutput: []string{ input: "Newline ending\n",
"不布人個我此而及單石業喜資富下 <clipped message>", splitOutput: []string{"Newline ending"},
"我河下日沒一我臺空達的常景便物 <clipped message>", nonSplitOutput: []string{"Newline ending"},
"沒為……子大我別名解成?生賣的 <clipped message>", },
"全直黑,我自我結毛分洲了世當, <clipped message>", "Long message containing UTF-8 multi-byte runes": {
"是政福那是東;斯說", input: "不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說",
}, splitOutput: []string{
nonSplitOutput: []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) { func TestGetSubLines(t *testing.T) {
for testname, testcase := range lineSplittingTestCases { 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) assert.Equalf(t, testcase.splitOutput, splitLines, "'%s' testcase should give expected lines with splitting.", testname)
for _, splitLine := range splitLines { for _, splitLine := range splitLines {
byteLength := len([]byte(splitLine)) 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) 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) 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") == "" { if os.Getenv("LOCAL_TEST") == "" {
t.Skip() t.Skip()
} }
input, err := ioutil.ReadFile("test.webp") input, err := ioutil.ReadFile("test.webp")
if err != nil { if err != nil {
t.Fail() t.Fail()
} }
d := &input d := &input
err = ConvertWebPToPNG(d) err = ConvertWebPToPNG(d)
if err != nil { if err != nil {
t.Fail() t.Fail()
} }
err = ioutil.WriteFile("test.png", *d, 0644)
err = ioutil.WriteFile("test.png", *d, 0o644) // nolint:gosec
if err != nil { if err != nil {
t.Fail() 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

@ -11,7 +11,6 @@ import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
"github.com/lrstanley/girc" "github.com/lrstanley/girc"
"github.com/missdeer/golib/ic"
"github.com/paulrosania/go-charset/charset" "github.com/paulrosania/go-charset/charset"
"github.com/saintfish/chardet" "github.com/saintfish/chardet"
@ -24,12 +23,12 @@ func (b *Birc) handleCharset(msg *config.Message) error {
if b.GetString("Charset") != "" { if b.GetString("Charset") != "" {
switch b.GetString("Charset") { switch b.GetString("Charset") {
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp": 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: default:
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
w, err := charset.NewWriter(b.GetString("Charset"), buf) w, err := charset.NewWriter(b.GetString("Charset"), buf)
if err != nil { 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 return err
} }
fmt.Fprint(w, msg.Text) fmt.Fprint(w, msg.Text)
@ -67,6 +66,20 @@ func (b *Birc) handleFiles(msg *config.Message) bool {
return true 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) { func (b *Birc) handleJoinPart(client *girc.Client, event girc.Event) {
if len(event.Params) == 0 { if len(event.Params) == 0 {
b.Log.Debugf("handleJoinPart: empty Params? %#v", event) 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 i := b.i
b.Nick = event.Params[0] b.Nick = event.Params[0]
i.Handlers.Add("PRIVMSG", b.handlePrivMsg) b.Log.Debug("Clearing handlers before adding in case of BNC reconnect")
i.Handlers.Add("CTCP_ACTION", b.handlePrivMsg) 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.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
i.Handlers.Add(girc.NOTICE, b.handleNotice) i.Handlers.AddBg(girc.NOTICE, b.handleNotice)
i.Handlers.Add("JOIN", b.handleJoinPart) i.Handlers.AddBg("JOIN", b.handleJoinPart)
i.Handlers.Add("PART", b.handleJoinPart) i.Handlers.AddBg("PART", b.handleJoinPart)
i.Handlers.Add("QUIT", b.handleJoinPart) i.Handlers.AddBg("QUIT", b.handleJoinPart)
i.Handlers.Add("KICK", b.handleJoinPart) i.Handlers.AddBg("KICK", b.handleJoinPart)
i.Handlers.Add("INVITE", b.handleInvite)
} }
func (b *Birc) handleNickServ() { func (b *Birc) handleNickServ() {
@ -181,7 +205,11 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Last(), event) b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Last(), event)
// set action 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 rmsg.Event = config.EventUserAction
} }
@ -212,7 +240,7 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
} }
switch mycharset { switch mycharset {
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp": 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: default:
r, err := charset.NewReader(mycharset, strings.NewReader(rmsg.Text)) r, err := charset.NewReader(mycharset, strings.NewReader(rmsg.Text))
if err != nil { if err != nil {
@ -229,6 +257,7 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
func (b *Birc) handleRunCommands() { func (b *Birc) handleRunCommands() {
for _, cmd := range b.GetStringSlice("RunCommands") { for _, cmd := range b.GetStringSlice("RunCommands") {
cmd = strings.ReplaceAll(cmd, "{BOTNICK}", b.Nick)
if err := b.i.Cmd.SendRaw(cmd); err != nil { if err := b.i.Cmd.SendRaw(cmd); err != nil {
b.Log.Errorf("RunCommands %s failed: %s", cmd, err) b.Log.Errorf("RunCommands %s failed: %s", cmd, err)
} }

View File

@ -2,6 +2,7 @@ package birc
import ( import (
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"hash/crc32" "hash/crc32"
"io/ioutil" "io/ioutil"
@ -30,6 +31,7 @@ type Birc struct {
Local chan config.Message // local queue for flood control Local chan config.Message // local queue for flood control
FirstConnection, authDone bool FirstConnection, authDone bool
MessageDelay, MessageQueue, MessageLength int MessageDelay, MessageQueue, MessageLength int
channels map[string]bool
*bridge.Config *bridge.Config
} }
@ -40,6 +42,8 @@ func New(cfg *bridge.Config) bridge.Bridger {
b.Nick = b.GetString("Nick") b.Nick = b.GetString("Nick")
b.names = make(map[string][]string) b.names = make(map[string][]string)
b.connected = make(chan error) b.connected = make(chan error)
b.channels = make(map[string]bool)
if b.GetInt("MessageDelay") == 0 { if b.GetInt("MessageDelay") == 0 {
b.MessageDelay = 1300 b.MessageDelay = 1300
} else { } else {
@ -69,6 +73,10 @@ func (b *Birc) Command(msg *config.Message) string {
} }
func (b *Birc) Connect() error { 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.Local = make(chan config.Message, b.MessageQueue+10)
b.Log.Infof("Connecting %s", b.GetString("Server")) b.Log.Infof("Connecting %s", b.GetString("Server"))
@ -112,6 +120,7 @@ func (b *Birc) Disconnect() error {
} }
func (b *Birc) JoinChannel(channel config.ChannelInfo) 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 // need to check if we have nickserv auth done before joining channels
for { for {
if b.authDone { if b.authDone {
@ -163,9 +172,9 @@ func (b *Birc) Send(msg config.Message) (string, error) {
} }
if b.GetBool("MessageSplit") { if b.GetBool("MessageSplit") {
msgLines = helper.GetSubLines(msg.Text, b.MessageLength) msgLines = helper.GetSubLines(msg.Text, b.MessageLength, b.GetString("MessageClipped"))
} else { } else {
msgLines = helper.GetSubLines(msg.Text, 0) msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
} }
for i := range msgLines { for i := range msgLines {
if len(b.Local) >= b.MessageQueue { if len(b.Local) >= b.MessageQueue {
@ -201,27 +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() { func (b *Birc) doSend() {
rate := time.Millisecond * time.Duration(b.MessageDelay) rate := time.Millisecond * time.Duration(b.MessageDelay)
throttle := time.NewTicker(rate) throttle := time.NewTicker(rate)
for msg := range b.Local { for msg := range b.Local {
<-throttle.C <-throttle.C
username := msg.Username username := msg.Username
if b.GetBool("Colornicks") && len(username) > 1 { // Optional support for the proposed RELAYMSG extension, described at
checksum := crc32.ChecksumIEEE([]byte(msg.Username)) // https://github.com/jlu5/ircv3-specifications/blob/master/extensions/relaymsg.md
colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes // nolint:nestif
username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username) if (b.i.HasCapability("overdrivenetworks.com/relaymsg") || b.i.HasCapability("draft/relaymsg")) &&
} b.GetBool("UseRelayMsg") {
username = sanitizeNick(username)
text := msg.Text
switch msg.Event { // Work around girc chomping leading commas on single word messages?
case config.EventUserAction: if strings.HasPrefix(text, ":") && !strings.ContainsRune(text, ' ') {
b.i.Cmd.Action(msg.Channel, username+msg.Text) text = ":" + text
case config.EventNoticeIRC: }
b.Log.Debugf("Sending notice to channel %s", msg.Channel)
b.i.Cmd.Notice(msg.Channel, username+msg.Text) if msg.Event == config.EventUserAction {
default: b.i.Cmd.SendRawf("RELAYMSG %s %s :\x01ACTION %s\x01", msg.Channel, username, text) //nolint:errcheck
b.Log.Debugf("Sending to channel %s", msg.Channel) } else {
b.i.Cmd.Message(msg.Channel, username+msg.Text) b.Log.Debugf("Sending RELAYMSG to channel %s: nick=%s", msg.Channel, username)
b.i.Cmd.SendRawf("RELAYMSG %s %s :%s", msg.Channel, username, text) //nolint:errcheck
}
} else {
if b.GetBool("Colornicks") {
checksum := crc32.ChecksumIEEE([]byte(msg.Username))
colorCode := checksum%14 + 2 // quick fix - prevent white or black color codes
username = fmt.Sprintf("\x03%02d%s\x0F", colorCode, msg.Username)
}
switch msg.Event {
case config.EventUserAction:
b.i.Cmd.Action(msg.Channel, username+msg.Text)
case config.EventNoticeIRC:
b.Log.Debugf("Sending notice to channel %s", msg.Channel)
b.i.Cmd.Notice(msg.Channel, username+msg.Text)
default:
b.Log.Debugf("Sending to channel %s", msg.Channel)
b.i.Cmd.Message(msg.Channel, username+msg.Text)
}
} }
} }
} }
@ -236,8 +276,11 @@ func (b *Birc) getClient() (*girc.Client, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
user := b.GetString("UserName")
if user == "" {
user = b.GetString("Nick")
}
// fix strict user handling of girc // fix strict user handling of girc
user := b.GetString("Nick")
for !girc.IsValidUser(user) { for !girc.IsValidUser(user) {
if len(user) == 1 || len(user) == 0 { if len(user) == 1 || len(user) == 0 {
user = "matterbridge" user = "matterbridge"
@ -245,6 +288,10 @@ func (b *Birc) getClient() (*girc.Client, error) {
} }
user = user[1:] user = user[1:]
} }
realName := b.GetString("RealName")
if realName == "" {
realName = b.GetString("Nick")
}
debug := ioutil.Discard debug := ioutil.Discard
if b.GetInt("DebugLevel") == 2 { if b.GetInt("DebugLevel") == 2 {
@ -258,19 +305,26 @@ func (b *Birc) getClient() (*girc.Client, error) {
b.Log.Debugf("setting pingdelay to %s", pingDelay) b.Log.Debugf("setting pingdelay to %s", pingDelay)
tlsConfig, err := b.getTLSConfig()
if err != nil {
return nil, err
}
i := girc.New(girc.Config{ i := girc.New(girc.Config{
Server: server, Server: server,
ServerPass: b.GetString("Password"), ServerPass: b.GetString("Password"),
Port: port, Port: port,
Nick: b.GetString("Nick"), Nick: b.GetString("Nick"),
User: user, User: user,
Name: b.GetString("Nick"), Name: realName,
SSL: b.GetBool("UseTLS"), SSL: b.GetBool("UseTLS"),
TLSConfig: &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, //nolint:gosec Bind: b.GetString("Bind"),
TLSConfig: tlsConfig,
PingDelay: pingDelay, PingDelay: pingDelay,
// skip gIRC internal rate limiting, since we have our own throttling // skip gIRC internal rate limiting, since we have our own throttling
AllowFlood: true, AllowFlood: true,
Debug: debug, Debug: debug,
SupportedCaps: map[string][]string{"overdrivenetworks.com/relaymsg": nil, "draft/relaymsg": nil},
}) })
return i, nil return i, nil
} }
@ -280,12 +334,16 @@ func (b *Birc) endNames(client *girc.Client, event girc.Event) {
sort.Strings(b.names[channel]) sort.Strings(b.names[channel])
maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow() maxNamesPerPost := (300 / b.nicksPerRow()) * b.nicksPerRow()
for len(b.names[channel]) > maxNamesPerPost { for len(b.names[channel]) > maxNamesPerPost {
b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost]), b.Remote <- config.Message{
Channel: channel, Account: b.Account} Username: b.Nick, Text: b.formatnicks(b.names[channel][0:maxNamesPerPost]),
Channel: channel, Account: b.Account,
}
b.names[channel] = b.names[channel][maxNamesPerPost:] b.names[channel] = b.names[channel][maxNamesPerPost:]
} }
b.Remote <- config.Message{Username: b.Nick, Text: b.formatnicks(b.names[channel]), b.Remote <- config.Message{
Channel: channel, Account: b.Account} Username: b.Nick, Text: b.formatnicks(b.names[channel]),
Channel: channel, Account: b.Account,
}
b.names[channel] = nil b.names[channel] = nil
b.i.Handlers.Clear(girc.RPL_NAMREPLY) b.i.Handlers.Clear(girc.RPL_NAMREPLY)
b.i.Handlers.Clear(girc.RPL_ENDOFNAMES) b.i.Handlers.Clear(girc.RPL_ENDOFNAMES)
@ -304,7 +362,18 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool {
return true return true
} }
// don't forward message from ourself // 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 true
} }
return false return false
@ -324,3 +393,23 @@ func (b *Birc) storeNames(client *girc.Client, event girc.Event) {
func (b *Birc) formatnicks(nicks []string) string { func (b *Birc) formatnicks(nicks []string) string {
return strings.Join(nicks, ", ") + " currently on IRC" 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

@ -3,11 +3,12 @@ package bmatrix
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"html" "html"
"strings" "strings"
"time" "time"
matrix "github.com/matrix-org/gomatrix" matrix "github.com/matterbridge/gomatrix"
) )
func newMatrixUsername(username string) *matrixUsername { func newMatrixUsername(username string) *matrixUsername {
@ -50,7 +51,7 @@ func interface2Struct(in interface{}, out interface{}) error {
return json.Unmarshal(jsonObj, out) return json.Unmarshal(jsonObj, out)
} }
// getDisplayName retrieves the displayName for mxid, querying the homserver if the mxid is not in the cache. // getDisplayName retrieves the displayName for mxid, querying the homeserver if the mxid is not in the cache.
func (b *Bmatrix) getDisplayName(mxid string) string { func (b *Bmatrix) getDisplayName(mxid string) string {
if b.GetBool("UseUserName") { if b.GetBool("UseUserName") {
return mxid[1:] return mxid[1:]
@ -82,20 +83,36 @@ func (b *Bmatrix) getDisplayName(mxid string) string {
func (b *Bmatrix) cacheDisplayName(mxid string, displayName string) string { func (b *Bmatrix) cacheDisplayName(mxid string, displayName string) string {
now := time.Now() now := time.Now()
// scan to delete old entries, to stop memory usage from becoming too high with old entries // 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{} toDelete := []string{}
b.RLock() conflict := false
for k, v := range b.NicknameMap {
if now.Sub(v.lastUpdated) > 10*time.Minute {
toDelete = append(toDelete, k)
}
}
b.RUnlock()
b.Lock() 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 { for _, v := range toDelete {
delete(b.NicknameMap, v) delete(b.NicknameMap, v)
} }
b.NicknameMap[mxid] = NicknameCacheEntry{ b.NicknameMap[mxid] = NicknameCacheEntry{
displayName: displayName, displayName: displayName,
lastUpdated: now, lastUpdated: now,
@ -164,3 +181,35 @@ func (b *Bmatrix) getAvatarURL(sender string) string {
return url 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

@ -12,7 +12,7 @@ import (
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
matrix "github.com/matrix-org/gomatrix" matrix "github.com/matterbridge/gomatrix"
) )
var ( var (
@ -30,6 +30,7 @@ type Bmatrix struct {
UserID string UserID string
NicknameMap map[string]NicknameCacheEntry NicknameMap map[string]NicknameCacheEntry
RoomMap map[string]string RoomMap map[string]string
rateMutex sync.RWMutex
sync.RWMutex sync.RWMutex
*bridge.Config *bridge.Config
} }
@ -47,8 +48,10 @@ type matrixUsername struct {
// SubTextMessage represents the new content of the message in edit messages. // SubTextMessage represents the new content of the message in edit messages.
type SubTextMessage struct { type SubTextMessage struct {
MsgType string `json:"msgtype"` MsgType string `json:"msgtype"`
Body string `json:"body"` 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. // MessageRelation explains how the current message relates to a previous message.
@ -64,6 +67,19 @@ type EditedMessage struct {
matrix.TextMessage 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 { func New(cfg *bridge.Config) bridge.Bridger {
b := &Bmatrix{Config: cfg} b := &Bmatrix{Config: cfg}
b.RoomMap = make(map[string]string) b.RoomMap = make(map[string]string)
@ -74,22 +90,33 @@ func New(cfg *bridge.Config) bridge.Bridger {
func (b *Bmatrix) Connect() error { func (b *Bmatrix) Connect() error {
var err error var err error
b.Log.Infof("Connecting %s", b.GetString("Server")) b.Log.Infof("Connecting %s", b.GetString("Server"))
b.mc, err = matrix.NewClient(b.GetString("Server"), "", "") if b.GetString("MxID") != "" && b.GetString("Token") != "" {
if err != nil { b.mc, err = matrix.NewClient(
return err 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"),
Identifier: matrix.NewUserIdentifier(b.GetString("Login")),
})
if err != nil {
return err
}
b.mc.SetCredentials(resp.UserID, resp.AccessToken)
b.UserID = resp.UserID
b.Log.Info("Connection succeeded")
go b.handlematrix() go b.handlematrix()
return nil return nil
} }
@ -99,25 +126,18 @@ func (b *Bmatrix) Disconnect() error {
} }
func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error { func (b *Bmatrix) JoinChannel(channel config.ChannelInfo) error {
retry: return b.retry(func() error {
resp, err := b.mc.JoinRoom(channel.Name, "", nil) resp, err := b.mc.JoinRoom(channel.Name, "", nil)
if err != nil { if err != nil {
httpErr := handleError(err) return err
if httpErr.Errcode == "M_LIMIT_EXCEEDED" {
b.Log.Infof("getting ratelimited by matrix, sleeping approx %d seconds before joining %s", httpErr.RetryAfterMs/1000, channel.Name)
time.Sleep((time.Duration(httpErr.RetryAfterMs) * time.Millisecond))
goto retry
} }
return err b.Lock()
} b.RoomMap[resp.RoomID] = channel.Name
b.Unlock()
b.Lock() return nil
b.RoomMap[resp.RoomID] = channel.Name })
b.Unlock()
return nil
} }
func (b *Bmatrix) Send(msg config.Message) (string, error) { func (b *Bmatrix) Send(msg config.Message) (string, error) {
@ -128,18 +148,59 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
username := newMatrixUsername(msg.Username) 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 // Make a action /me of the message
if msg.Event == config.EventUserAction { if msg.Event == config.EventUserAction {
m := matrix.TextMessage{ m := matrix.TextMessage{
MsgType: "m.emote", MsgType: "m.emote",
Body: username.plain + msg.Text, Body: body,
FormattedBody: username.formatted + msg.Text, FormattedBody: formattedBody,
Format: "org.matrix.custom.html",
} }
resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m)
if err != nil { if b.GetBool("HTMLDisable") {
return "", err 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 // Delete message
@ -147,17 +208,34 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
if msg.ID == "" { if msg.ID == "" {
return "", nil return "", nil
} }
resp, err := b.mc.RedactEvent(channel, msg.ID, &matrix.ReqRedact{})
if err != nil { msgID := ""
return "", err
} err := b.retry(func() error {
return resp.EventID, err 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 // Upload a file if it exists
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) { 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) b.Log.Errorf("sendText failed: %s", err)
} }
} }
@ -169,25 +247,39 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
// Edit message if we have an ID // Edit message if we have an ID
if msg.ID != "" { if msg.ID != "" {
rmsg := EditedMessage{TextMessage: matrix.TextMessage{ rmsg := EditedMessage{
Body: username.plain + msg.Text, TextMessage: matrix.TextMessage{
MsgType: "m.text", Body: body,
}} MsgType: "m.text",
if b.GetBool("HTMLDisable") { Format: "org.matrix.custom.html",
rmsg.TextMessage.FormattedBody = username.formatted + "* " + msg.Text FormattedBody: formattedBody,
} else { },
rmsg.Format = "org.matrix.custom.html"
rmsg.TextMessage.FormattedBody = username.formatted + "* " + helper.ParseMarkdown(msg.Text)
} }
rmsg.NewContent = SubTextMessage{ rmsg.NewContent = SubTextMessage{
Body: rmsg.TextMessage.Body, Body: rmsg.TextMessage.Body,
MsgType: "m.text", 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{ rmsg.RelatedTo = MessageRelation{
EventID: msg.ID, EventID: msg.ID,
Type: "m.replace", Type: "m.replace",
} }
_, err := b.mc.SendMessageEvent(channel, "m.room.message", rmsg)
err := b.retry(func() error {
_, err := b.mc.SendMessageEvent(channel, "m.room.message", rmsg)
return err
})
if err != nil { if err != nil {
return "", err return "", err
} }
@ -199,29 +291,104 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
if msg.Event == config.EventJoinLeave { if msg.Event == config.EventJoinLeave {
m := matrix.TextMessage{ m := matrix.TextMessage{
MsgType: "m.notice", MsgType: "m.notice",
Body: username.plain + msg.Text, Body: body,
FormattedBody: username.formatted + msg.Text, FormattedBody: formattedBody,
Format: "org.matrix.custom.html",
} }
resp, err := b.mc.SendMessageEvent(channel, "m.room.message", m)
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 { if err != nil {
return "", err return "", err
} }
return resp.EventID, err
}
if msg.ParentValid() {
m := ReplyMessage{
TextMessage: matrix.TextMessage{
MsgType: "m.text",
Body: body,
FormattedBody: formattedBody,
Format: "org.matrix.custom.html",
},
}
if b.GetBool("HTMLDisable") {
m.TextMessage.Format = ""
m.TextMessage.FormattedBody = ""
}
m.RelatedTo = InReplyToRelation{
InReplyTo: InReplyToRelationContent{
EventID: msg.ParentID,
},
}
var (
resp *matrix.RespSendEvent
err error
)
err = b.retry(func() error {
resp, err = b.mc.SendMessageEvent(channel, "m.room.message", m)
return err
})
if err != nil {
return "", err
}
return resp.EventID, err return resp.EventID, err
} }
if b.GetBool("HTMLDisable") { if b.GetBool("HTMLDisable") {
resp, err := b.mc.SendText(channel, username.plain+msg.Text) var (
resp *matrix.RespSendEvent
err error
)
err = b.retry(func() error {
resp, err = b.mc.SendText(channel, body)
return err
})
if err != nil { if err != nil {
return "", err return "", err
} }
return resp.EventID, err return resp.EventID, err
} }
// Post normal message with HTML support (eg riot.im) // Post normal message with HTML support (eg riot.im)
resp, err := b.mc.SendFormattedText(channel, username.plain+msg.Text, username.formatted+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 { if err != nil {
return "", err return "", err
} }
return resp.EventID, err return resp.EventID, err
} }
@ -232,6 +399,9 @@ func (b *Bmatrix) handlematrix() {
syncer.OnEventType("m.room.member", b.handleMemberChange) syncer.OnEventType("m.room.member", b.handleMemberChange)
go func() { go func() {
for { for {
if b == nil {
return
}
if err := b.mc.Sync(); err != nil { if err := b.mc.Sync(); err != nil {
b.Log.Println("Sync() returned ", err) b.Log.Println("Sync() returned ", err)
} }
@ -269,6 +439,38 @@ func (b *Bmatrix) handleEdit(ev *matrix.Event, rmsg config.Message) bool {
return true 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) { 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 // 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 ev.Content["membership"] == "join" {
@ -299,13 +501,6 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
Avatar: b.getAvatarURL(ev.Sender), Avatar: b.getAvatarURL(ev.Sender),
} }
// 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
}
// Remove homeserver suffix if configured // Remove homeserver suffix if configured
if b.GetBool("NoHomeServerSuffix") { if b.GetBool("NoHomeServerSuffix") {
re := regexp.MustCompile("(.*?):.*") re := regexp.MustCompile("(.*?):.*")
@ -321,6 +516,13 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
return 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 // Do we have a /me action
if ev.Content["msgtype"].(string) == "m.emote" { if ev.Content["msgtype"].(string) == "m.emote" {
rmsg.Event = config.EventUserAction rmsg.Event = config.EventUserAction
@ -331,6 +533,11 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
return return
} }
// Is it a reply?
if b.handleReply(ev, rmsg) {
return
}
// Do we have attachments // Do we have attachments
if b.containsAttachment(ev.Content) { if b.containsAttachment(ev.Content) {
err := b.handleDownloadFile(&rmsg, ev.Content) err := b.handleDownloadFile(&rmsg, ev.Content)
@ -341,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.Log.Debugf("<= Sending message from %s on %s to gateway", ev.Sender, b.Account)
b.Remote <- rmsg 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())
}
} }
} }
@ -420,13 +632,25 @@ func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *conf
sp := strings.Split(fi.Name, ".") sp := strings.Split(fi.Name, ".")
mtype := mime.TypeByExtension("." + sp[len(sp)-1]) mtype := mime.TypeByExtension("." + sp[len(sp)-1])
// image and video uploads send no username, we have to do this ourself here #715 // image and video uploads send no username, we have to do this ourself here #715
_, err := b.mc.SendFormattedText(channel, username.plain+fi.Comment, username.formatted+fi.Comment) err := b.retry(func() error {
_, err := b.mc.SendFormattedText(channel, username.plain+fi.Comment, username.formatted+fi.Comment)
return err
})
if err != nil { if err != nil {
b.Log.Errorf("file comment failed: %#v", err) b.Log.Errorf("file comment failed: %#v", err)
} }
b.Log.Debugf("uploading file: %s %s", fi.Name, mtype) 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 { if err != nil {
b.Log.Errorf("file upload failed: %#v", err) b.Log.Errorf("file upload failed: %#v", err)
return return
@ -435,40 +659,56 @@ func (b *Bmatrix) handleUploadFile(msg *config.Message, channel string, fi *conf
switch { switch {
case strings.Contains(mtype, "video"): case strings.Contains(mtype, "video"):
b.Log.Debugf("sendVideo %s", res.ContentURI) 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 { if err != nil {
b.Log.Errorf("sendVideo failed: %#v", err) b.Log.Errorf("sendVideo failed: %#v", err)
} }
case strings.Contains(mtype, "image"): case strings.Contains(mtype, "image"):
b.Log.Debugf("sendImage %s", res.ContentURI) 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 { if err != nil {
b.Log.Errorf("sendImage failed: %#v", err) b.Log.Errorf("sendImage failed: %#v", err)
} }
case strings.Contains(mtype, "audio"): case strings.Contains(mtype, "audio"):
b.Log.Debugf("sendAudio %s", res.ContentURI) b.Log.Debugf("sendAudio %s", res.ContentURI)
_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.AudioMessage{ err = b.retry(func() error {
MsgType: "m.audio", _, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.AudioMessage{
Body: fi.Name, MsgType: "m.audio",
URL: res.ContentURI, Body: fi.Name,
Info: matrix.AudioInfo{ URL: res.ContentURI,
Mimetype: mtype, Info: matrix.AudioInfo{
Size: uint(len(*fi.Data)), Mimetype: mtype,
}, Size: uint(len(*fi.Data)),
},
})
return err
}) })
if err != nil { if err != nil {
b.Log.Errorf("sendAudio failed: %#v", err) b.Log.Errorf("sendAudio failed: %#v", err)
} }
default: default:
b.Log.Debugf("sendFile %s", res.ContentURI) b.Log.Debugf("sendFile %s", res.ContentURI)
_, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.FileMessage{ err = b.retry(func() error {
MsgType: "m.file", _, err = b.mc.SendMessageEvent(channel, "m.room.message", matrix.FileMessage{
Body: fi.Name, MsgType: "m.file",
URL: res.ContentURI, Body: fi.Name,
Info: matrix.FileInfo{ URL: res.ContentURI,
Mimetype: mtype, Info: matrix.FileInfo{
Size: uint(len(*fi.Data)), Mimetype: mtype,
}, Size: uint(len(*fi.Data)),
},
})
return err
}) })
if err != nil { if err != nil {
b.Log.Errorf("sendFile failed: %#v", err) b.Log.Errorf("sendFile failed: %#v", err)

View File

@ -1,10 +1,12 @@
package bmattermost package bmattermost
import ( import (
"context"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterclient" "github.com/matterbridge/matterclient"
"github.com/mattermost/mattermost-server/v5/model" "github.com/mattermost/mattermost/server/public/model"
) )
// handleDownloadAvatar downloads the avatar of userid from channel // 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{}), Extra: make(map[string][]interface{}),
} }
if _, ok := b.avatarMap[userid]; !ok { if _, ok := b.avatarMap[userid]; !ok {
data, resp := b.mc.Client.GetProfileImage(userid, "") var (
if resp.Error != nil { data []byte
b.Log.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error) 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 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 { if err != nil {
b.Log.Error(err) b.Log.Error(err)
return 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 { func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error {
url, _ := b.mc.Client.GetFileLink(id) url, _, _ := b.mc.Client.GetFileLink(context.TODO(), id)
finfo, resp := b.mc.Client.GetFileInfo(id) finfo, _, err := b.mc.Client.GetFileInfo(context.TODO(), id)
if resp.Error != nil {
return resp.Error
}
err := helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General)
if err != nil { if err != nil {
return err return err
} }
data, resp := b.mc.Client.DownloadFile(id, true) err = helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General)
if resp.Error != nil { if err != nil {
return resp.Error 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) helper.HandleDownloadData(b.Log, rmsg, finfo.Name, rmsg.Text, url, &data, b.General)
return nil return nil
@ -86,18 +93,24 @@ func (b *Bmattermost) handleMatter() {
} }
} }
//nolint:cyclop
func (b *Bmattermost) handleMatterClient(messages chan *config.Message) { func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
for message := range b.mc.MessageChan { 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) { if b.skipMessage(message) {
b.Log.Debugf("Skipped message: %#v", message) b.Log.Debugf("Skipped message: %#v", message)
continue 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) // only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" { 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) b.Log.Debugf("== Receiving event %#v", message)
@ -105,10 +118,10 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
rmsg := &config.Message{ rmsg := &config.Message{
Username: message.Username, Username: message.Username,
UserID: message.UserID, UserID: message.UserID,
Channel: message.Channel, Channel: channelName,
Text: message.Text, Text: message.Text,
ID: message.Post.Id, ID: message.Post.Id,
ParentID: message.Post.ParentId, ParentID: message.Post.RootId, // ParentID is obsolete with mattermost
Extra: make(map[string][]interface{}), Extra: make(map[string][]interface{}),
} }
@ -116,11 +129,11 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
b.handleProps(rmsg, message) b.handleProps(rmsg, message)
// create a text for bridges that don't support native editing // 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") 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 rmsg.Event = config.EventMsgDelete
} }
@ -132,8 +145,10 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
} }
// Use nickname instead of username if defined // Use nickname instead of username if defined
if nick := b.mc.GetNickName(rmsg.UserID); nick != "" { if !b.GetBool("useusername") {
rmsg.Username = nick if nick := b.mc.GetNickName(rmsg.UserID); nick != "" {
rmsg.Username = nick
}
} }
messages <- rmsg messages <- rmsg
@ -144,6 +159,7 @@ func (b *Bmattermost) handleMatterHook(messages chan *config.Message) {
for { for {
message := b.mh.Receive() message := b.mh.Receive()
b.Log.Debugf("Receiving from matterhook %#v", message) b.Log.Debugf("Receiving from matterhook %#v", message)
messages <- &config.Message{ messages <- &config.Message{
UserID: message.UserID, UserID: message.UserID,
Username: message.UserName, 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) { func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
var err error var err error
var res, id string var res, id string
channelID := b.mc.GetChannelId(msg.Channel, b.TeamID) channelID := b.getChannelID(msg.Channel)
for _, f := range msg.Extra["file"] { for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo) fi := f.(config.FileInfo)
id, err = b.mc.UploadFile(*fi.Data, channelID, fi.Name) 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 return res, err
} }
//nolint:forcetypeassert
func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Message) { func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Message) {
props := message.Post.Props props := message.Post.Props
if props == nil { if props == nil {
@ -183,16 +199,18 @@ func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Me
} }
if _, ok := props["attachments"].([]interface{}); ok { if _, ok := props["attachments"].([]interface{}); ok {
rmsg.Extra["attachments"] = props["attachments"].([]interface{}) rmsg.Extra["attachments"] = props["attachments"].([]interface{})
if rmsg.Text == "" { if rmsg.Text != "" {
for _, attachment := range rmsg.Extra["attachments"] { return
attach := attachment.(map[string]interface{}) }
if attach["text"].(string) != "" {
rmsg.Text += attach["text"].(string) for _, attachment := range rmsg.Extra["attachments"] {
continue attach := attachment.(map[string]interface{})
} if attach["text"].(string) != "" {
if attach["fallback"].(string) != "" { rmsg.Text += attach["text"].(string)
rmsg.Text += attach["fallback"].(string) continue
} }
if attach["fallback"].(string) != "" {
rmsg.Text += attach["fallback"].(string)
} }
} }
} }

View File

@ -1,13 +1,14 @@
package bmattermost package bmattermost
import ( import (
"net/http"
"strings" "strings"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterclient"
"github.com/42wim/matterbridge/matterhook" "github.com/42wim/matterbridge/matterhook"
"github.com/mattermost/mattermost-server/v5/model" "github.com/matterbridge/matterclient"
"github.com/mattermost/mattermost/server/public/model"
) )
func (b *Bmattermost) doConnectWebhookBind() error { func (b *Bmattermost) doConnectWebhookBind() error {
@ -15,8 +16,10 @@ func (b *Bmattermost) doConnectWebhookBind() error {
case b.GetString("WebhookURL") != "": case b.GetString("WebhookURL") != "":
b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)") b.Log.Info("Connecting using webhookurl (sending) and webhookbindaddress (receiving)")
b.mh = matterhook.New(b.GetString("WebhookURL"), b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), matterhook.Config{
BindAddress: b.GetString("WebhookBindAddress")}) InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
BindAddress: b.GetString("WebhookBindAddress"),
})
case b.GetString("Token") != "": case b.GetString("Token") != "":
b.Log.Info("Connecting using token (sending)") b.Log.Info("Connecting using token (sending)")
err := b.apiLogin() err := b.apiLogin()
@ -32,8 +35,10 @@ func (b *Bmattermost) doConnectWebhookBind() error {
default: default:
b.Log.Info("Connecting using webhookbindaddress (receiving)") b.Log.Info("Connecting using webhookbindaddress (receiving)")
b.mh = matterhook.New(b.GetString("WebhookURL"), b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), matterhook.Config{
BindAddress: b.GetString("WebhookBindAddress")}) InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
BindAddress: b.GetString("WebhookBindAddress"),
})
} }
return nil return nil
} }
@ -41,8 +46,10 @@ func (b *Bmattermost) doConnectWebhookBind() error {
func (b *Bmattermost) doConnectWebhookURL() error { func (b *Bmattermost) doConnectWebhookURL() error {
b.Log.Info("Connecting using webhookurl (sending)") b.Log.Info("Connecting using webhookurl (sending)")
b.mh = matterhook.New(b.GetString("WebhookURL"), b.mh = matterhook.New(b.GetString("WebhookURL"),
matterhook.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), matterhook.Config{
DisableServer: true}) InsecureSkipVerify: b.GetBool("SkipTLSVerify"),
DisableServer: true,
})
if b.GetString("Token") != "" { if b.GetString("Token") != "" {
b.Log.Info("Connecting using token (receiving)") b.Log.Info("Connecting using token (receiving)")
err := b.apiLogin() err := b.apiLogin()
@ -59,13 +66,14 @@ func (b *Bmattermost) doConnectWebhookURL() error {
return nil return nil
} }
//nolint:wrapcheck
func (b *Bmattermost) apiLogin() error { func (b *Bmattermost) apiLogin() error {
password := b.GetString("Password") password := b.GetString("Password")
if b.GetString("Token") != "" { if b.GetString("Token") != "" {
password = "token=" + 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") { if b.GetBool("debug") {
b.mc.SetLogLevel("debug") b.mc.SetLogLevel("debug")
} }
@ -73,14 +81,13 @@ func (b *Bmattermost) apiLogin() error {
b.mc.SkipVersionCheck = b.GetBool("SkipVersionCheck") b.mc.SkipVersionCheck = b.GetBool("SkipVersionCheck")
b.mc.NoTLS = b.GetBool("NoTLS") b.mc.NoTLS = b.GetBool("NoTLS")
b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server")) 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 return err
} }
b.Log.Info("Connection succeeded") b.Log.Info("Connection succeeded")
b.TeamID = b.mc.GetTeamId() b.TeamID = b.mc.GetTeamID()
go b.mc.WsReceiver()
go b.mc.StatusLoop()
return nil return nil
} }
@ -113,6 +120,7 @@ func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
if b.GetBool("PrefixMessagesWithNick") { if b.GetBool("PrefixMessagesWithNick") {
msg.Text = msg.Username + msg.Text msg.Text = msg.Username + msg.Text
} }
if msg.Extra != nil { if msg.Extra != nil {
// this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE // this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE
for _, rmsg := range helper.HandleExtra(&msg, b.General) { 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"] { for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo) fi := f.(config.FileInfo)
if fi.URL != "" { 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 // skipMessages returns true if this message should not be handled
//
//nolint:gocyclo,cyclop
func (b *Bmattermost) skipMessage(message *matterclient.Message) bool { func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
// Handle join/leave // Handle join/leave
if message.Type == "system_join_leave" || skipJoinMessageTypes := map[string]struct{}{
message.Type == "system_join_channel" || "system_join_leave": {}, // deprecated for system_add_to_channel
message.Type == "system_leave_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") { if b.GetBool("nosendjoinpart") {
return true 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.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
b.Remote <- config.Message{ b.Remote <- config.Message{
Username: "system", Username: "system",
Text: message.Text, Text: message.Text,
Channel: message.Channel, Channel: channelName,
Account: b.Account, Account: b.Account,
Event: config.EventJoinLeave, Event: config.EventJoinLeave,
} }
@ -183,7 +209,7 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
} }
// Handle edited messages // 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 return true
} }
@ -196,13 +222,14 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
// Ignore messages sent from matterbridge // Ignore messages sent from matterbridge
if message.Post.Props != nil { if message.Post.Props != nil {
if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok { 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 return true
} }
} }
// Ignore messages sent from a user logged in as the bot // Ignore messages sent from a user logged in as the bot
if b.mc.User.Username == message.Username { if b.mc.User.Username == message.Username {
b.Log.Debug("message from same user as bot, ignoring")
return true return true
} }
@ -212,14 +239,56 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
} }
// ignore messages from other teams than ours // 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 return true
} }
// only handle posted, edited or deleted events // only handle posted, edited or deleted events
if !(message.Raw.Event == "posted" || message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED || if !(message.Raw.EventType() == "posted" || message.Raw.EventType() == model.WebsocketEventPostEdited ||
message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED) { message.Raw.EventType() == model.WebsocketEventPostDeleted) {
return true return true
} }
return false 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 package bmattermost
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"strings"
"sync"
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterclient"
"github.com/42wim/matterbridge/matterhook" "github.com/42wim/matterbridge/matterhook"
"github.com/matterbridge/matterclient"
"github.com/rs/xid" "github.com/rs/xid"
) )
type Bmattermost struct { type Bmattermost struct {
mh *matterhook.Client mh *matterhook.Client
mc *matterclient.MMClient mc *matterclient.Client
v6 bool
uuid string uuid string
TeamID string TeamID string
*bridge.Config *bridge.Config
avatarMap map[string]string avatarMap map[string]string
channelsMutex sync.RWMutex
channelInfoMap map[string]*config.ChannelInfo
} }
const mattermostPlugin = "mattermost.plugin" const mattermostPlugin = "mattermost.plugin"
func New(cfg *bridge.Config) bridge.Bridger { 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() b.uuid = xid.New().String()
return b return b
} }
@ -37,6 +50,13 @@ func (b *Bmattermost) Connect() error {
if b.Account == mattermostPlugin { if b.Account == mattermostPlugin {
return nil return nil
} }
if strings.HasPrefix(b.getVersion(), "6.") || strings.HasPrefix(b.getVersion(), "7.") {
if !b.v6 {
b.v6 = true
}
}
if b.GetString("WebhookBindAddress") != "" { if b.GetString("WebhookBindAddress") != "" {
if err := b.doConnectWebhookBind(); err != nil { if err := b.doConnectWebhookBind(); err != nil {
return err return err
@ -60,6 +80,7 @@ func (b *Bmattermost) Connect() error {
go b.handleMatter() go b.handleMatter()
case b.GetString("Login") != "": case b.GetString("Login") != "":
b.Log.Info("Connecting using login/password (sending and receiving)") b.Log.Info("Connecting using login/password (sending and receiving)")
b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
err := b.apiLogin() err := b.apiLogin()
if err != nil { if err != nil {
return err return err
@ -81,14 +102,21 @@ func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error {
if b.Account == mattermostPlugin { if b.Account == mattermostPlugin {
return nil return nil
} }
b.channelsMutex.Lock()
b.channelInfoMap[channel.ID] = &channel
b.channelsMutex.Unlock()
// we can only join channels using the API // we can only join channels using the API
if b.GetString("WebhookURL") == "" && b.GetString("WebhookBindAddress") == "" { if b.GetString("WebhookURL") == "" && b.GetString("WebhookBindAddress") == "" {
id := b.mc.GetChannelId(channel.Name, b.TeamID) id := b.getChannelID(channel.Name)
if id == "" { if id == "" {
return fmt.Errorf("Could not find channel ID for channel %s", channel.Name) return fmt.Errorf("Could not find channel ID for channel %s", channel.Name)
} }
return b.mc.JoinChannel(id) return b.mc.JoinChannel(id)
} }
return nil return nil
} }
@ -118,19 +146,31 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
if msg.ID == "" { if msg.ID == "" {
return "", nil return "", nil
} }
return msg.ID, b.mc.DeleteMessage(msg.ID) return msg.ID, b.mc.DeleteMessage(msg.ID)
} }
// Handle prefix hint for unthreaded messages. // Handle prefix hint for unthreaded messages.
if msg.ParentID == "msg-parent-not-found" { if msg.ParentNotFound() {
msg.ParentID = "" msg.ParentID = ""
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text) 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 // Upload a file if it exists
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) { 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) b.Log.Errorf("PostMessage failed: %s", err)
} }
} }
@ -150,5 +190,5 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
} }
// Post normal message // 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)
} }

View File

@ -19,8 +19,10 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
var defaultScopes = []string{"openid", "profile", "offline_access", "Group.Read.All", "Group.ReadWrite.All"} var (
var attachRE = regexp.MustCompile(`<attachment id=.*?attachment>`) defaultScopes = []string{"openid", "profile", "offline_access", "Group.Read.All", "Group.ReadWrite.All"}
attachRE = regexp.MustCompile(`<attachment id=.*?attachment>`)
)
type Bmsteams struct { type Bmsteams struct {
gc *msgraph.GraphServiceRequestBuilder gc *msgraph.GraphServiceRequestBuilder
@ -50,7 +52,7 @@ func (b *Bmsteams) Connect() error {
b.Log.Errorf("Couldn't save sessionfile in %s: %s", tokenCachePath, err) b.Log.Errorf("Couldn't save sessionfile in %s: %s", tokenCachePath, err)
} }
// make file readable only for matterbridge user // make file readable only for matterbridge user
err = os.Chmod(tokenCachePath, 0600) err = os.Chmod(tokenCachePath, 0o600)
if err != nil { if err != nil {
b.Log.Errorf("Couldn't change permissions for %s: %s", tokenCachePath, err) b.Log.Errorf("Couldn't change permissions for %s: %s", tokenCachePath, err)
} }
@ -86,13 +88,16 @@ func (b *Bmsteams) JoinChannel(channel config.ChannelInfo) error {
func (b *Bmsteams) Send(msg config.Message) (string, error) { func (b *Bmsteams) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg) b.Log.Debugf("=> Receiving %#v", msg)
if msg.ParentID != "" && msg.ParentID != "msg-parent-not-found" { if msg.ParentValid() {
return b.sendReply(msg) return b.sendReply(msg)
} }
if msg.ParentID == "msg-parent-not-found" {
// Handle prefix hint for unthreaded messages.
if msg.ParentNotFound() {
msg.ParentID = "" msg.ParentID = ""
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text) msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
} }
ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(msg.Channel).Messages().Request() ct := b.gc.Teams().ID(b.GetString("TeamID")).Channels().ID(msg.Channel).Messages().Request()
text := msg.Username + msg.Text text := msg.Username + msg.Text
content := &msgraph.ItemBody{Content: &text} content := &msgraph.ItemBody{Content: &text}
@ -165,7 +170,7 @@ func (b *Bmsteams) poll(channelName string) error {
} }
// skip non-user message for now. // skip non-user message for now.
if msg.From.User == nil { if msg.From == nil || msg.From.User == nil {
continue continue
} }

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

View File

@ -19,6 +19,12 @@ func (b *Bmumble) handleTextMessage(event *gumble.TextMessageEvent) {
if event.TextMessage.Sender != nil { if event.TextMessage.Sender != nil {
sender = event.TextMessage.Sender.Name 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 // Convert Mumble HTML messages to markdown
parts, err := b.convertHTMLtoMarkdown(event.TextMessage.Message) parts, err := b.convertHTMLtoMarkdown(event.TextMessage.Message)
if err != nil { if err != nil {
@ -36,7 +42,14 @@ func (b *Bmumble) handleTextMessage(event *gumble.TextMessageEvent) {
if part.Image == nil { if part.Image == nil {
rmsg.Text = part.Text rmsg.Text = part.Text
} else { } else {
fname := b.Account + "_" + strconv.FormatInt(now.UnixNano(), 10) + "_" + strconv.Itoa(i) + part.FileExtension 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{}) rmsg.Extra = make(map[string][]interface{})
if err = helper.HandleDownloadSize(b.Log, &rmsg, fname, int64(len(part.Image)), b.General); err != nil { 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") b.Log.WithError(err).Warn("not including image in message")
@ -56,7 +69,6 @@ func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) {
} }
// No need to talk or listen // No need to talk or listen
event.Client.Self.SetSelfDeafened(true) event.Client.Self.SetSelfDeafened(true)
event.Client.Self.SetSelfMuted(true)
// if the Channel variable is set, this is a reconnect -> rejoin channel // if the Channel variable is set, this is a reconnect -> rejoin channel
if b.Channel != nil { if b.Channel != nil {
if err := b.doJoin(event.Client, *b.Channel); err != nil { if err := b.doJoin(event.Client, *b.Channel); err != nil {
@ -72,19 +84,75 @@ func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) {
} }
} }
func (b *Bmumble) handleUserChange(event *gumble.UserChangeEvent) { func (b *Bmumble) handleJoinLeave(event *gumble.UserChangeEvent) {
// Only care about changes to self // Ignore events happening before setup is done
if event.User != event.Client.Self { if b.Channel == nil {
return return
} }
// Someone attempted to move the user out of the configured channel; attempt to join back if b.GetBool("nosendjoinpart") {
if b.Channel != nil { 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 { if err := b.doJoin(event.Client, *b.Channel); err != nil {
b.Log.Error(err) 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) { func (b *Bmumble) handleDisconnect(event *gumble.DisconnectEvent) {
b.connected <- *event b.connected <- *event
} }

View File

@ -8,6 +8,7 @@ import (
"io/ioutil" "io/ioutil"
"net" "net"
"strconv" "strconv"
"strings"
"time" "time"
"layeh.com/gumble/gumble" "layeh.com/gumble/gumble"
@ -92,7 +93,7 @@ func (b *Bmumble) JoinChannel(channel config.ChannelInfo) error {
func (b *Bmumble) Send(msg config.Message) (string, error) { func (b *Bmumble) Send(msg config.Message) (string, error) {
// Only process text messages // Only process text messages
b.Log.Debugf("=> Received local message %#v", msg) b.Log.Debugf("=> Received local message %#v", msg)
if msg.Event != "" && msg.Event != config.EventUserAction { if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave {
return "", nil return "", nil
} }
@ -184,6 +185,7 @@ func (b *Bmumble) doConnect() error {
gumbleConfig.Password = password gumbleConfig.Password = password
} }
registerNullCodecAsOpus()
client, err := gumble.DialWithDialer(new(net.Dialer), b.GetString("Server"), gumbleConfig, &b.tlsConfig) client, err := gumble.DialWithDialer(new(net.Dialer), b.GetString("Server"), gumbleConfig, &b.tlsConfig)
if err != nil { if err != nil {
return err return err
@ -248,12 +250,19 @@ func (b *Bmumble) processMessage(msg *config.Message) {
// If there is a maximum message length, split and truncate the lines // If there is a maximum message length, split and truncate the lines
var msgLines []string var msgLines []string
if maxLength := b.serverConfig.MaximumMessageLength; maxLength != nil { if maxLength := b.serverConfig.MaximumMessageLength; maxLength != nil {
msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username)) 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 { } else {
msgLines = helper.GetSubLines(msg.Text, 0) msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
} }
// Send the individual lindes // Send the individual lines
for i := range msgLines { 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) b.client.Self.Channel.Send(msg.Username+msgLines[i], false)
} }
} }

View File

@ -74,44 +74,33 @@ func (b *Btalk) JoinChannel(channel config.ChannelInfo) error {
} }
b.rooms = append(b.rooms, newRoom) b.rooms = append(b.rooms, newRoom)
// Config
guestSuffix := " (Guest)"
if b.IsKeySet("GuestSuffix") {
guestSuffix = b.GetString("GuestSuffix")
}
go func() { go func() {
for msg := range c { for msg := range c {
msg := msg msg := msg
// ignore messages that are one of the following
// * not a message from a user if msg.Error != nil {
// * from ourselves b.Log.Errorf("Fatal message poll error: %s\n", msg.Error)
if msg.MessageType != ocs.MessageComment || msg.ActorID == b.user.User {
continue return
}
remoteMessage := config.Message{
Text: formatRichObjectString(msg.Message, msg.MessageParameters),
Channel: newRoom.room.Token,
Username: DisplayName(msg, 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 // Ignore messages that are from the bot user
err = b.handleFiles(&remoteMessage, &msg) if msg.ActorID == b.user.User || msg.ActorType == "bridged" {
if err != nil { continue
b.Log.Errorf("Error handling file: %#v", msg) }
// 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 continue
} }
b.Log.Debugf("<= Message is %#v", remoteMessage)
b.Remote <- remoteMessage
} }
}() }()
return nil return nil
@ -124,16 +113,40 @@ func (b *Btalk) Send(msg config.Message) (string, error) {
return "", nil return "", nil
} }
// Talk currently only supports sending normal messages // Standard Message Send
if msg.Event != "" { if msg.Event == "" {
return "", nil // 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
} }
sentMessage, err := r.room.SendMessage(msg.Username + msg.Text)
if err != nil { // Message Deletion
b.Log.Errorf("Could not send message to room %v from %v: %v", msg.Channel, msg.Username, err) if msg.Event == config.EventMsgDelete {
return "", nil 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
} }
return strconv.Itoa(sentMessage.ID), nil
// Message is not a type that is currently supported
return "", nil
} }
func (b *Btalk) getRoom(token string) *Broom { func (b *Btalk) getRoom(token string) *Broom {
@ -145,6 +158,17 @@ func (b *Btalk) getRoom(token string) *Broom {
return nil 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 { func (b *Btalk) handleFiles(mmsg *config.Message, message *ocs.TalkRoomMessageData) error {
for _, parameter := range message.MessageParameters { for _, parameter := range message.MessageParameters {
if parameter.Type == ocs.ROSTypeFile { if parameter.Type == ocs.ROSTypeFile {
@ -170,6 +194,74 @@ func (b *Btalk) handleFiles(mmsg *config.Message, message *ocs.TalkRoomMessageDa
return nil 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 // Spec: https://github.com/nextcloud/server/issues/1706#issue-182308785
func formatRichObjectString(message string, parameters map[string]ocs.RichObjectString) string { func formatRichObjectString(message string, parameters map[string]ocs.RichObjectString) string {
for id, parameter := range parameters { for id, parameter := range parameters {
@ -190,7 +282,7 @@ func formatRichObjectString(message string, parameters map[string]ocs.RichObject
return message return message
} }
func DisplayName(msg ocs.TalkRoomMessageData, suffix string) string { func DisplayName(msg *ocs.TalkRoomMessageData, suffix string) string {
if msg.ActorType == ocs.ActorGuest { if msg.ActorType == ocs.ActorGuest {
if msg.ActorDisplayName == "" { if msg.ActorDisplayName == "" {
return "Guest" return "Guest"

View File

@ -1,7 +1,10 @@
package brocketchat package brocketchat
import ( import (
"fmt"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/matterbridge/Rocket.Chat.Go.SDK/models" "github.com/matterbridge/Rocket.Chat.Go.SDK/models"
) )
@ -58,6 +61,7 @@ func (b *Brocketchat) handleStatusEvent(ev models.Message, rmsg *config.Message)
func (b *Brocketchat) handleRocketClient(messages chan *config.Message) { func (b *Brocketchat) handleRocketClient(messages chan *config.Message) {
for message := range b.messageChan { for message := range b.messageChan {
message := message
// skip messages with same ID, apparently messages get duplicated for an unknown reason // skip messages with same ID, apparently messages get duplicated for an unknown reason
if _, ok := b.cache.Get(message.ID); ok { if _, ok := b.cache.Get(message.ID); ok {
continue continue
@ -76,8 +80,11 @@ func (b *Brocketchat) handleRocketClient(messages chan *config.Message) {
Account: b.Account, Account: b.Account,
UserID: message.User.ID, UserID: message.User.ID,
ID: message.ID, ID: message.ID,
Extra: make(map[string][]interface{}),
} }
b.handleAttachments(&message, rmsg)
// handleStatusEvent returns false if the message should be dropped // 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 // in that case it is probably some modification to the channel we do not want to relay
if b.handleStatusEvent(m, rmsg) { if b.handleStatusEvent(m, rmsg) {
@ -86,6 +93,38 @@ func (b *Brocketchat) handleRocketClient(messages chan *config.Message) {
} }
} }
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 { func (b *Brocketchat) handleUploadFile(msg *config.Message) error {
for _, f := range msg.Extra["file"] { for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo) fi := f.(config.FileInfo)

View File

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

View File

@ -27,7 +27,8 @@ func (b *Bslack) handleSlack() {
b.Log.Debug("Start listening for Slack messages") b.Log.Debug("Start listening for Slack messages")
for message := range messages { for message := range messages {
// don't do any action on deleted/typing 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) b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
// cleanup the message // cleanup the message
message.Text = b.replaceMention(message.Text) message.Text = b.replaceMention(message.Text)
@ -76,6 +77,13 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) {
continue continue
} }
messages <- rmsg messages <- rmsg
case *slack.FileDeletedEvent:
rmsg, err := b.handleFileDeletedEvent(ev)
if err != nil {
b.Log.Printf("%#v", err)
continue
}
messages <- rmsg
case *slack.OutgoingErrorEvent: case *slack.OutgoingErrorEvent:
b.Log.Debugf("%#v", ev.Error()) b.Log.Debugf("%#v", ev.Error())
case *slack.ChannelJoinedEvent: case *slack.ChannelJoinedEvent:
@ -95,6 +103,8 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) {
b.users.populateUser(ev.User) b.users.populateUser(ev.User)
case *slack.HelloEvent, *slack.LatencyReport, *slack.ConnectingEvent: case *slack.HelloEvent, *slack.LatencyReport, *slack.ConnectingEvent:
continue continue
case *slack.UserChangeEvent:
b.users.invalidateUser(ev.User.ID)
default: default:
b.Log.Debugf("Unhandled incoming event: %T", ev) b.Log.Debugf("Unhandled incoming event: %T", ev)
} }
@ -220,6 +230,26 @@ func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, er
return rmsg, nil 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 { func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) bool {
switch ev.SubType { switch ev.SubType {
case sChannelJoined, sMemberJoined: case sChannelJoined, sMemberJoined:
@ -252,6 +282,13 @@ func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message)
return false 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) { func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message) {
// File comments are set by the system (because there is no username given). // File comments are set by the system (because there is no username given).
if ev.SubType == sFileComment { if ev.SubType == sFileComment {
@ -260,12 +297,15 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message)
// See if we have some text in the attachments. // See if we have some text in the attachments.
if rmsg.Text == "" { if rmsg.Text == "" {
for _, attach := range ev.Attachments { for i, attach := range ev.Attachments {
if attach.Text != "" { if attach.Text != "" {
if attach.Title != "" { if attach.Title != "" {
rmsg.Text = attach.Title + "\n" rmsg.Text = getMessageTitle(&ev.Attachments[i])
} }
rmsg.Text += attach.Text rmsg.Text += attach.Text
if attach.Footer != "" {
rmsg.Text += "\n\n" + attach.Footer
}
} else { } else {
rmsg.Text = attach.Fallback rmsg.Text = attach.Fallback
} }
@ -279,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. // If we have files attached, download them (in memory) and put a pointer to it in msg.Extra.
for i := range ev.Files { 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 { if err := b.handleDownloadFile(rmsg, &ev.Files[i], false); err != nil {
b.Log.Errorf("Could not download incoming file: %#v", err) b.Log.Errorf("Could not download incoming file: %#v", err)
} }
@ -328,7 +370,7 @@ func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File, retr
// that the comment is not duplicated. // that the comment is not duplicated.
comment := rmsg.Text comment := rmsg.Text
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 return nil
} }

View File

@ -87,6 +87,9 @@ func (b *Bslack) populateMessageWithUserInfo(ev *slack.MessageEvent, rmsg *confi
if user.Profile.DisplayName != "" { if user.Profile.DisplayName != "" {
rmsg.Username = user.Profile.DisplayName rmsg.Username = user.Profile.DisplayName
} }
if b.GetBool("UseFullName") && user.Profile.RealName != "" {
rmsg.Username = user.Profile.RealName
}
return nil return nil
} }
@ -98,7 +101,9 @@ func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config
var err error var err error
var bot *slack.Bot var bot *slack.Bot
for { for {
bot, err = b.rtm.GetBotInfo(ev.BotID) bot, err = b.rtm.GetBotInfo(slack.GetBotInfoParameters{
Bot: ev.BotID,
})
if err == nil { if err == nil {
break break
} }
@ -124,7 +129,7 @@ var (
mentionRE = regexp.MustCompile(`<@([a-zA-Z0-9]+)>`) mentionRE = regexp.MustCompile(`<@([a-zA-Z0-9]+)>`)
channelRE = regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`) channelRE = regexp.MustCompile(`<#[a-zA-Z0-9]+\|(.+?)>`)
variableRE = regexp.MustCompile(`<!((?:subteam\^)?[a-zA-Z0-9]+)(?:\|@?(.+?))?>`) variableRE = regexp.MustCompile(`<!((?:subteam\^)?[a-zA-Z0-9]+)(?:\|@?(.+?))?>`)
urlRE = regexp.MustCompile(`<(.*?)(\|.*?)?>`) urlRE = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
codeFenceRE = regexp.MustCompile(`(?m)^` + "```" + `\w+$`) codeFenceRE = regexp.MustCompile(`(?m)^` + "```" + `\w+$`)
topicOrPurposeRE = regexp.MustCompile(`(?s)(@.+) (cleared|set)(?: the)? channel (topic|purpose)(?:: (.*))?`) 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 // @see https://api.slack.com/docs/message-formatting#linking_to_urls
func (b *Bslack) replaceURL(text string) string { func (b *Bslack) replaceURL(text string) string {
for _, r := range urlRE.FindAllStringSubmatch(text, -1) { return urlRE.ReplaceAllString(text, "[${2}](${1})")
if len(strings.TrimSpace(r[2])) == 1 { // A display text separator was found, but the text was blank
text = strings.Replace(text, r[0], "", 1)
} else {
text = strings.Replace(text, r[0], r[1], 1)
}
}
return text
} }
func (b *Bslack) replaceb0rkedMarkDown(text string) string { func (b *Bslack) replaceb0rkedMarkDown(text string) string {

View File

@ -36,24 +36,25 @@ type Bslack struct {
} }
const ( const (
sHello = "hello" sHello = "hello"
sChannelJoin = "channel_join" sChannelJoin = "channel_join"
sChannelLeave = "channel_leave" sChannelLeave = "channel_leave"
sChannelJoined = "channel_joined" sChannelJoined = "channel_joined"
sMemberJoined = "member_joined_channel" sMemberJoined = "member_joined_channel"
sMessageChanged = "message_changed" sMessageChanged = "message_changed"
sMessageDeleted = "message_deleted" sMessageDeleted = "message_deleted"
sSlackAttachment = "slack_attachment" sSlackAttachment = "slack_attachment"
sPinnedItem = "pinned_item" sPinnedItem = "pinned_item"
sUnpinnedItem = "unpinned_item" sUnpinnedItem = "unpinned_item"
sChannelTopic = "channel_topic" sChannelTopic = "channel_topic"
sChannelPurpose = "channel_purpose" sChannelPurpose = "channel_purpose"
sFileComment = "file_comment" sFileComment = "file_comment"
sMeMessage = "me_message" sMeMessage = "me_message"
sUserTyping = "user_typing" sUserTyping = "user_typing"
sLatencyReport = "latency_report" sLatencyReport = "latency_report"
sSystemUser = "system" sSystemUser = "system"
sSlackBotUser = "slackbot" sSlackBotUser = "slackbot"
cfileDownloadChannel = "file_download_channel"
tokenConfig = "Token" tokenConfig = "Token"
incomingWebhookConfig = "WebhookBindAddress" incomingWebhookConfig = "WebhookBindAddress"
@ -156,7 +157,7 @@ func (b *Bslack) JoinChannel(channel config.ChannelInfo) error {
// try to join a channel when in legacy // try to join a channel when in legacy
if b.legacy { if b.legacy {
_, err := b.sc.JoinChannel(channel.Name) _, _, _, err := b.sc.JoinConversation(channel.Name)
if err != nil { if err != nil {
switch err.Error() { switch err.Error() {
case "name_taken", "restricted_action": case "name_taken", "restricted_action":
@ -195,7 +196,7 @@ func (b *Bslack) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg) b.Log.Debugf("=> Receiving %#v", msg)
} }
msg.Text = helper.ClipMessage(msg.Text, messageLength) msg.Text = helper.ClipMessage(msg.Text, messageLength, b.GetString("MessageClipped"))
msg.Text = b.replaceCodeFence(msg.Text) msg.Text = b.replaceCodeFence(msg.Text)
// Make a action /me of the message // Make a action /me of the message
@ -299,7 +300,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
} }
// Handle prefix hint for unthreaded messages. // Handle prefix hint for unthreaded messages.
if msg.ParentID == "msg-parent-not-found" { if msg.ParentNotFound() {
msg.ParentID = "" msg.ParentID = ""
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text) msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
} }
@ -320,7 +321,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
} }
// Upload a file if it exists. // Upload a file if it exists.
if msg.Extra != nil { if len(msg.Extra) > 0 {
extraMsgs := helper.HandleExtra(&msg, b.General) extraMsgs := helper.HandleExtra(&msg, b.General)
for i := range extraMsgs { for i := range extraMsgs {
rmsg := &extraMsgs[i] rmsg := &extraMsgs[i]
@ -331,7 +332,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
} }
} }
// Upload files if necessary (from Slack, Telegram or Mattermost). // Upload files if necessary (from Slack, Telegram or Mattermost).
b.uploadFile(&msg, channelInfo.ID) return b.uploadFile(&msg, channelInfo.ID)
} }
// Post message. // Post message.
@ -442,7 +443,8 @@ func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (s
} }
// uploadFile handles native upload of files // 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"] { for _, f := range msg.Extra["file"] {
fi, ok := f.(config.FileInfo) fi, ok := f.(config.FileInfo)
if !ok { if !ok {
@ -459,7 +461,7 @@ func (b *Bslack) uploadFile(msg *config.Message, channelID string) {
b.cache.Add("filename"+fi.Name, ts) b.cache.Add("filename"+fi.Name, ts)
initialComment := fmt.Sprintf("File from %s", msg.Username) initialComment := fmt.Sprintf("File from %s", msg.Username)
if fi.Comment != "" { 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{ res, err := b.sc.UploadFile(slack.FileUploadParameters{
Reader: bytes.NewReader(*fi.Data), Reader: bytes.NewReader(*fi.Data),
@ -470,13 +472,22 @@ func (b *Bslack) uploadFile(msg *config.Message, channelID string) {
}) })
if err != nil { if err != nil {
b.Log.Errorf("uploadfile %#v", err) b.Log.Errorf("uploadfile %#v", err)
return return "", err
} }
if res.ID != "" { if res.ID != "" {
b.Log.Debugf("Adding file ID %s to cache with timestamp %s", res.ID, ts.String()) b.Log.Debugf("Adding file ID %s to cache with timestamp %s", res.ID, ts.String())
b.cache.Add("file"+res.ID, ts) 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 { func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption {

View File

@ -113,6 +113,12 @@ func (b *users) populateUser(userID string) {
b.users[userID] = user 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) { func (b *users) populateUsers(wait bool) {
b.refreshMutex.Lock() b.refreshMutex.Lock()
if !wait && (time.Now().Before(b.earliestRefresh) || b.refreshInProgress) { 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 // We only retrieve public and private channels, not IMs
// and MPIMs as those do not have a channel name. // and MPIMs as those do not have a channel name.
queryParams := &slack.GetConversationsParameters{ queryParams := &slack.GetConversationsParameters{
ExcludeArchived: "true", ExcludeArchived: true,
Types: []string{"public_channel,private_channel"}, Types: []string{"public_channel,private_channel"},
Limit: 1000,
} }
for { for {
channels, nextCursor, err := b.sc.GetConversations(queryParams) channels, nextCursor, err := b.sc.GetConversations(queryParams)

View File

@ -1,22 +1,41 @@
package btelegram package btelegram
import ( import (
"fmt"
"html" "html"
"regexp" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"unicode/utf16" "unicode/utf16"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "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 { func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message {
// handle channels // handle channels
if posted != nil { if posted != nil {
message = posted if posted.Text == "/chatId" {
rmsg.Text = message.Text 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 // edited channel message
@ -43,6 +62,11 @@ func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Mess
return return
} }
if message.ForwardFromChat != nil && message.ForwardFrom == nil {
rmsg.Text = "Forwarded from " + message.ForwardFromChat.Title + ": " + rmsg.Text
return
}
if message.ForwardFrom == nil { if message.ForwardFrom == nil {
rmsg.Text = "Forwarded from " + unknownUser + ": " + rmsg.Text rmsg.Text = "Forwarded from " + unknownUser + ": " + rmsg.Text
return return
@ -52,6 +76,9 @@ func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Mess
if b.GetBool("UseFirstName") { if b.GetBool("UseFirstName") {
usernameForward = message.ForwardFrom.FirstName usernameForward = message.ForwardFrom.FirstName
} }
if b.GetBool("UseFullName") {
usernameForward = message.ForwardFrom.FirstName + " " + message.ForwardFrom.LastName
}
if usernameForward == "" { if usernameForward == "" {
usernameForward = message.ForwardFrom.UserName usernameForward = message.ForwardFrom.UserName
@ -69,12 +96,16 @@ func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Mess
// handleQuoting handles quoting of previous messages // handleQuoting handles quoting of previous messages
func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Message) { 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 := "" usernameReply := ""
if message.ReplyToMessage.From != nil { if message.ReplyToMessage.From != nil {
if b.GetBool("UseFirstName") { if b.GetBool("UseFirstName") {
usernameReply = message.ReplyToMessage.From.FirstName usernameReply = message.ReplyToMessage.From.FirstName
} }
if b.GetBool("UseFullName") {
usernameReply = message.ReplyToMessage.From.FirstName + " " + message.ReplyToMessage.From.LastName
}
if usernameReply == "" { if usernameReply == "" {
usernameReply = message.ReplyToMessage.From.UserName usernameReply = message.ReplyToMessage.From.UserName
if usernameReply == "" { if usernameReply == "" {
@ -86,7 +117,11 @@ func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Messag
usernameReply = unknownUser usernameReply = unknownUser
} }
if !b.GetBool("QuoteDisable") { 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)
} }
} }
} }
@ -94,10 +129,15 @@ func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Messag
// handleUsername handles the correct setting of the username // handleUsername handles the correct setting of the username
func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) { func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) {
if message.From != nil { if message.From != nil {
rmsg.UserID = strconv.Itoa(message.From.ID) rmsg.UserID = strconv.FormatInt(message.From.ID, 10)
if b.GetBool("UseFirstName") { if b.GetBool("UseFirstName") {
rmsg.Username = message.From.FirstName 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 == "" { if rmsg.Username == "" {
rmsg.Username = message.From.UserName rmsg.Username = message.From.UserName
if rmsg.Username == "" { if rmsg.Username == "" {
@ -110,6 +150,35 @@ func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Messa
} }
} }
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 we really didn't find a username, set it to unknown
if rmsg.Username == "" { if rmsg.Username == "" {
rmsg.Username = unknownUser rmsg.Username = unknownUser
@ -122,10 +191,16 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
if update.Message == nil && update.ChannelPost == nil && if update.Message == nil && update.ChannelPost == nil &&
update.EditedMessage == nil && update.EditedChannelPost == 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 continue
} }
if b.GetInt("debuglevel") == 1 {
spew.Dump(update.Message)
}
b.handleGroupUpdate(update)
var message *tgbotapi.Message var message *tgbotapi.Message
rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})} rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})}
@ -144,6 +219,19 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
// set the ID's from the channel or group message // set the ID's from the channel or group message
rmsg.ID = strconv.Itoa(message.MessageID) rmsg.ID = strconv.Itoa(message.MessageID)
rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10) 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 // handle username
b.handleUsername(&rmsg, message) b.handleUsername(&rmsg, message)
@ -160,14 +248,12 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
// quote the previous message // quote the previous message
b.handleQuoting(&rmsg, message) b.handleQuoting(&rmsg, message)
// handle entities (adding URLs)
b.handleEntities(&rmsg, message)
if rmsg.Text != "" || len(rmsg.Extra) > 0 { 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 // channels don't have (always?) user information. see #410
if message.From != nil { 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) b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
@ -177,61 +263,99 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
} }
} }
// handleDownloadAvatar downloads the avatar of userid from channel func (b *Btelegram) handleGroupUpdate(update tgbotapi.Update) {
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful. if msg := update.Message; msg != nil {
// logs an error message if it fails switch {
func (b *Btelegram) handleDownloadAvatar(userid int, channel string) { case msg.NewChatMembers != nil:
rmsg := config.Message{Username: "system", b.handleUserJoin(update)
Text: "avatar", case msg.LeftChatMember != nil:
Channel: channel, b.handleUserLeave(update)
Account: b.Account,
UserID: strconv.Itoa(userid),
Event: config.EventAvatarDownload,
Extra: make(map[string][]interface{})}
if _, ok := b.avatarMap[strconv.Itoa(userid)]; !ok {
photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1})
if err != nil {
b.Log.Errorf("Userprofile download failed for %#v %s", userid, err)
}
if len(photos.Photos) > 0 {
photo := photos.Photos[0][0]
url := b.getFileDirectURL(photo.FileID)
name := strconv.Itoa(userid) + ".png"
b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)
err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General)
if err != nil {
b.Log.Error(err)
return
}
data, err := helper.DownloadFile(url)
if err != nil {
b.Log.Errorf("download %s failed %#v", url, err)
return
}
helper.HandleDownloadData(b.Log, &rmsg, name, rmsg.Text, "", data, b.General)
b.Remote <- rmsg
} }
} }
} }
func (b *Btelegram) handleUserJoin(update tgbotapi.Update) {
msg := update.Message
for _, user := range msg.NewChatMembers {
rmsg := config.Message{
UserID: strconv.FormatInt(user.ID, 10),
Username: user.FirstName, // for some reason all the other name felids are empty on this event (at least for me)
Channel: strconv.FormatInt(msg.Chat.ID, 10),
Account: b.Account,
Protocol: b.Protocol,
Event: config.EventJoinLeave,
Text: "joined chat",
}
b.Remote <- rmsg
}
}
func (b *Btelegram) handleUserLeave(update tgbotapi.Update) {
msg := update.Message
user := msg.LeftChatMember
rmsg := config.Message{
UserID: strconv.FormatInt(user.ID, 10),
Username: user.FirstName, // for some reason all the other name felids are empty on this event (at least for me)
Channel: strconv.FormatInt(msg.Chat.ID, 10),
Account: b.Account,
Protocol: b.Protocol,
Event: config.EventJoinLeave,
Text: "left chat",
}
b.Remote <- rmsg
}
// handleDownloadAvatar downloads the avatar of userid from channel
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
// logs an error message if it fails
func (b *Btelegram) handleDownloadAvatar(userid int64, channel string) {
rmsg := config.Message{
Username: "system",
Text: "avatar",
Channel: channel,
Account: b.Account,
UserID: strconv.FormatInt(userid, 10),
Event: config.EventAvatarDownload,
Extra: make(map[string][]interface{}),
}
if _, ok := b.avatarMap[strconv.FormatInt(userid, 10)]; ok {
return
}
photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1})
if err != nil {
b.Log.Errorf("Userprofile download failed for %#v %s", userid, err)
}
if len(photos.Photos) > 0 {
photo := photos.Photos[0][0]
url := b.getFileDirectURL(photo.FileID)
name := strconv.FormatInt(userid, 10) + ".png"
b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)
err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General)
if err != nil {
b.Log.Error(err)
return
}
data, err := helper.DownloadFile(url)
if err != nil {
b.Log.Errorf("download %s failed %#v", url, err)
return
}
helper.HandleDownloadData(b.Log, &rmsg, name, rmsg.Text, "", data, b.General)
b.Remote <- rmsg
}
}
func (b *Btelegram) maybeConvertTgs(name *string, data *[]byte) { func (b *Btelegram) maybeConvertTgs(name *string, data *[]byte) {
var format string format := b.GetString("MediaConvertTgs")
switch b.GetString("MediaConvertTgs") { if helper.SupportsFormat(format) {
case FormatWebp: b.Log.Debugf("Format supported by %s, converting %v", helper.LottieBackend(), name)
b.Log.Debugf("Tgs to WebP conversion enabled, converting %v", name) } else {
format = FormatWebp
case FormatPng:
// The WebP to PNG converter can't handle animated webp files yet,
// and I'm not going to write a path for x/image/webp.
// The error message would be:
// conversion failed: webp: non-Alpha VP8X is not implemented
// So instead, we tell lottie to directly go to PNG.
b.Log.Debugf("Tgs to PNG conversion enabled, converting %v", name)
format = FormatPng
default:
// Otherwise, no conversion was requested. Trying to run the usual webp // Otherwise, no conversion was requested. Trying to run the usual webp
// converter would fail, because '.tgs.webp' is actually a gzipped JSON // converter would fail, because '.tgs.webp' is actually a gzipped JSON
// file, and has nothing to do with WebP. // file, and has nothing to do with WebP.
@ -259,12 +383,12 @@ func (b *Btelegram) maybeConvertWebp(name *string, data *[]byte) {
// handleDownloadFile handles file download // handleDownloadFile handles file download
func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error { func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error {
size := 0 size := int64(0)
var url, name, text string var url, name, text string
switch { switch {
case message.Sticker != nil: case message.Sticker != nil:
text, name, url = b.getDownloadInfo(message.Sticker.FileID, ".webp", true) text, name, url = b.getDownloadInfo(message.Sticker.FileID, ".webp", true)
size = message.Sticker.FileSize size = int64(message.Sticker.FileSize)
case message.Voice != nil: case message.Voice != nil:
text, name, url = b.getDownloadInfo(message.Voice.FileID, ".ogg", true) text, name, url = b.getDownloadInfo(message.Voice.FileID, ".ogg", true)
size = message.Voice.FileSize size = message.Voice.FileSize
@ -280,8 +404,8 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa
name = message.Document.FileName name = message.Document.FileName
text = " " + message.Document.FileName + " : " + url text = " " + message.Document.FileName + " : " + url
case message.Photo != nil: case message.Photo != nil:
photos := *message.Photo photos := message.Photo
size = photos[len(photos)-1].FileSize size = int64(photos[len(photos)-1].FileSize)
text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true) text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true)
} }
@ -311,6 +435,11 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa
b.maybeConvertWebp(&name, data) 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) helper.HandleDownloadData(b.Log, rmsg, name, message.Caption, "", data, b.General)
return nil return nil
} }
@ -322,7 +451,7 @@ func (b *Btelegram) getDownloadInfo(id string, suffix string, urlpart bool) (str
urlPart := strings.Split(url, "/") urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1] name = urlPart[len(urlPart)-1]
} }
if suffix != "" && !strings.HasSuffix(name, suffix) { if suffix != "" && !strings.HasSuffix(name, suffix) && !strings.HasSuffix(name, ".webm") {
name += suffix name += suffix
} }
text := " " + url text := " " + url
@ -334,11 +463,15 @@ func (b *Btelegram) handleDelete(msg *config.Message, chatid int64) (string, err
if msg.ID == "" { if msg.ID == "" {
return "", nil return "", nil
} }
msgid, err := strconv.Atoi(msg.ID) msgid, err := strconv.Atoi(msg.ID)
if err != nil { if err != nil {
return "", err return "", err
} }
_, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid})
cfg := tgbotapi.NewDeleteMessage(chatid, msgid)
_, err = b.c.Request(cfg)
return "", err return "", err
} }
@ -376,31 +509,57 @@ func (b *Btelegram) handleEdit(msg *config.Message, chatid int64) (string, error
} }
// handleUploadFile handles native upload of files // handleUploadFile handles native upload of files
func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64) string { func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64, threadid int, parentID int) (string, error) {
var c tgbotapi.Chattable var media []interface{}
for _, f := range msg.Extra["file"] { for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo) fi := f.(config.FileInfo)
file := tgbotapi.FileBytes{ file := tgbotapi.FileBytes{
Name: fi.Name, Name: fi.Name,
Bytes: *fi.Data, Bytes: *fi.Data,
} }
re := regexp.MustCompile(".(jpg|png)$")
if re.MatchString(fi.Name) { if b.GetString("MessageFormat") == HTMLFormat {
c = tgbotapi.NewPhotoUpload(chatid, file) fi.Comment = makeHTML(html.EscapeString(fi.Comment))
} else {
c = tgbotapi.NewDocumentUpload(chatid, file)
} }
_, err := b.c.Send(c)
if err != nil { switch filepath.Ext(fi.Name) {
b.Log.Errorf("file upload failed: %#v", err) case ".jpg", ".jpe", ".png":
} pc := tgbotapi.NewInputMediaPhoto(file)
if fi.Comment != "" { if fi.Comment != "" {
if _, err := b.sendMessage(chatid, msg.Username, fi.Comment); err != nil { pc.Caption, pc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
b.Log.Errorf("posting file comment %s failed: %s", fi.Comment, err)
} }
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 { func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string {
@ -408,7 +567,7 @@ func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string
if format == "" { if format == "" {
format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})" format = "{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
} }
quoteMessagelength := len(quoteMessage) quoteMessagelength := len([]rune(quoteMessage))
if b.GetInt("QuoteLengthLimit") != 0 && quoteMessagelength >= b.GetInt("QuoteLengthLimit") { if b.GetInt("QuoteLengthLimit") != 0 && quoteMessagelength >= b.GetInt("QuoteLengthLimit") {
runes := []rune(quoteMessage) runes := []rune(quoteMessage)
quoteMessage = string(runes[0:b.GetInt("QuoteLengthLimit")]) quoteMessage = string(runes[0:b.GetInt("QuoteLengthLimit")])
@ -427,21 +586,61 @@ func (b *Btelegram) handleEntities(rmsg *config.Message, message *tgbotapi.Messa
if message.Entities == nil { if message.Entities == nil {
return 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" { if e.Type == "text_link" {
offset := e.Offset + indexMovedBy
url, err := e.ParseURL() url, err := e.ParseURL()
if err != nil { if err != nil {
b.Log.Errorf("entity text_link url parse failed: %s", err) b.Log.Errorf("entity text_link url parse failed: %s", err)
continue continue
} }
utfEncodedString := utf16.Encode([]rune(rmsg.Text)) utfEncodedString := utf16.Encode([]rune(rmsg.Text))
if e.Offset+e.Length > len(utfEncodedString) { if offset+e.Length > len(utfEncodedString) {
b.Log.Errorf("entity length is too long %d > %d", e.Offset+e.Length, len(utfEncodedString)) b.Log.Errorf("entity length is too long %d > %d", offset+e.Length, len(utfEncodedString))
continue continue
} }
link := utf16.Decode(utfEncodedString[e.Offset : e.Offset+e.Length]) rmsg.Text = string(utf16.Decode(asRunes[:offset+e.Length])) + " (" + url.String() + ")" + string(utf16.Decode(asRunes[offset+e.Length:]))
rmsg.Text = strings.Replace(rmsg.Text, string(link), url.String(), 1) 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 ( import (
"bytes" "bytes"
"html"
"github.com/russross/blackfriday" "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) { func (options *customHTML) BlockCode(out *bytes.Buffer, text []byte, lang string) {
out.WriteString("<pre>") out.WriteString("<pre>")
out.WriteString(html.EscapeString(string(text))) out.WriteString(string(text))
out.WriteString("</pre>\n") 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) { func (options *customHTML) Header(out *bytes.Buffer, text func() bool, level int, id string) {
options.Paragraph(out, text) options.Paragraph(out, text)
} }
@ -42,6 +47,10 @@ func (options *customHTML) BlockQuote(out *bytes.Buffer, text []byte) {
out.WriteByte('\n') 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) { func (options *customHTML) List(out *bytes.Buffer, text func() bool, flags int) {
options.Paragraph(out, text) options.Paragraph(out, text)
} }

View File

@ -1,6 +1,7 @@
package btelegram package btelegram
import ( import (
"fmt"
"html" "html"
"log" "log"
"strconv" "strconv"
@ -9,7 +10,7 @@ import (
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "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 ( const (
@ -17,8 +18,6 @@ const (
HTMLFormat = "HTML" HTMLFormat = "HTML"
HTMLNick = "htmlnick" HTMLNick = "htmlnick"
MarkdownV2 = "MarkdownV2" MarkdownV2 = "MarkdownV2"
FormatPng = "png"
FormatWebp = "webp"
) )
type Btelegram struct { type Btelegram struct {
@ -32,10 +31,10 @@ func New(cfg *bridge.Config) bridge.Bridger {
if tgsConvertFormat != "" { if tgsConvertFormat != "" {
err := helper.CanConvertTgsToX() err := helper.CanConvertTgsToX()
if err != nil { if err != nil {
log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but lottie does not appear to work:\n%#v", tgsConvertFormat, err) 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 tgsConvertFormat != FormatPng && tgsConvertFormat != FormatWebp { if !helper.SupportsFormat(tgsConvertFormat) {
log.Fatalf("Telegram bridge configured to convert .tgs files to '%s', but only '%s' and '%s' are supported.", FormatPng, FormatWebp, 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)} return &Btelegram{Config: cfg, avatarMap: make(map[string]string)}
@ -51,11 +50,7 @@ func (b *Btelegram) Connect() error {
} }
u := tgbotapi.NewUpdate(0) u := tgbotapi.NewUpdate(0)
u.Timeout = 60 u.Timeout = 60
updates, err := b.c.GetUpdatesChan(u) updates := b.c.GetUpdatesChan(u)
if err != nil {
b.Log.Debugf("%#v", err)
return err
}
b.Log.Info("Connection succeeded") b.Log.Info("Connection succeeded")
go b.handleRecv(updates) go b.handleRecv(updates)
return nil return nil
@ -69,11 +64,63 @@ func (b *Btelegram) JoinChannel(channel config.ChannelInfo) error {
return nil 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) { func (b *Btelegram) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg) b.Log.Debugf("=> Receiving %#v", msg)
// get the chatid chatid, topicid, err := b.getIds(msg.Channel)
chatid, err := strconv.ParseInt(msg.Channel, 10, 64)
if err != nil { if err != nil {
return "", err return "", err
} }
@ -84,7 +131,7 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
} }
if b.GetString("MessageFormat") == HTMLFormat { if b.GetString("MessageFormat") == HTMLFormat {
msg.Text = makeHTML(msg.Text) msg.Text = makeHTML(html.EscapeString(msg.Text))
} }
// Delete message // Delete message
@ -92,16 +139,27 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
return b.handleDelete(&msg, chatid) 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 // Upload a file if it exists
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) { 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) b.Log.Errorf("sendMessage failed: %s", msgErr)
} }
} }
// check if we have files to upload (from slack, telegram or mattermost) // check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 { if len(msg.Extra["file"]) > 0 {
b.handleUploadFile(&msg, chatid) return b.handleUploadFile(&msg, chatid, topicid, parentID)
} }
} }
@ -115,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 // Ignore empty text field needs for prevent double messages from whatsapp to telegram
// when sending media with text caption // when sending media with text caption
if msg.Text != "" { if msg.Text != "" {
return b.sendMessage(chatid, msg.Username, msg.Text) return b.sendMessage(chatid, topicid, msg.Username, msg.Text, parentID)
} }
return "", nil return "", nil
@ -129,27 +187,13 @@ func (b *Btelegram) getFileDirectURL(id string) string {
return res 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 := tgbotapi.NewMessage(chatid, "")
m.Text = username + text m.Text, m.ParseMode = TGGetParseMode(b, username, text)
if b.GetString("MessageFormat") == HTMLFormat { if topicid != 0 {
b.Log.Debug("Using mode HTML") m.BaseChat.MessageThreadID = topicid
m.ParseMode = tgbotapi.ModeHTML
} }
if b.GetString("MessageFormat") == "Markdown" { m.ReplyToMessageID = parentID
b.Log.Debug("Using mode markdown")
m.ParseMode = tgbotapi.ModeMarkdown
}
if b.GetString("MessageFormat") == MarkdownV2 {
b.Log.Debug("Using mode MarkdownV2")
m.ParseMode = MarkdownV2
}
if strings.ToLower(b.GetString("MessageFormat")) == HTMLNick {
b.Log.Debug("Using mode HTML - nick only")
m.Text = username + html.EscapeString(text)
m.ParseMode = tgbotapi.ModeHTML
}
m.DisableWebPagePreview = b.GetBool("DisableWebPagePreview") m.DisableWebPagePreview = b.GetBool("DisableWebPagePreview")
res, err := b.c.Send(m) res, err := b.c.Send(m)
@ -159,6 +203,37 @@ func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, er
return strconv.Itoa(res.MessageID), nil 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) { func (b *Btelegram) cacheAvatar(msg *config.Message) (string, error) {
fi := msg.Extra["file"][0].(config.FileInfo) fi := msg.Extra["file"][0].(config.FileInfo)
/* if we have a sha we have successfully uploaded the file to the media server, /* 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 package bwhatsapp
import ( import (
@ -24,7 +25,8 @@ Check:
func (b *Bwhatsapp) HandleError(err error) { func (b *Bwhatsapp) HandleError(err error) {
// ignore received invalid data errors. https://github.com/42wim/matterbridge/issues/843 // ignore received invalid data errors. https://github.com/42wim/matterbridge/issues/843
// ignore tag 174 errors. https://github.com/42wim/matterbridge/issues/1094 // 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") { if strings.Contains(err.Error(), "error processing data: received invalid data") ||
strings.Contains(err.Error(), "invalid string with tag 174") {
return return
} }
@ -47,16 +49,22 @@ func (b *Bwhatsapp) reconnect(err error) {
Max: 5 * time.Minute, Max: 5 * time.Minute,
Jitter: true, Jitter: true,
} }
for { for {
d := bf.Duration() d := bf.Duration()
b.Log.Errorf("Connection failed, underlying error: %v", err) b.Log.Errorf("Connection failed, underlying error: %v", err)
b.Log.Infof("Waiting %s...", d) b.Log.Infof("Waiting %s...", d)
time.Sleep(d) time.Sleep(d)
b.Log.Info("Reconnecting...") b.Log.Info("Reconnecting...")
err := b.conn.Restore() err := b.conn.Restore()
if err == nil { if err == nil {
bf.Reset() bf.Reset()
b.startedAt = uint64(time.Now().Unix()) b.startedAt = uint64(time.Now().Unix())
return return
} }
} }
@ -64,7 +72,7 @@ func (b *Bwhatsapp) reconnect(err error) {
// HandleTextMessage sent from WhatsApp, relay it to the brige // HandleTextMessage sent from WhatsApp, relay it to the brige
func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) { func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
if message.Info.FromMe { // || !strings.Contains(strings.ToLower(message.Text), "@echo") { if message.Info.FromMe {
return return
} }
// whatsapp sends last messages to show context , cut them // whatsapp sends last messages to show context , cut them
@ -72,12 +80,10 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
return return
} }
messageTime := time.Unix(int64(message.Info.Timestamp), 0) // TODO check how behaves between timezones
groupJID := message.Info.RemoteJid groupJID := message.Info.RemoteJid
senderJID := message.Info.SenderJid senderJID := message.Info.SenderJid
if len(senderJID) == 0 { if len(senderJID) == 0 {
// TODO workaround till https://github.com/Rhymen/go-whatsapp/issues/86 resolved
if message.Info.Source != nil && message.Info.Source.Participant != nil { if message.Info.Source != nil && message.Info.Source.Participant != nil {
senderJID = *message.Info.Source.Participant senderJID = *message.Info.Source.Participant
} }
@ -101,110 +107,276 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
if mention == "" { if mention == "" {
mention = "someone" mention = "someone"
} }
message.Text = strings.Replace(message.Text, "@"+numberAndSuffix[0], "@"+mention, 1) 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{ rmsg := config.Message{
UserID: senderJID, UserID: senderJID,
Username: senderName, Username: senderName,
Text: message.Text, Text: message.Text,
Timestamp: messageTime, Channel: groupJID,
Channel: groupJID, Account: b.Account,
Account: b.Account, Protocol: b.Protocol,
Protocol: b.Protocol, Extra: make(map[string][]interface{}),
Extra: make(map[string][]interface{}),
// ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string // ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string
// Event string `json:"event"` ID: message.Info.Id,
// Gateway string // will be added during message processing }
ID: message.Info.Id}
if avatarURL, exists := b.userAvatars[senderJID]; exists { if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL 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.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg b.Remote <- rmsg
} }
// HandleImageMessage sent from WhatsApp, relay it to the brige // HandleImageMessage sent from WhatsApp, relay it to the brige
// nolint:funlen
func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) { 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 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 senderJID := message.Info.SenderJid
if len(senderJID) == 0 { if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
// 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 senderName := b.getSenderName(message.Info.SenderJid)
senderName := b.getSenderName(senderJID)
if senderName == "" { if senderName == "" {
senderName = "Someone" // don't expose telephone number senderName = "Someone" // don't expose telephone number
} }
b.Log.Debugf("<= Sending message from %s on %s to gateway", senderJID, b.Account)
rmsg := config.Message{ rmsg := config.Message{
UserID: senderJID, UserID: senderJID,
Username: senderName, Username: senderName,
Timestamp: messageTime, Channel: message.Info.RemoteJid,
Channel: groupJID, Account: b.Account,
Account: b.Account, Protocol: b.Protocol,
Protocol: b.Protocol, Extra: make(map[string][]interface{}),
Extra: make(map[string][]interface{}), ID: message.Info.Id,
// 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}
if avatarURL, exists := b.userAvatars[senderJID]; exists { if avatarURL, exists := b.userAvatars[senderJID]; exists {
rmsg.Avatar = avatarURL rmsg.Avatar = avatarURL
} }
// Download and unencrypt content fileExt, err := mime.ExtensionsByType(message.Type)
data, err := message.Download()
if err != nil { if err != nil {
b.Log.Errorf("%v", err) b.Log.Errorf("Mimetype detection error: %s", err)
return return
} }
// Get file extension by mimetype // rename .jfif to .jpg https://github.com/42wim/matterbridge/issues/1292
fileExt, err := mime.ExtensionsByType(message.Type) if fileExt[0] == ".jfif" {
if err != nil { fileExt[0] = ".jpg"
b.Log.Errorf("%v", err) }
return
// 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]) 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 // Move file to bridge storage
helper.HandleDownloadData(b.Log, &rmsg, filename, message.Caption, "", &data, b.General) 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 b.Remote <- rmsg
} }
//func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) { // HandleVideoMessage downloads video messages
// fmt.Println(message) // TODO implement func (b *Bwhatsapp) HandleVideoMessage(message whatsapp.VideoMessage) {
//} if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
// return
//func (b *Bwhatsapp) HandleJsonMessage(message string) { }
// fmt.Println(message) // TODO implement
//} senderJID := message.Info.SenderJid
// TODO HandleRawMessage if len(message.Info.SenderJid) == 0 && message.Info.Source != nil && message.Info.Source.Participant != nil {
// TODO HandleAudioMessage 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" "errors"
"fmt" "fmt"
"os" "os"
"strings"
qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go" qrcodeTerminal "github.com/Baozisoftware/qrcode-terminal-go"
"github.com/Rhymen/go-whatsapp" "github.com/Rhymen/go-whatsapp"
) )
type ProfilePicInfo struct { type ProfilePicInfo struct {
URL string `json:"eurl"` URL string `json:"eurl"`
Tag string `json:"tag"` Tag string `json:"tag"`
Status int16 `json:"status"`
Status int16 `json:"status"`
} }
func qrFromTerminal(invert bool) chan string { func qrFromTerminal(invert bool) chan string {
qr := make(chan string) qr := make(chan string)
go func() { go func() {
terminal := qrcodeTerminal.New() terminal := qrcodeTerminal.New()
if invert { if invert {
terminal = qrcodeTerminal.New2(qrcodeTerminal.ConsoleColors.BrightWhite, qrcodeTerminal.ConsoleColors.BrightBlack, qrcodeTerminal.QRCodeRecoveryLevels.Medium) 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 { if err != nil {
return session, err return session, err
} }
defer file.Close() defer file.Close()
decoder := gob.NewDecoder(file) decoder := gob.NewDecoder(file)
err = decoder.Decode(&session)
if err != nil { return session, decoder.Decode(&session)
return session, err
}
return session, nil
} }
func (b *Bwhatsapp) writeSession(session whatsapp.Session) error { func (b *Bwhatsapp) writeSession(session whatsapp.Session) error {
@ -65,11 +66,31 @@ func (b *Bwhatsapp) writeSession(session whatsapp.Session) error {
if err != nil { if err != nil {
return err 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 { func (b *Bwhatsapp) getSenderName(senderJid string) string {
@ -90,8 +111,7 @@ func (b *Bwhatsapp) getSenderName(senderJid string) string {
} }
// try to reload this contact // try to reload this contact
_, err := b.conn.Contacts() if _, err := b.conn.Contacts(); err != nil {
if err != nil {
b.Log.Errorf("error on update of contacts: %v", err) b.Log.Errorf("error on update of contacts: %v", err)
} }
@ -114,6 +134,7 @@ func (b *Bwhatsapp) getSenderNotify(senderJid string) string {
if sender, exists := b.users[senderJid]; exists { if sender, exists := b.users[senderJid]; exists {
return sender.Notify return sender.Notify
} }
return "" return ""
} }
@ -122,11 +143,20 @@ func (b *Bwhatsapp) GetProfilePicThumb(jid string) (*ProfilePicInfo, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get avatar: %v", err) return nil, fmt.Errorf("failed to get avatar: %v", err)
} }
content := <-data content := <-data
info := &ProfilePicInfo{} info := &ProfilePicInfo{}
err = json.Unmarshal([]byte(content), info) err = json.Unmarshal([]byte(content), info)
if err != nil { if err != nil {
return info, fmt.Errorf("failed to unmarshal avatar info: %v", err) return info, fmt.Errorf("failed to unmarshal avatar info: %v", err)
} }
return info, nil 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 { type Bwhatsapp struct {
*bridge.Config *bridge.Config
// https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L18-L21
session *whatsapp.Session session *whatsapp.Session
conn *whatsapp.Conn conn *whatsapp.Conn
startedAt uint64 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 // 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 { func New(cfg *bridge.Config) bridge.Bridger {
number := cfg.GetString(cfgNumber) 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 == "" { if number == "" {
cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: 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), users: make(map[string]whatsapp.Contact),
userAvatars: make(map[string]string), userAvatars: make(map[string]string),
} }
return b return b
} }
// Connect to WhatsApp. Required implementation of the Bridger interface // 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 { func (b *Bwhatsapp) Connect() error {
b.RLock() // TODO do we need locking for Whatsapp?
defer b.RUnlock()
number := b.GetString(cfgNumber) number := b.GetString(cfgNumber)
if number == "" { 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..") b.Log.Debugln("Connecting to WhatsApp..")
conn, err := whatsapp.NewConn(20 * time.Second) conn, err := whatsapp.NewConn(20 * time.Second)
if err != nil { if err != nil {
@ -77,35 +78,18 @@ func (b *Bwhatsapp) Connect() error {
b.Log.Debugln("WhatsApp connection successful") b.Log.Debugln("WhatsApp connection successful")
// load existing session in order to keep it between restarts // load existing session in order to keep it between restarts
if b.session == nil { b.session, err = b.restoreSession()
var session whatsapp.Session if err != nil {
session, err = b.readSession() b.Log.Warn(err.Error())
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())
}
} }
// login to a new session // login to a new session
if b.session == nil { if b.session == nil {
err = b.Login() if err = b.Login(); err != nil {
if err != nil {
return err return err
} }
} }
b.startedAt = uint64(time.Now().Unix()) b.startedAt = uint64(time.Now().Unix())
_, err = b.conn.Contacts() _, err = b.conn.Contacts()
@ -116,6 +100,7 @@ func (b *Bwhatsapp) Connect() error {
// see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013 // see https://github.com/Rhymen/go-whatsapp/issues/137#issuecomment-480316013
for len(b.conn.Store.Contacts) == 0 { for len(b.conn.Store.Contacts) == 0 {
b.conn.Contacts() // nolint:errcheck b.conn.Contacts() // nolint:errcheck
<-time.After(1 * time.Second) <-time.After(1 * time.Second)
} }
@ -135,12 +120,13 @@ func (b *Bwhatsapp) Connect() error {
info, err := b.GetProfilePicThumb(jid) info, err := b.GetProfilePicThumb(jid)
if err != nil { if err != nil {
b.Log.Warnf("Could not get profile photo of %s: %v", jid, err) b.Log.Warnf("Could not get profile photo of %s: %v", jid, err)
} else { } else {
// TODO any race conditions here? b.Lock()
b.userAvatars[jid] = info.URL b.userAvatars[jid] = info.URL
b.Unlock()
} }
} }
b.Log.Debug("Finished getting avatars..") b.Log.Debug("Finished getting avatars..")
}() }()
@ -157,8 +143,10 @@ func (b *Bwhatsapp) Login() error {
session, err := b.conn.Login(qrChan) session, err := b.conn.Login(qrChan)
if err != nil { if err != nil {
b.Log.Warnln("Failed to log in:", err) b.Log.Warnln("Failed to log in:", err)
return err return err
} }
b.session = &session b.session = &session
b.Log.Infof("Logged into session: %#v", session) b.Log.Infof("Logged into session: %#v", session)
@ -169,29 +157,17 @@ func (b *Bwhatsapp) Login() error {
fmt.Fprintf(os.Stderr, "error saving session: %v\n", err) 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 return nil
} }
// Disconnect is called while reconnecting to the bridge // 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 // Required implementation of the Bridger interface
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
func (b *Bwhatsapp) Disconnect() error { func (b *Bwhatsapp) Disconnect() error {
// We could Logout, but that would close the session completely and would require a new QR code scan // 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 // https://github.com/Rhymen/go-whatsapp/blob/c31092027237441cffba1b9cb148eadf7c83c3d2/session.go#L377-L381
return nil return nil
} }
func isGroupJid(identifier string) bool {
return strings.HasSuffix(identifier, "@g.us") || strings.HasSuffix(identifier, "@temp") || strings.HasSuffix(identifier, "@broadcast")
}
// JoinChannel Join a WhatsApp group specified in gateway config as channel='number-id@g.us' or channel='Channel name' // 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 // Required implementation of the Bridger interface
// https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16 // https://github.com/42wim/matterbridge/blob/2cfd880cdb0df29771bf8f31df8d990ab897889d/bridge/bridge.go#L11-L16
@ -210,39 +186,33 @@ func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
if _, exists := b.conn.Store.Contacts[channel.Name]; !exists { if _, exists := b.conn.Store.Contacts[channel.Name]; !exists {
return fmt.Errorf("account doesn't belong to group with jid %s", channel.Name) 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) { return nil
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)
case 1: // channel.Name specifies group name that might change, warn about it
return fmt.Errorf("group name might change. Please configure gateway with channel=\"%v\" instead of channel=\"%v\"", jids[0], channel.Name) var jids []string
for id, contact := range b.conn.Store.Contacts {
default: if isGroupJid(id) && contact.Name == channel.Name {
return fmt.Errorf("there is more than one group with name '%s'. Please specify one of JIDs as channel name: %v", channel.Name, jids) 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 // Post a document message from the bridge to WhatsApp
@ -316,22 +286,23 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
if msg.ID == "" { if msg.ID == "" {
// No message ID in case action is executed on a message sent before the bridge was started // 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 // 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 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 // Edit message
if msg.ID != "" { if msg.ID != "" {
b.Log.Debugf("updating message with id %s", msg.ID) b.Log.Debugf("updating message with id %s", msg.ID)
msg.Text += " (edited)" if b.GetString("editsuffix") != "" {
// TODO handle edit as a message reply with updated text msg.Text += b.GetString("EditSuffix")
} else {
msg.Text += " (edited)"
}
} }
// Handle Upload a file // Handle Upload a file
@ -361,16 +332,7 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Sending %#v", msg) b.Log.Debugf("=> Sending %#v", msg)
// create message ID return b.conn.Send(message)
// 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
} }
// 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 // 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
}

View File

@ -1,8 +1,12 @@
package bxmpp package bxmpp
import ( import (
"bytes"
"crypto/tls" "crypto/tls"
"encoding/json"
"fmt" "fmt"
"net/http"
"net/url"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -10,6 +14,7 @@ import (
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
lru "github.com/hashicorp/golang-lru"
"github.com/jpillora/backoff" "github.com/jpillora/backoff"
"github.com/matterbridge/go-xmpp" "github.com/matterbridge/go-xmpp"
"github.com/rs/xid" "github.com/rs/xid"
@ -24,13 +29,20 @@ type Bxmpp struct {
connected bool connected bool
sync.RWMutex sync.RWMutex
StanzaIDs *lru.Cache
OriginIDs *lru.Cache
avatarAvailability map[string]bool avatarAvailability map[string]bool
avatarMap map[string]string avatarMap map[string]string
} }
func New(cfg *bridge.Config) bridge.Bridger { func New(cfg *bridge.Config) bridge.Bridger {
stanzaIDs, _ := lru.New(5000)
originIDs, _ := lru.New(5000)
return &Bxmpp{ return &Bxmpp{
Config: cfg, Config: cfg,
StanzaIDs: stanzaIDs,
OriginIDs: originIDs,
xmppMap: make(map[string]string), xmppMap: make(map[string]string),
avatarAvailability: make(map[string]bool), avatarAvailability: make(map[string]bool),
avatarMap: make(map[string]string), avatarMap: make(map[string]string),
@ -86,14 +98,21 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
} }
// Upload a file (in XMPP case send the upload URL because XMPP has no native upload support). // Upload a file (in XMPP case send the upload URL because XMPP has no native upload support).
var err error
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) { for _, rmsg := range helper.HandleExtra(&msg, b.General) {
b.Log.Debugf("=> Sending attachement message %#v", rmsg) b.Log.Debugf("=> Sending attachement message %#v", rmsg)
if _, err := b.xc.Send(xmpp.Chat{ if b.GetString("WebhookURL") != "" {
Type: "groupchat", err = b.postSlackCompatibleWebhook(msg)
Remote: rmsg.Channel + "@" + b.GetString("Muc"), } else {
Text: rmsg.Username + rmsg.Text, _, err = b.xc.Send(xmpp.Chat{
}); err != nil { 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.") b.Log.WithError(err).Error("Unable to send message with share URL.")
} }
} }
@ -102,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 var msgReplaceID string
msgID := xid.New().String() msgID := xid.New().String()
if msg.ID != "" { if msg.ID != "" {
msgID = msg.ID
msgReplaceID = 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) b.Log.Debugf("=> Sending message %#v", msg)
if _, err := b.xc.Send(xmpp.Chat{ if _, err := b.xc.Send(xmpp.Chat{
Type: "groupchat", Type: "groupchat",
@ -116,18 +153,53 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
Text: msg.Username + msg.Text, Text: msg.Username + msg.Text,
ID: msgID, ID: msgID,
ReplaceID: msgReplaceID, ReplaceID: msgReplaceID,
ReplyID: replyID,
}); err != nil { }); err != nil {
return "", err return "", err
} }
return msgID, nil return msgID, nil
} }
func (b *Bxmpp) createXMPP() error { func (b *Bxmpp) postSlackCompatibleWebhook(msg config.Message) error {
if !strings.Contains(b.GetString("Jid"), "@") { type XMPPWebhook struct {
return fmt.Errorf("the Jid %s doesn't contain an @", b.GetString("Jid")) 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{ tc := &tls.Config{
ServerName: strings.Split(b.GetString("Jid"), "@")[1], ServerName: serverName,
InsecureSkipVerify: b.GetBool("SkipTLSVerify"), // nolint: gosec InsecureSkipVerify: b.GetBool("SkipTLSVerify"), // nolint: gosec
} }
@ -228,7 +300,13 @@ func (b *Bxmpp) handleXMPP() error {
for { for {
m, err := b.xc.Recv() m, err := b.xc.Recv()
if err != nil { 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) { switch v := m.(type) {
@ -236,6 +314,11 @@ func (b *Bxmpp) handleXMPP() error {
if v.Type == "groupchat" { if v.Type == "groupchat" {
b.Log.Debugf("== Receiving %#v", v) 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. // Skip invalid messages.
if b.skipMessage(v) { if b.skipMessage(v) {
continue continue
@ -260,6 +343,12 @@ func (b *Bxmpp) handleXMPP() error {
if v.ReplaceID != "" { if v.ReplaceID != "" {
msgID = v.ReplaceID msgID = v.ReplaceID
} }
var parentID string
if res, ok := b.OriginIDs.Get(v.ReplyID); ok {
parentID, _ = res.(string)
}
rmsg := config.Message{ rmsg := config.Message{
Username: b.parseNick(v.Remote), Username: b.parseNick(v.Remote),
Text: v.Text, Text: v.Text,
@ -267,6 +356,7 @@ func (b *Bxmpp) handleXMPP() error {
Account: b.Account, Account: b.Account,
Avatar: avatar, Avatar: avatar,
UserID: v.Remote, UserID: v.Remote,
ParentID: parentID,
ID: msgID, ID: msgID,
Event: event, Event: event,
} }
@ -339,7 +429,7 @@ func (b *Bxmpp) handleUploadFile(msg *config.Message) error {
func (b *Bxmpp) parseNick(remote string) string { func (b *Bxmpp) parseNick(remote string) string {
s := strings.Split(remote, "@") s := strings.Split(remote, "@")
if len(s) > 0 { if len(s) > 1 {
s = strings.Split(s[1], "/") s = strings.Split(s[1], "/")
if len(s) == 2 { if len(s) == 2 {
return s[1] // nick return s[1] // nick
@ -378,6 +468,11 @@ func (b *Bxmpp) skipMessage(message xmpp.Chat) bool {
return true return true
} }
// Ignore messages posted by our webhook
if b.GetString("WebhookURL") != "" && strings.Contains(message.ID, "webhookbot") {
return true
}
// skip delayed messages // skip delayed messages
return !message.Stamp.IsZero() && time.Since(message.Stamp).Minutes() > 5 return !message.Stamp.IsZero() && time.Since(message.Stamp).Minutes() > 5
} }

View File

@ -2,6 +2,7 @@ package bzulip
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io/ioutil" "io/ioutil"
"strconv" "strconv"
"strings" "strings"
@ -11,6 +12,7 @@ import (
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/version"
gzb "github.com/matterbridge/gozulipbot" gzb "github.com/matterbridge/gozulipbot"
) )
@ -27,7 +29,7 @@ func New(cfg *bridge.Config) bridge.Bridger {
} }
func (b *Bzulip) Connect() error { 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() bot.Init()
q, err := bot.RegisterAll() q, err := bot.RegisterAll()
b.q = q b.q = q
@ -104,29 +106,31 @@ func (b *Bzulip) getChannel(id int) string {
func (b *Bzulip) handleQueue() error { func (b *Bzulip) handleQueue() error {
for { for {
messages, err := b.q.GetEvents() 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 { 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 continue
} }
for _, m := range messages { for _, m := range messages {

File diff suppressed because it is too large Load Diff

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 FROM alpine:edge as certs
RUN apk --update add ca-certificates 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 FROM scratch
ARG VERSION=1.12.3
COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt 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 COPY --from=certs /bin/matterbridge /bin/matterbridge
RUN chmod +x /bin/matterbridge
ENTRYPOINT ["/bin/matterbridge"] ENTRYPOINT ["/bin/matterbridge"]

View File

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

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

@ -1,4 +1,5 @@
// +build !nowhatsapp // +build !nowhatsapp
// +build !whatsappmulti
package bridgemap package bridgemap

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

@ -14,7 +14,7 @@ import (
"github.com/d5/tengo/v2" "github.com/d5/tengo/v2"
"github.com/d5/tengo/v2/stdlib" "github.com/d5/tengo/v2/stdlib"
lru "github.com/hashicorp/golang-lru" lru "github.com/hashicorp/golang-lru"
"github.com/matterbridge/emoji" "github.com/kyokomi/emoji/v2"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
@ -66,7 +66,7 @@ func New(rootLogger *logrus.Logger, cfg *config.Gateway, r *Router) *Gateway {
func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string { func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string {
ID := protocol + " " + mID ID := protocol + " " + mID
if gw.Messages.Contains(ID) { if gw.Messages.Contains(ID) {
return mID return ID
} }
// If not keyed, iterate through cache for downstream, and infer upstream. // If not keyed, iterate through cache for downstream, and infer upstream.
@ -75,7 +75,7 @@ func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string {
ids := v.([]*BrMsgID) ids := v.([]*BrMsgID)
for _, downstreamMsgObj := range ids { for _, downstreamMsgObj := range ids {
if ID == downstreamMsgObj.ID { if ID == downstreamMsgObj.ID {
return strings.Replace(mid.(string), protocol+" ", "", 1) return mid.(string)
} }
} }
} }
@ -127,7 +127,7 @@ func (gw *Gateway) AddConfig(cfg *config.Gateway) error {
gw.logger.Errorf("mapChannels() failed: %s", err) gw.logger.Errorf("mapChannels() failed: %s", err)
} }
for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) { for _, br := range append(gw.MyConfig.In, append(gw.MyConfig.InOut, gw.MyConfig.Out...)...) {
br := br //scopelint br := br // scopelint
err := gw.AddBridge(&br) err := gw.AddBridge(&br)
if err != nil { if err != nil {
return err return err
@ -299,13 +299,30 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
igNicks := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreNicks")) igNicks := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreNicks"))
igMessages := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreMessages")) 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 true
} }
return false 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 { func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) string {
if dest.GetBool("StripNick") { if dest.GetBool("StripNick") {
re := regexp.MustCompile("[^a-zA-Z0-9]+") re := regexp.MustCompile("[^a-zA-Z0-9]+")
@ -337,20 +354,21 @@ func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) stri
} }
i++ 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.ReplaceAll(nick, "{BRIDGE}", br.Name)
nick = strings.Replace(nick, "{PROTOCOL}", br.Protocol, -1) nick = strings.ReplaceAll(nick, "{PROTOCOL}", br.Protocol)
nick = strings.Replace(nick, "{GATEWAY}", gw.Name, -1) nick = strings.ReplaceAll(nick, "{GATEWAY}", gw.Name)
nick = strings.Replace(nick, "{LABEL}", br.GetString("Label"), -1) nick = strings.ReplaceAll(nick, "{LABEL}", br.GetString("Label"))
nick = strings.Replace(nick, "{NICK}", msg.Username, -1) nick = strings.ReplaceAll(nick, "{NICK}", msg.Username)
nick = strings.Replace(nick, "{CHANNEL}", msg.Channel, -1) nick = strings.ReplaceAll(nick, "{USERID}", msg.UserID)
nick = strings.ReplaceAll(nick, "{CHANNEL}", msg.Channel)
tengoNick, err := gw.modifyUsernameTengo(msg, br) tengoNick, err := gw.modifyUsernameTengo(msg, br)
if err != nil { if err != nil {
gw.logger.Errorf("modifyUsernameTengo error: %s", err) gw.logger.Errorf("modifyUsernameTengo error: %s", err)
} }
nick = strings.Replace(nick, "{TENGO}", tengoNick, -1) //nolint:gocritic nick = strings.ReplaceAll(nick, "{TENGO}", tengoNick)
return nick return nick
} }
@ -385,6 +403,7 @@ func (gw *Gateway) modifyMessage(msg *config.Message) {
} }
// replace :emoji: to unicode // replace :emoji: to unicode
emoji.ReplacePadding = ""
msg.Text = emoji.Sprint(msg.Text) msg.Text = emoji.Sprint(msg.Text)
br := gw.Bridges[msg.Account] br := gw.Bridges[msg.Account]
@ -445,22 +464,25 @@ func (gw *Gateway) SendMessage(
msg.Avatar = gw.modifyAvatar(rmsg, dest) msg.Avatar = gw.modifyAvatar(rmsg, dest)
msg.Username = gw.modifyUsername(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 // for api we need originchannel as channel
if dest.Protocol == apiProtocol { if dest.Protocol == apiProtocol {
msg.Channel = rmsg.Channel msg.Channel = rmsg.Channel
} }
msg.ParentID = gw.getDestMsgID(rmsg.Protocol+" "+canonicalParentMsgID, dest, channel) msg.ParentID = gw.getDestMsgID(canonicalParentMsgID, dest, channel)
if msg.ParentID == "" { 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 // 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 != "" { if msg.ParentID == "" && rmsg.ParentID != "" {
msg.ParentID = "msg-parent-not-found" msg.ParentID = config.ParentIDNotFound
} }
drop, err := gw.modifyOutMessageTengo(rmsg, &msg, dest) drop, err := gw.modifyOutMessageTengo(rmsg, &msg, dest)
@ -495,7 +517,7 @@ func (gw *Gateway) SendMessage(
if mID != "" { if mID != "" {
gw.logger.Debugf("mID %s: %s", dest.Account, mID) gw.logger.Debugf("mID %s: %s", dest.Account, mID)
return mID, nil 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 return "", nil
} }
@ -549,6 +571,7 @@ func modifyInMessageTengo(filename string, msg *config.Message) error {
s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...)) s.SetImports(stdlib.GetModuleMap(stdlib.AllModuleNames()...))
_ = s.Add("msgText", msg.Text) _ = s.Add("msgText", msg.Text)
_ = s.Add("msgUsername", msg.Username) _ = s.Add("msgUsername", msg.Username)
_ = s.Add("msgUserID", msg.UserID)
_ = s.Add("msgAccount", msg.Account) _ = s.Add("msgAccount", msg.Account)
_ = s.Add("msgChannel", msg.Channel) _ = s.Add("msgChannel", msg.Channel)
c, err := s.Compile() c, err := s.Compile()
@ -577,6 +600,7 @@ func (gw *Gateway) modifyUsernameTengo(msg *config.Message, br *bridge.Bridge) (
_ = s.Add("result", "") _ = s.Add("result", "")
_ = s.Add("msgText", msg.Text) _ = s.Add("msgText", msg.Text)
_ = s.Add("msgUsername", msg.Username) _ = s.Add("msgUsername", msg.Username)
_ = s.Add("msgUserID", msg.UserID)
_ = s.Add("nick", msg.Username) _ = s.Add("nick", msg.Username)
_ = s.Add("msgAccount", msg.Account) _ = s.Add("msgAccount", msg.Account)
_ = s.Add("msgChannel", msg.Channel) _ = s.Add("msgChannel", msg.Channel)
@ -631,6 +655,7 @@ func (gw *Gateway) modifyOutMessageTengo(origmsg *config.Message, msg *config.Me
_ = s.Add("outEvent", msg.Event) _ = s.Add("outEvent", msg.Event)
_ = s.Add("msgText", msg.Text) _ = s.Add("msgText", msg.Text)
_ = s.Add("msgUsername", msg.Username) _ = s.Add("msgUsername", msg.Username)
_ = s.Add("msgUserID", msg.UserID)
_ = s.Add("msgDrop", drop) _ = s.Add("msgDrop", drop)
c, err := s.Compile() c, err := s.Compile()
if err != nil { if err != nil {

View File

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

View File

@ -110,7 +110,9 @@ func (r *Router) disableBridge(br *bridge.Bridge, err error) bool {
if r.BridgeValues().General.IgnoreFailureOnStart { if r.BridgeValues().General.IgnoreFailureOnStart {
r.logger.Error(err) r.logger.Error(err)
// setting this bridge empty // setting this bridge empty
*br = bridge.Bridge{} *br = bridge.Bridge{
Log: br.Log,
}
return true return true
} }
return false return false
@ -160,7 +162,7 @@ func (r *Router) handleReceive() {
// For some bridges we always add/update the message ID. // For some bridges we always add/update the message ID.
// This is necessary as msgIDs will change if a bridge returns // This is necessary as msgIDs will change if a bridge returns
// a different ID in response to edits. // a different ID in response to edits.
if !exists || msg.Protocol == "discord" { if !exists {
gw.Messages.Add(msg.Protocol+" "+msg.ID, msgIDs) gw.Messages.Add(msg.Protocol+" "+msg.ID, msgIDs)
} }
} }

185
go.mod
View File

@ -1,58 +1,153 @@
module github.com/42wim/matterbridge module github.com/42wim/matterbridge
require ( require (
github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f
github.com/Jeffail/gabs v1.1.1 // indirect github.com/Benau/tgsconverter v0.0.0-20210809170556-99f4a4f6337f
github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560 github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560
github.com/Rhymen/go-whatsapp v0.1.2-0.20201122130733-6e5488ac98df github.com/Rhymen/go-whatsapp v0.1.2-0.20211102134409-31a2e740845c
github.com/d5/tengo/v2 v2.6.2 github.com/SevereCloud/vksdk/v2 v2.17.0
github.com/davecgh/go-spew v1.1.1 github.com/bwmarrin/discordgo v0.28.1
github.com/fsnotify/fsnotify v1.4.9 github.com/d5/tengo/v2 v2.17.0
github.com/go-telegram-bot-api/telegram-bot-api v1.0.1-0.20200524105306-7434b0456e81 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/gomarkdown/markdown v0.0.0-20201113031856-722100d81a8e github.com/fsnotify/fsnotify v1.7.0
github.com/google/gops v0.3.13 github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2
github.com/gopackage/ddp v0.0.0-20170117053602-652027933df4 // indirect github.com/google/gops v0.3.27
github.com/gorilla/schema v1.2.0 github.com/gorilla/schema v1.4.1
github.com/gorilla/websocket v1.4.2 github.com/harmony-development/shibshib v0.0.0-20220101224523-c98059d09cfa
github.com/hashicorp/golang-lru v0.5.4 github.com/hashicorp/golang-lru v1.0.2
github.com/jpillora/backoff v1.0.0 github.com/jpillora/backoff v1.0.0
github.com/keybase/go-keybase-chat-bot v0.0.0-20200505163032-5cacf52379da github.com/keybase/go-keybase-chat-bot v0.0.0-20221220212439-e48d9abd2c20
github.com/labstack/echo/v4 v4.1.17 github.com/kyokomi/emoji/v2 v2.2.13
github.com/lrstanley/girc v0.0.0-20190801035559-4fc93959e1a7 github.com/labstack/echo/v4 v4.12.0
github.com/matrix-org/gomatrix v0.0.0-20200827122206-7dd5e2a05bcd github.com/lrstanley/girc v0.0.0-20240823210506-80555f2adb03
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20200411204219-d5c18ce75048 github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20211016222428-79310a412696
github.com/matterbridge/discordgo v0.22.1 github.com/matterbridge/go-xmpp v0.0.0-20240523230155-7154bfeb76e8
github.com/matterbridge/emoji v2.1.1-0.20191117213217-af507f6b02db+incompatible github.com/matterbridge/gomatrix v0.0.0-20220411225302-271e5088ea27
github.com/matterbridge/go-xmpp v0.0.0-20200418225040-c8a3a57b4050 github.com/matterbridge/gozulipbot v0.0.0-20211023205727-a19d6c1f3b75
github.com/matterbridge/gozulipbot v0.0.0-20200820220548-be5824faa913
github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba
github.com/mattermost/mattermost-server/v5 v5.29.0 github.com/matterbridge/matterclient v0.0.0-20240817214420-3d4c3aef3dc1
github.com/mattn/godown v0.0.0-20201027140031-2c7783b24de7 github.com/matterbridge/telegram-bot-api/v6 v6.5.0
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect github.com/mattermost/mattermost/server/public v0.1.6
github.com/missdeer/golib v1.0.4 github.com/mattn/godown v0.0.1
github.com/mreiferson/go-httpclient v0.0.0-20160630210159-31f0106b4474 // indirect github.com/mdp/qrterminal v1.0.1
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect github.com/mitchellh/mapstructure v1.5.0
github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9 github.com/nelsonken/gomf v0.0.0-20190423072027-c65cc0469e94
github.com/olahol/melody v1.2.1
github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c
github.com/rs/xid v1.2.1 github.com/rs/xid v1.5.0
github.com/russross/blackfriday v1.5.2 github.com/russross/blackfriday v1.6.0
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
github.com/shazow/ssh-chat v1.10.1 github.com/shazow/ssh-chat v1.10.1
github.com/sirupsen/logrus v1.7.0 github.com/sirupsen/logrus v1.9.3
github.com/slack-go/slack v0.7.2 github.com/slack-go/slack v0.14.0
github.com/spf13/viper v1.7.1 github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.6.1 github.com/stretchr/testify v1.9.0
github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50 github.com/vincent-petithory/dataurl v1.0.0
github.com/writeas/go-strip-markdown v2.0.1+incompatible github.com/writeas/go-strip-markdown v2.0.1+incompatible
github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
github.com/yaegashi/msgraph.go v0.1.4 github.com/yaegashi/msgraph.go v0.1.4
github.com/zfjagann/golang-ring v0.0.0-20190304061218-d34796e0a6c2 github.com/zfjagann/golang-ring v0.0.0-20220330170733-19bcea1b6289
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5 go.mau.fi/whatsmeow v0.0.0-20240821142752-3d63c6fcc1a7
golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58 golang.org/x/image v0.19.0
gomod.garykim.dev/nc-talk v0.1.5 golang.org/x/oauth2 v0.22.0
gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376 golang.org/x/text v0.17.0
layeh.com/gumble v0.0.0-20200818122324-146f9205029b gomod.garykim.dev/nc-talk v0.3.0
google.golang.org/protobuf v1.34.2
layeh.com/gumble v0.0.0-20221205141517-d1df60a3cc14
modernc.org/sqlite v1.32.0
) )
go 1.15 require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Benau/go_rlottie v0.0.0-20210807002906-98c1b2421989 // indirect
github.com/Jeffail/gabs v1.4.0 // indirect
github.com/apex/log v1.9.0 // indirect
github.com/av-elier/go-decimal-to-rational v0.0.0-20191127152832-89e6aad02ecf // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gopackage/ddp v0.0.3 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-hclog v1.6.3 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.6.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/kettek/apng v0.0.0-20191108220231-414630eed80f // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/labstack/gommon v0.4.2 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect
github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect
github.com/mattermost/logr/v2 v2.0.21 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/monaco-io/request v1.0.5 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nxadm/tail v1.4.11 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/pborman/uuid v1.2.1 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/philhofer/fwd v1.1.3-0.20240612014219-fbbf4953d986 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rickb777/date v1.12.4 // indirect
github.com/rickb777/plural v1.2.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/zerolog v1.33.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4 // indirect
github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882 // indirect
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/tinylib/msgp v1.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wiggin77/merror v1.0.5 // indirect
github.com/wiggin77/srslog v1.0.1 // indirect
go.mau.fi/libsignal v0.1.1 // indirect
go.mau.fi/util v0.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.25.0 // indirect
golang.org/x/exp v0.0.0-20240707233637-46b078467d37 // indirect
golang.org/x/net v0.27.0 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/term v0.22.0 // indirect
golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240722135656-d784300faade // indirect
google.golang.org/grpc v1.65.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.55.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
rsc.io/qr v0.2.0 // indirect
)
//replace github.com/matrix-org/gomatrix => github.com/matterbridge/gomatrix v0.0.0-20220205235239-607eb9ee6419
go 1.22.0

1416
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,7 @@ import (
"log" "log"
"net" "net"
"net/http" "net/http"
"regexp"
) )
// Message for rocketchat outgoing webhook. // Message for rocketchat outgoing webhook.
@ -68,7 +69,6 @@ func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
msg := Message{} msg := Message{}
body, err := ioutil.ReadAll(r.Body) body, err := ioutil.ReadAll(r.Body)
log.Println(string(body))
if err != nil { if err != nil {
log.Println(err) log.Println(err)
http.NotFound(w, r) http.NotFound(w, r)
@ -89,7 +89,11 @@ func (c *Client) ServeHTTP(w http.ResponseWriter, r *http.Request) {
msg.ChannelName = "#" + msg.ChannelName msg.ChannelName = "#" + msg.ChannelName
if c.Token != "" { if c.Token != "" {
if msg.Token != c.Token { if msg.Token != c.Token {
log.Println("invalid token " + msg.Token + " from " + r.RemoteAddr) if regexp.MustCompile(`[^a-zA-Z0-9]+`).MatchString(msg.Token) {
log.Println("invalid token " + msg.Token + " from " + r.RemoteAddr)
} else {
log.Println("invalid token from " + r.RemoteAddr)
}
http.NotFound(w, r) http.NotFound(w, r)
return return
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

View File

@ -10,15 +10,13 @@ import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/gateway" "github.com/42wim/matterbridge/gateway"
"github.com/42wim/matterbridge/gateway/bridgemap" "github.com/42wim/matterbridge/gateway/bridgemap"
"github.com/42wim/matterbridge/version"
"github.com/google/gops/agent" "github.com/google/gops/agent"
prefixed "github.com/matterbridge/logrus-prefixed-formatter" prefixed "github.com/matterbridge/logrus-prefixed-formatter"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
var ( var (
version = "1.20.1-dev"
githash string
flagConfig = flag.String("conf", "matterbridge.toml", "config file") flagConfig = flag.String("conf", "matterbridge.toml", "config file")
flagDebug = flag.Bool("debug", false, "enable debug") flagDebug = flag.Bool("debug", false, "enable debug")
flagVersion = flag.Bool("version", false, "show version") flagVersion = flag.Bool("version", false, "show version")
@ -28,7 +26,7 @@ var (
func main() { func main() {
flag.Parse() flag.Parse()
if *flagVersion { if *flagVersion {
fmt.Printf("version: %s %s\n", version, githash) fmt.Printf("version: %s %s\n", version.Release, version.GitHash)
return return
} }
@ -43,8 +41,8 @@ func main() {
} }
} }
logger.Printf("Running version %s %s", version, githash) logger.Printf("Running version %s %s", version.Release, version.GitHash)
if strings.Contains(version, "-dev") { if strings.Contains(version.Release, "-dev") {
logger.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.") logger.Println("WARNING: THIS IS A DEVELOPMENT VERSION. Things may break.")
} }
@ -67,7 +65,7 @@ func main() {
if err = r.Start(); err != nil { if err = r.Start(); err != nil {
logger.Fatalf("Starting gateway failed: %s", err) logger.Fatalf("Starting gateway failed: %s", err)
} }
logger.Printf("Gateway(s) started succesfully. Now relaying messages") logger.Printf("Gateway(s) started successfully. Now relaying messages")
select {} select {}
} }

61
matterbridge.toml.multi Normal file
View File

@ -0,0 +1,61 @@
#WARNING: as this file contains credentials, be sure to set correct file permissions
[irc]
[irc.foo]
Server="irc.myfooserver.com:6667"
Nick="matterbot"
# Can also connect to multiple different servers of the same protocol:
[irc]
[irc.bar]
Server="irc.mybarserver.com:6667"
Nick="matterbot"
[telegram]
[telegram.mytelegram]
Token="123456789:FillInYourTokenHereThatIsImportant"
[mattermost]
[mattermost.work]
#do not prefix it wit http:// or https://
Server="yourmattermostserver.domain"
Team="yourteam"
Login="yourlogin"
Password="yourpass"
PrefixMessagesWithNick=true
# Bridge 1: Copy all messages from all rooms to all rooms.
# This shows how you can have multiple rooms in a single bridge.
[[gateway]]
name="cats-are-cool"
enable=true
[[gateway.inout]]
account="irc.foo"
channel="#cats-are-cool"
[[gateway.inout]]
account="irc.bar"
channel="#cats-are-cool"
[[gateway.inout]]
account="telegram.mytelegram"
channel="-1234567890123"
[[gateway.inout]]
account="mattermost.work"
channel="cats-are-cool"
# Bridge 2: Copy some messages from some rooms to some rooms.
# This shows how you can have multiple bridges.
[[gateway]]
name="dog-announcements"
enable=true
[[gateway.in]]
account="irc.foo"
channel="#dog-announcements"
[[gateway.in]]
account="irc.bar"
channel="#dog-announcements"
[[gateway.out]]
account="telegram.mytelegram"
channel="-9876543219876"
[[gateway.out]]
account="mattermost.work"
channel="dog-announcements"

View File

@ -9,12 +9,12 @@
[irc] [irc]
#You can configure multiple servers "[irc.name]" or "[irc.name2]" #You can configure multiple servers "[irc.name]" or "[irc.name2]"
#In this example we use [irc.freenode] #In this example we use [irc.libera]
#REQUIRED #REQUIRED
[irc.freenode] [irc.libera]
#irc server to connect to. #irc server to connect to.
#REQUIRED #REQUIRED
Server="irc.freenode.net:6667" Server="irc.libera.chat:6667"
#Password for irc server (if necessary) #Password for irc server (if necessary)
#OPTIONAL (default "") #OPTIONAL (default "")
@ -24,7 +24,14 @@ Password=""
#OPTIONAL (default false) #OPTIONAL (default false)
UseTLS=false UseTLS=false
#Enable SASL (PLAIN) authentication. (freenode requires this from eg AWS hosts) #Use client certificate - see CertFP https://libera.chat/guides/certfp.html
#Specify filename which contains private key and cert
#OPTIONAL (default "")
#
#TLSClientCertificate="cert.pem"
TLSClientCertificate=""
#Enable SASL (PLAIN) authentication. (libera requires this from eg AWS hosts)
#It uses NickServNick and NickServPassword as login and password #It uses NickServNick and NickServPassword as login and password
#OPTIONAL (default false) #OPTIONAL (default false)
UseSASL=false UseSASL=false
@ -34,6 +41,11 @@ UseSASL=false
#OPTIONAL (default false) #OPTIONAL (default false)
SkipTLSVerify=true SkipTLSVerify=true
#Local address to use for server connection
#Note that Server and Bind must resolve to addresses of the same family.
#OPTIONAL (default "")
Bind=""
#If you know your charset, you can specify it manually. #If you know your charset, you can specify it manually.
#Otherwise it tries to detect this automatically. Select one below #Otherwise it tries to detect this automatically. Select one below
# "iso-8859-2:1987", "iso-8859-9:1989", "866", "latin9", "iso-8859-10:1992", "iso-ir-109", "hebrew", # "iso-8859-2:1987", "iso-8859-9:1989", "866", "latin9", "iso-8859-10:1992", "iso-ir-109", "hebrew",
@ -55,7 +67,15 @@ Charset=""
#REQUIRED #REQUIRED
Nick="matterbot" Nick="matterbot"
#If you registered your bot with a service like Nickserv on freenode. #Real name/gecos displayed in e.g. /WHOIS and /WHO
#OPTIONAL (defaults to the nick)
RealName="Matterbridge instance on IRC"
#IRC username/ident preceding the hostname in hostmasks and /WHOIS
#OPTIONAL (defaults to the nick)
UserName="bridge"
#If you registered your bot with a service like Nickserv on libera.
#Also being used when UseSASL=true #Also being used when UseSASL=true
# #
#Note: if you want do to quakenet auth, set NickServNick="Q@CServe.quakenet.org" #Note: if you want do to quakenet auth, set NickServNick="Q@CServe.quakenet.org"
@ -76,20 +96,24 @@ MessageDelay=1300
#Maximum amount of messages to hold in queue. If queue is full #Maximum amount of messages to hold in queue. If queue is full
#messages will be dropped. #messages will be dropped.
#<message clipped> will be add to the message that fills the queue. #<clipped message> will be add to the message that fills the queue.
#OPTIONAL (default 30) #OPTIONAL (default 30)
MessageQueue=30 MessageQueue=30
#Maximum length of message sent to irc server. If it exceeds #Maximum length of message sent to irc server. If it exceeds
#<message clipped> will be add to the message. #<clipped message> will be add to the message.
#OPTIONAL (default 400) #OPTIONAL (default 400)
MessageLength=400 MessageLength=400
#Split messages on MessageLength instead of showing the <message clipped> #Split messages on MessageLength instead of showing the <clipped message>
#WARNING: this could lead to flooding #WARNING: this could lead to flooding
#OPTIONAL (default false) #OPTIONAL (default false)
MessageSplit=false MessageSplit=false
#Message to show when a message is too big
#Default "<clipped message>"
MessageClipped="<clipped message>"
#Delay in seconds to rejoin a channel when kicked #Delay in seconds to rejoin a channel when kicked
#OPTIONAL (default 0) #OPTIONAL (default 0)
RejoinDelay=0 RejoinDelay=0
@ -98,10 +122,11 @@ RejoinDelay=0
#Only works in IRC right now. #Only works in IRC right now.
ColorNicks=false ColorNicks=false
#RunCommands allows you to send RAW irc commands after connection #RunCommands allows you to send RAW irc commands after connection.
#The string {BOTNICK} (case sensitive) will be replaced with the bot's current nickname.
#Array of strings #Array of strings
#OPTIONAL (default empty) #OPTIONAL (default empty)
RunCommands=["PRIVMSG user hello","PRIVMSG chanserv something"] RunCommands=["PRIVMSG user hello","PRIVMSG chanserv something", "MODE {BOTNICK} +B"]
#PingDelay specifies how long to wait to send a ping to the irc server. #PingDelay specifies how long to wait to send a ping to the irc server.
#You can use s for second, m for minute #You can use s for second, m for minute
@ -138,7 +163,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace. #nicks you want to replace.
#see replacemessages for syntaxa #see replacemessages for syntax
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]
@ -163,7 +188,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack, discord #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -173,7 +198,7 @@ ShowJoinPart=false
VerboseJoinPart=false VerboseJoinPart=false
#Do not send joins/parts to other bridges #Do not send joins/parts to other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
#OPTIONAL (default false) #OPTIONAL (default false)
NoSendJoinPart=false NoSendJoinPart=false
@ -193,6 +218,19 @@ ShowTopicChange=false
#OPTIONAL (default 0) #OPTIONAL (default 0)
JoinDelay=0 JoinDelay=0
#Use the optional RELAYMSG extension for username spoofing on IRC.
#This requires an IRCd that supports the draft/relaymsg specification: currently this includes
#Oragono 2.4.0+ and InspIRCd 3 with the m_relaymsg contrib module.
#See https://github.com/42wim/matterbridge/issues/667#issuecomment-634214165 for more details.
#Spoofed nicks will use the configured RemoteNickFormat, replacing reserved IRC characters
#(!+%@&#$:'"?*,.) with a hyphen (-).
#On most configurations, the RemoteNickFormat must include a separator character such as "/".
#You should make sure that the settings here match your IRCd.
#This option overrides ColorNicks.
#OPTIONAL (default false)
UseRelayMsg=false
#RemoteNickFormat="{NICK}/{PROTOCOL}"
################################################################### ###################################################################
#XMPP section #XMPP section
################################################################### ###################################################################
@ -206,12 +244,16 @@ JoinDelay=0
#REQUIRED #REQUIRED
Server="jabber.example.com:5222" Server="jabber.example.com:5222"
#Use anonymous MUC login
#OPTIONAL (default false)
Anonymous=false
#Jid #Jid
#REQUIRED #REQUIRED if Anonymous=false
Jid="user@example.com" Jid="user@example.com"
#Password #Password
#REQUIRED #REQUIRED if Anonymous=false
Password="yourpass" Password="yourpass"
#MUC #MUC
@ -259,7 +301,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#Nicks you want to replace. #Nicks you want to replace.
#See ReplaceMessages for syntaxA #See ReplaceMessages for syntax
#OPTIONAL (default empty) #OPTIONAL (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]
@ -283,7 +325,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack, discord #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -297,6 +339,11 @@ StripNick=false
#OPTIONAL (default false) #OPTIONAL (default false)
ShowTopicChange=false ShowTopicChange=false
#Enable sending messages using a webhook instead of regular MUC messages.
#Only works with a prosody server using mod_slack_webhook. Does not support editing.
#OPTIONAL (default "")
WebhookURL="https://yourdomain/prosody/msg/someid"
################################################################### ###################################################################
#mattermost section #mattermost section
################################################################### ###################################################################
@ -362,6 +409,10 @@ SkipTLSVerify=true
## RELOADABLE SETTINGS ## RELOADABLE SETTINGS
## Settings below can be reloaded by editing the file ## Settings below can be reloaded by editing the file
# UseUserName shows the username instead of the server nickname
# OPTIONAL (default false)
UseUserName=false
#how to format the list of IRC nicks when displayed in mattermost. #how to format the list of IRC nicks when displayed in mattermost.
#Possible options are "table" and "plain" #Possible options are "table" and "plain"
#OPTIONAL (default plain) #OPTIONAL (default plain)
@ -417,7 +468,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace. #nicks you want to replace.
#see replacemessages for syntaxa #see replacemessages for syntax
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]
@ -441,12 +492,12 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack, discord #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
#Do not send joins/parts to other bridges #Do not send joins/parts to other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
#OPTIONAL (default false) #OPTIONAL (default false)
NoSendJoinPart=false NoSendJoinPart=false
@ -462,86 +513,9 @@ ShowTopicChange=false
################################################################### ###################################################################
#Gitter section #Gitter section
#Best to make a dedicated gitter account for the bot. #Gitter has been moved to matrix - see https://github.com/42wim/matterbridge/issues/1969 how to migrate
################################################################### ###################################################################
[gitter]
#You can configure multiple servers "[gitter.name]" or "[gitter.name2]"
#In this example we use [gitter.myproject]
#REQUIRED
[gitter.myproject]
#Token to connect with Gitter API
#You can get your token by going to https://developer.gitter.im/docs/welcome and SIGN IN
#REQUIRED
Token="Yourtokenhere"
## RELOADABLE SETTINGS
## Settings below can be reloaded by editing the file
#Nicks you want to ignore.
#Regular expressions supported
#Messages from those users will not be sent to other bridges.
#OPTIONAL
IgnoreNicks="ircspammer1 ircspammer2"
#Messages you want to ignore.
#Messages matching these regexp will be ignored and not sent to other bridges
#See https://regex-golang.appspot.com/assets/html/index.html for more regex info
#OPTIONAL (example below ignores messages starting with ~~ or messages containing badword
IgnoreMessages="^~~ badword"
#messages you want to replace.
#it replaces outgoing messages from the bridge.
#so you need to place it by the sending bridge definition.
#regular expressions supported
#some examples:
#this replaces cat => dog and sleep => awake
#replacemessages=[ ["cat","dog"], ["sleep","awake"] ]
#this replaces every number with number. 123 => numbernumbernumber
#replacemessages=[ ["[0-9]","number"] ]
#optional (default empty)
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
#Extractnicks is used to for example rewrite messages from other relaybots
#See https://github.com/42wim/matterbridge/issues/713 and https://github.com/42wim/matterbridge/issues/466
#some examples:
#this replaces a message like "Relaybot: <relayeduser> something interesting" to "relayeduser: something interesting"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ] ]
#you can use multiple entries for multiplebots
#this also replaces a message like "otherbot: (relayeduser) something else" to "relayeduser: something else"
#ExtractNicks=[ [ "Relaybot", "<(.*?)>\\s+" ],[ "otherbot","\\((.*?)\\)\\s+" ]
#OPTIONAL (default empty)
ExtractNicks=[ ["otherbot","<(.*?)>\\s+" ] ]
#extra label that can be used in the RemoteNickFormat
#optional (default empty)
Label=""
#RemoteNickFormat defines how remote users appear on this bridge
#See [general] config section for default options
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack, discord
#OPTIONAL (default false)
ShowJoinPart=false
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
#It will strip other characters from the nick
#OPTIONAL (default false)
StripNick=false
#Enable to show topic changes from other bridges
#Only works hiding/show topic changes from slack bridge for now
#OPTIONAL (default false)
ShowTopicChange=false
################################################################### ###################################################################
# #
# Keybase # Keybase
@ -612,7 +586,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace. #nicks you want to replace.
#see replacemessages for syntaxa #see replacemessages for syntax
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]
@ -636,7 +610,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack, discord #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -751,7 +725,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace. #nicks you want to replace.
#see replacemessages for syntaxa #see replacemessages for syntax
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]
@ -775,12 +749,12 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack, discord #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
#Do not send joins/parts to other bridges #Do not send joins/parts to other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
#OPTIONAL (default false) #OPTIONAL (default false)
NoSendJoinPart=false NoSendJoinPart=false
@ -808,6 +782,14 @@ PreserveThreading=false
#OPTIONAL (default false) #OPTIONAL (default false)
ShowUserTyping=false ShowUserTyping=false
#Message to show when a message is too big
#Default "<clipped message>"
MessageClipped="<clipped message>"
#If enabled use the slack "Real Name" as username.
#OPTIONAL (default false)
UseFullName=false
################################################################### ###################################################################
#discord section #discord section
################################################################### ###################################################################
@ -830,6 +812,14 @@ Server="yourservername"
## All settings below can be reloaded by editing the file. ## All settings below can be reloaded by editing the file.
## They are also all optional. ## They are also all optional.
# AllowMention controls which mentions are allowed. If not specified, all mentions are allowed.
# Note that even when a mention is not allowed, it will still be displayed nicely and be clickable. It just prevents the ping/notification.
#
# "everyone" allows @everyone and @here mentions
# "roles" allows @role mentions
# "users" allows @user mentions
AllowMention=["everyone", "roles", "users"]
# ShowEmbeds shows the title, description and URL of embedded messages (sent by other bots) # ShowEmbeds shows the title, description and URL of embedded messages (sent by other bots)
ShowEmbeds=false ShowEmbeds=false
@ -846,10 +836,11 @@ UseUserName=false
# UseDiscriminator appends the `#xxxx` discriminator when used with UseUserName # UseDiscriminator appends the `#xxxx` discriminator when used with UseUserName
UseDiscriminator=false UseDiscriminator=false
# WebhookURL sends messages in the style of puppets. # AutoWebhooks automatically configures message sending in the style of puppets.
# This only works if you have one discord channel, if you have multiple discord channels you'll have to specify it in the gateway config # This is an easier alternative to manually configuring "WebhookURL" for each gateway,
# Example: "https://discordapp.com/api/webhooks/1234/abcd_xyzw" # as turning this on will automatically load or create webhooks for each channel.
WebhookURL="" # This feature requires the "Manage Webhooks" permission (either globally or as per-channel).
AutoWebhooks=false
# EditDisable disables sending of edits to other bridges # EditDisable disables sending of edits to other bridges
EditDisable=false EditDisable=false
@ -934,6 +925,17 @@ ShowTopicChange=false
# Supported from the following bridges: slack # Supported from the following bridges: slack
SyncTopic=false SyncTopic=false
# Message to show when a message is too big
# Default "<clipped message>"
MessageClipped="<clipped message>"
# Before clipping, try to split messages into at most this many parts. 0 is treated like 1.
# Be careful with large numbers, as this might cause flooding.
# Example: A maximum telegram message of 4096 bytes is received. This requires 3 Discord
# messages (each capped at a hardcoded 1950 bytes).
# Default 1
MessageSplitMaxCount=3
################################################################### ###################################################################
#telegram section #telegram section
################################################################### ###################################################################
@ -969,6 +971,12 @@ DisableWebPagePreview=false
#OPTIONAL (default false) #OPTIONAL (default false)
UseFirstName=false UseFirstName=false
#If enabled use the "Full Name" as username. If this is empty use the Username
#If disabled use the "Username" as username. If this is empty use the First Name and Last Name as Full Name
#If all names are empty, username will be "unknown"
#OPTIONAL (default false)
UseFullName=false
#WARNING! If enabled this will relay GIF/stickers/documents and other attachments as URLs #WARNING! If enabled this will relay GIF/stickers/documents and other attachments as URLs
#Those URLs will contain your bot-token. This may not be what you want. #Those URLs will contain your bot-token. This may not be what you want.
#For now there is no secure way to relay GIF/stickers/documents without seeing your token. #For now there is no secure way to relay GIF/stickers/documents without seeing your token.
@ -992,6 +1000,13 @@ QuoteFormat="{MESSAGE} (re @{QUOTENICK}: {QUOTEMESSAGE})"
#OPTIONAL (default false) #OPTIONAL (default false)
MediaConvertWebPToPNG=false MediaConvertWebPToPNG=false
#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
#MediaConvertTgs="png"
#Disable sending of edits to other bridges #Disable sending of edits to other bridges
#OPTIONAL (default false) #OPTIONAL (default false)
EditDisable=false EditDisable=false
@ -1025,7 +1040,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace. #nicks you want to replace.
#see replacemessages for syntaxa #see replacemessages for syntax
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]
@ -1053,7 +1068,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack, discord #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -1067,6 +1082,12 @@ StripNick=false
#OPTIONAL (default false) #OPTIONAL (default false)
ShowTopicChange=false ShowTopicChange=false
#Opportunistically preserve threaded replies between Telegram groups.
#This only works if the parent message is still in the cache.
#Cache is flushed between restarts.
#OPTIONAL (default false)
PreserveThreading=false
################################################################### ###################################################################
#rocketchat section #rocketchat section
################################################################### ###################################################################
@ -1160,7 +1181,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace. #nicks you want to replace.
#see replacemessages for syntaxa #see replacemessages for syntax
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]
@ -1184,7 +1205,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack, discord #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -1211,12 +1232,16 @@ ShowTopicChange=false
#REQUIRED #REQUIRED
Server="https://matrix.org" Server="https://matrix.org"
#login/pass of your bot. #Authentication for your bot.
#You can use either login/password OR mxid/token. The latter will be preferred if found.
#Use a dedicated user for this and not your own! #Use a dedicated user for this and not your own!
#Messages sent from this user will not be relayed to avoid loops. #Messages sent from this user will not be relayed to avoid loops.
#REQUIRED #REQUIRED
Login="yourlogin" Login="yourlogin"
Password="yourpass" Password="yourpass"
#OR
MxID="@yourlogin:domain.tld"
Token="tokenforthebotuser"
#Whether to send the homeserver suffix. eg ":matrix.org" in @username:matrix.org #Whether to send the homeserver suffix. eg ":matrix.org" in @username:matrix.org
#to other bridges, or only send "username".(true only sends username) #to other bridges, or only send "username".(true only sends username)
@ -1234,12 +1259,14 @@ HTMLDisable=false
# UseUserName shows the username instead of the server nickname # UseUserName shows the username instead of the server nickname
UseUserName=false UseUserName=false
#Whether to prefix messages from other bridges to matrix with the sender's nick. # Matrix quotes replies and as of matterbridge 1.24.0 we strip those as this causes
#Useful if username overrides for incoming webhooks isn't enabled on the # issues with bridges support threading and have PreserveThreading enabled.
#matrix server. If you set PrefixMessagesWithNick to true, each message # But if you for example use mattermost or discord with webhooks you'll need to enable
#from bridge to matrix will by default be prefixed by the RemoteNickFormat setting. i # this (and keep PreserveThreading disabled) if you want something that looks like a reply from matrix.
#OPTIONAL (default false) # See issues:
PrefixMessagesWithNick=false # - https://github.com/42wim/matterbridge/issues/1819
# - https://github.com/42wim/matterbridge/issues/1780
KeepQuotedReply=false
#Nicks you want to ignore. #Nicks you want to ignore.
#Regular expressions supported #Regular expressions supported
@ -1266,7 +1293,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace. #nicks you want to replace.
#see replacemessages for syntaxa #see replacemessages for syntax
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]
@ -1290,10 +1317,15 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack, discord #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
#Rename the bot in the current room to the username of the message
#This will make an additional API request per message and will probably count towards rate limits
#OPTIONAL (default false)
SpoofUsername=false
#StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285 #StripNick only allows alphanumerical nicks. See https://github.com/42wim/matterbridge/issues/285
#It will strip other characters from the nick #It will strip other characters from the nick
#OPTIONAL (default false) #OPTIONAL (default false)
@ -1358,7 +1390,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace. #nicks you want to replace.
#see replacemessages for syntaxa #see replacemessages for syntax
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]
@ -1382,7 +1414,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack, discord #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -1397,9 +1429,7 @@ StripNick=false
ShowTopicChange=false ShowTopicChange=false
################################################################### ###################################################################
#
# NCTalk (Nextcloud Talk) # NCTalk (Nextcloud Talk)
#
################################################################### ###################################################################
[nctalk.bridge] [nctalk.bridge]
@ -1421,10 +1451,11 @@ Password = "talkuserpass"
# Suffix for Guest Users # Suffix for Guest Users
GuestSuffix = " (Guest)" GuestSuffix = " (Guest)"
# Separate display name (Note: needs to be configured from Nextcloud Talk to work)
SeparateDisplayName=false
################################################################### ###################################################################
#
# Mumble # Mumble
#
################################################################### ###################################################################
[mumble.bridge] [mumble.bridge]
@ -1435,7 +1466,7 @@ Server = "mumble.yourdomain.me:64738"
# Nickname to log in as # Nickname to log in as
Nick = "matterbridge" Nick = "matterbridge"
# Some servers require a password # Some servers require a password
# OPTIONAL (default empty) # OPTIONAL (default empty)
Password = "serverpasswordhere" Password = "serverpasswordhere"
@ -1467,10 +1498,30 @@ TLSCACertificate=mumble-ca.crt
# OPTIONAL (default false) # OPTIONAL (default false)
SkipTLSVerify=false SkipTLSVerify=false
#Message to show when a message is too big
#Default "<clipped message>"
MessageClipped="<clipped message>"
#Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
#OPTIONAL (default false)
ShowJoinPart=false
#Do not send joins/parts to other bridges
#OPTIONAL (default false)
NoSendJoinPart=false
###################################################################
#VK
################################################################### ###################################################################
# #
[vk.myvk]
#Group access token
#See https://vk.com/dev/bots_docs
Token="Yourtokenhere"
###################################################################
# WhatsApp # WhatsApp
#
################################################################### ###################################################################
[whatsapp.bridge] [whatsapp.bridge]
@ -1497,9 +1548,7 @@ Label="Organization"
################################################################### ###################################################################
#
# zulip # zulip
#
################################################################### ###################################################################
[zulip] [zulip]
@ -1550,7 +1599,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace. #nicks you want to replace.
#see replacemessages for syntaxa #see replacemessages for syntax
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]
@ -1574,7 +1623,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> " RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#Enable to show users joins/parts from other bridges #Enable to show users joins/parts from other bridges
#Currently works for messages from the following bridges: irc, mattermost, slack, discord #Currently works for messages from the following bridges: irc, mattermost, mumble, slack, discord
#OPTIONAL (default false) #OPTIONAL (default false)
ShowJoinPart=false ShowJoinPart=false
@ -1588,6 +1637,18 @@ StripNick=false
#OPTIONAL (default false) #OPTIONAL (default false)
ShowTopicChange=false ShowTopicChange=false
###################################################################
# Harmony
###################################################################
[harmony.chat_harmonyapp_io]
Homeserver = "https://chat.harmonyapp.io:2289"
Token = "your token goes here"
UserID = "user id of the bot account"
Community = "community id that channels will be located in"
UseUserName = true
RemoteNickFormat = "{NICK}"
################################################################### ###################################################################
#API #API
################################################################### ###################################################################
@ -1632,7 +1693,9 @@ RemoteNickFormat="{NICK}"
## Settings below can be reloaded by editing the file ## Settings below can be reloaded by editing the file
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick / username. #The string "{NICK}" (case sensitive) will be replaced by the actual nick.
#The string "{NOPINGNICK}" (case sensitive) will be replaced by the actual nick / username, but with a ZWSP inside the nick, so the irc user with the same nick won't get pinged.
#The string "{USERID}" (case sensitive) will be replaced by the user ID.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge #The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
#The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge #The string "{PROTOCOL}" (case sensitive) will be replaced by the protocol used by the bridge
@ -1658,7 +1721,7 @@ StripNick=false
#The MediaServerDownload will be used so that bridges without native uploading support: #The MediaServerDownload will be used so that bridges without native uploading support:
#gitter, irc and xmpp will be shown links to the files on MediaServerDownload #gitter, irc and xmpp will be shown links to the files on MediaServerDownload
# #
#More information https://github.com/42wim/matterbridge/wiki/Mediaserver-setup-%5Badvanced%5D #More information https://github.com/42wim/matterbridge/wiki/Mediaserver-setup-%28advanced%29
#OPTIONAL (default empty) #OPTIONAL (default empty)
MediaServerUpload="https://user:pass@yourserver.com/upload" MediaServerUpload="https://user:pass@yourserver.com/upload"
#OPTIONAL (default empty) #OPTIONAL (default empty)
@ -1707,7 +1770,7 @@ LogFile="/var/log/matterbridge.log"
#This script will receive every incoming message and can be used to modify the Username and the Text of that message. #This script will receive every incoming message and can be used to modify the Username and the Text of that message.
#The script will have the following global variables: #The script will have the following global variables:
#to modify: msgUsername and msgText #to modify: msgUsername and msgText
#to read: msgChannel and msgAccount #to read: msgUserID, msgChannel, msgAccount
# #
#The script is reloaded on every message, so you can modify the script on the fly. #The script is reloaded on every message, so you can modify the script on the fly.
# #
@ -1731,6 +1794,7 @@ InMessage="example.tengo"
#read-only: #read-only:
#inAccount, inProtocol, inChannel, inGateway, inEvent #inAccount, inProtocol, inChannel, inGateway, inEvent
#outAccount, outProtocol, outChannel, outGateway, outEvent #outAccount, outProtocol, outChannel, outGateway, outEvent
#msgUserID
# #
#read-write: #read-write:
#msgText, msgUsername, msgDrop #msgText, msgUsername, msgDrop
@ -1748,7 +1812,7 @@ OutMessage="example.tengo"
#RemoteNickFormat allows you to specify the location of a tengo (https://github.com/d5/tengo/) script. #RemoteNickFormat allows you to specify the location of a tengo (https://github.com/d5/tengo/) script.
#The script will have the following global variables: #The script will have the following global variables:
#to modify: result #to modify: result
#to read: channel, bridge, gateway, protocol, nick #to read: channel, bridge, gateway, protocol, nick, msgUserID
# #
#The result will be set in {TENGO} in the RemoteNickFormat key of every bridge where {TENGO} is specified #The result will be set in {TENGO} in the RemoteNickFormat key of every bridge where {TENGO} is specified
# #
@ -1786,7 +1850,7 @@ enable=true
# account specified above # account specified above
# REQUIRED # REQUIRED
account="irc.freenode" account="irc.libera"
# The channel key in each gateway is mapped to a similar group chat ID on the chat platform # The channel key in each gateway is mapped to a similar group chat ID on the chat platform
# To find the group chat ID for different platforms, refer to the table below # To find the group chat ID for different platforms, refer to the table below
@ -1803,7 +1867,8 @@ enable=true
# ------------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------------------------
# irc | channel | #general | The # symbol is required and should be lowercase! # irc | channel | #general | The # symbol is required and should be lowercase!
# ------------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------------------------
# mattermost | channel | general | This is the channel name as seen in the URL, not the display name # | channel | general | This is the channel name as seen in the URL, not the display name
# mattermost | channel id | ID:oc4wifyuojgw5f3nsuweesmz8w | This is the channel ID (only use if you know what you're doing)
# ------------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------------------------
# matrix | #channel:server | #yourchannel:matrix.org | Encrypted rooms are not supported in matrix # matrix | #channel:server | #yourchannel:matrix.org | Encrypted rooms are not supported in matrix
# ------------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------------------------
@ -1814,20 +1879,23 @@ enable=true
# rocketchat | channel | #channel | # is required for private channels too # rocketchat | channel | #channel | # is required for private channels too
# ------------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------------------------
# slack | channel name | general | Do not include the # symbol # slack | channel name | general | Do not include the # symbol
# | channel id | ID:C123456 | The underlying ID of a channel. This doesn't work with # | channel id | ID:C123456 | The underlying ID of a channel. This doesn't work with webhooks.
# ------------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------------------------
# steam | chatid | example needed | The number in the URL when you click "enter chat room" in the browser # steam | chatid | example needed | The number in the URL when you click "enter chat room" in the browser
# ------------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------------------------
# nctalk | token | xs25tz5y | The token in the URL when you are in a chat. It will be the last part of the URL. # nctalk | token | xs25tz5y | The token in the URL when you are in a chat. It will be the last part of the URL.
# ------------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------------------------
# telegram | chatid | -123456789 | A large negative number. see https://www.linkedin.com/pulse/telegram-bots-beginners-marco-frau # telegram | chatid | -123456789 | A large negative number. see https://www.linkedin.com/pulse/telegram-bots-beginners-marco-frau
# | chatid/topicid | -123456789/12 | A large negative number/number.
# -------------------------------------------------------------------------------------------------------------------------------------
# vk | peerid | 2000000002 | A number that starts form 2000000000. Use --debug and send any message in chat to get PeerID in the logs
# ------------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------------------------
# whatsapp | group JID | 48111222333-123455678999@g.us | A unique group JID. If you specify an empty string, bridge will list all the possibilities # whatsapp | group JID | 48111222333-123455678999@g.us | A unique group JID. If you specify an empty string, bridge will list all the possibilities
# | "Group Name" | "Family Chat" | if you specify a group name, the bridge will find hint the JID to specify. Names can change over time and are not stable. # | "Group Name" | "Family Chat" | if you specify a group name, the bridge will find hint the JID to specify. Names can change over time and are not stable.
# ------------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------------------------
# xmpp | channel | general | The room name # xmpp | channel | general | The room name
# ------------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------------------------
# zulip | stream/topic:topic | general/off-topic:food | Do not use the # when specifying a topic # zulip | stream/topic:topic | general/topic:food | Do not use the # when specifying a topic
# ------------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------------------------
# #
@ -1842,7 +1910,7 @@ enable=true
#[[gateway.out]] specifies the account and channels we will sent messages to. #[[gateway.out]] specifies the account and channels we will sent messages to.
[[gateway.out]] [[gateway.out]]
account="irc.freenode" account="irc.libera"
channel="#testing" channel="#testing"
#OPTIONAL - only used for IRC and XMPP protocols at the moment #OPTIONAL - only used for IRC and XMPP protocols at the moment
@ -1861,18 +1929,25 @@ enable=true
#OPTIONAL - your irc / xmpp channel key #OPTIONAL - your irc / xmpp channel key
key="yourkey" key="yourkey"
# Discord specific gateway options
[[gateway.inout]] [[gateway.inout]]
account="discord.game" account="discord.game"
channel="mygreatgame" channel="mygreatgame"
#OPTIONAL - webhookurl only works for discord (it needs a different URL for each cahnnel)
[gateway.inout.options] [gateway.inout.options]
webhookurl="https://discordapp.com/api/webhooks/123456789123456789/C9WPqExYWONPDZabcdef-def1434FGFjstasJX9pYht73y" # WebhookURL sends messages in the style of "puppets". You must configure a webhook URL for each channel you want to bridge.
# If you have more than one channel and don't wnat to configure each channel manually, see the "AutoWebhooks" option in the gateway config.
# Example: "https://discord.com/api/webhooks/1234/abcd_xyzw"
WebhookURL=""
[[gateway.inout]] [[gateway.inout]]
account="zulip.streamchat" account="zulip.streamchat"
channel="general/topic:mytopic" channel="general/topic:mytopic"
[[gateway.inout]]
account="harmony.chat_harmonyapp_io"
channel="channel id goes here"
#API example #API example
#[[gateway.inout]] #[[gateway.inout]]
#account="api.local" #account="api.local"

View File

@ -1,7 +1,7 @@
#WARNING: as this file contains credentials, be sure to set correct file permissions #WARNING: as this file contains credentials, be sure to set correct file permissions
[irc] [irc]
[irc.freenode] [irc.libera]
Server="irc.freenode.net:6667" Server="irc.libera.chat:6667"
Nick="matterbot" Nick="matterbot"
[mattermost] [mattermost]
@ -17,7 +17,7 @@
name="gateway1" name="gateway1"
enable=true enable=true
[[gateway.inout]] [[gateway.inout]]
account="irc.freenode" account="irc.libera"
channel="#testing" channel="#testing"
[[gateway.inout]] [[gateway.inout]]
@ -29,6 +29,6 @@ enable=true
#name="gateway2" #name="gateway2"
#enable=true #enable=true
#inout = [ #inout = [
# { account="irc.freenode", channel="#testing", options={key="channelkey"}}, # { account="irc.libera", channel="#testing", options={key="channelkey"}},
# { account="mattermost.work", channel="off-topic" }, # { account="mattermost.work", channel="off-topic" },
#] #]

1
matterclient/README.md Normal file
View File

@ -0,0 +1 @@
Find matterclient on https://github.com/matterbridge/matterclient

View File

@ -1,226 +0,0 @@
package matterclient
import (
"errors"
"strings"
"github.com/mattermost/mattermost-server/v5/model"
)
// GetChannels returns all channels we're members off
func (m *MMClient) GetChannels() []*model.Channel {
m.RLock()
defer m.RUnlock()
var channels []*model.Channel
// our primary team channels first
channels = append(channels, m.Team.Channels...)
for _, t := range m.OtherTeams {
if t.Id != m.Team.Id {
channels = append(channels, t.Channels...)
}
}
return channels
}
func (m *MMClient) GetChannelHeader(channelId string) string { //nolint:golint
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
for _, channel := range append(t.Channels, t.MoreChannels...) {
if channel.Id == channelId {
return channel.Header
}
}
}
return ""
}
func getNormalisedName(channel *model.Channel) string {
if channel.Type == model.CHANNEL_GROUP {
// (deprecated in favor of ReplaceAll in go 1.12)
res := strings.Replace(channel.DisplayName, ", ", "-", -1) //nolint: gocritic
res = strings.Replace(res, " ", "_", -1) //nolint: gocritic
return res
}
return channel.Name
}
func (m *MMClient) GetChannelId(name string, teamId string) string { //nolint:golint
m.RLock()
defer m.RUnlock()
if teamId != "" {
return m.getChannelIdTeam(name, teamId)
}
for _, t := range m.OtherTeams {
for _, channel := range append(t.Channels, t.MoreChannels...) {
if getNormalisedName(channel) == name {
return channel.Id
}
}
}
return ""
}
func (m *MMClient) getChannelIdTeam(name string, teamId string) string { //nolint:golint
for _, t := range m.OtherTeams {
if t.Id == teamId {
for _, channel := range append(t.Channels, t.MoreChannels...) {
if getNormalisedName(channel) == name {
return channel.Id
}
}
}
}
return ""
}
func (m *MMClient) GetChannelName(channelId string) string { //nolint:golint
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
if t == nil {
continue
}
for _, channel := range append(t.Channels, t.MoreChannels...) {
if channel.Id == channelId {
return getNormalisedName(channel)
}
}
}
return ""
}
func (m *MMClient) GetChannelTeamId(id string) string { //nolint:golint
m.RLock()
defer m.RUnlock()
for _, t := range append(m.OtherTeams, m.Team) {
for _, channel := range append(t.Channels, t.MoreChannels...) {
if channel.Id == id {
return channel.TeamId
}
}
}
return ""
}
func (m *MMClient) GetLastViewedAt(channelId string) int64 { //nolint:golint
m.RLock()
defer m.RUnlock()
res, resp := m.Client.GetChannelMember(channelId, m.User.Id, "")
if resp.Error != nil {
return model.GetMillis()
}
return res.LastViewedAt
}
// GetMoreChannels returns existing channels where we're not a member off.
func (m *MMClient) GetMoreChannels() []*model.Channel {
m.RLock()
defer m.RUnlock()
var channels []*model.Channel
for _, t := range m.OtherTeams {
channels = append(channels, t.MoreChannels...)
}
return channels
}
// GetTeamFromChannel returns teamId belonging to channel (DM channels have no teamId).
func (m *MMClient) GetTeamFromChannel(channelId string) string { //nolint:golint
m.RLock()
defer m.RUnlock()
var channels []*model.Channel
for _, t := range m.OtherTeams {
channels = append(channels, t.Channels...)
if t.MoreChannels != nil {
channels = append(channels, t.MoreChannels...)
}
for _, c := range channels {
if c.Id == channelId {
if c.Type == model.CHANNEL_GROUP {
return "G"
}
return t.Id
}
}
channels = nil
}
return ""
}
func (m *MMClient) JoinChannel(channelId string) error { //nolint:golint
m.RLock()
defer m.RUnlock()
for _, c := range m.Team.Channels {
if c.Id == channelId {
m.logger.Debug("Not joining ", channelId, " already joined.")
return nil
}
}
m.logger.Debug("Joining ", channelId)
_, resp := m.Client.AddChannelMember(channelId, m.User.Id)
if resp.Error != nil {
return resp.Error
}
return nil
}
func (m *MMClient) UpdateChannelsTeam(teamID string) error {
mmchannels, resp := m.Client.GetChannelsForTeamForUser(teamID, m.User.Id, false, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
for idx, t := range m.OtherTeams {
if t.Id == teamID {
m.Lock()
m.OtherTeams[idx].Channels = mmchannels
m.Unlock()
}
}
mmchannels, resp = m.Client.GetPublicChannelsForTeam(teamID, 0, 5000, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
for idx, t := range m.OtherTeams {
if t.Id == teamID {
m.Lock()
m.OtherTeams[idx].MoreChannels = mmchannels
m.Unlock()
}
}
return nil
}
func (m *MMClient) UpdateChannels() error {
if err := m.UpdateChannelsTeam(m.Team.Id); err != nil {
return err
}
for _, t := range m.OtherTeams {
if err := m.UpdateChannelsTeam(t.Id); err != nil {
return err
}
}
return nil
}
func (m *MMClient) UpdateChannelHeader(channelId string, header string) { //nolint:golint
channel := &model.Channel{Id: channelId, Header: header}
m.logger.Debugf("updating channelheader %#v, %#v", channelId, header)
_, resp := m.Client.UpdateChannel(channel)
if resp.Error != nil {
m.logger.Error(resp.Error)
}
}
func (m *MMClient) UpdateLastViewed(channelId string) error { //nolint:golint
m.logger.Debugf("posting lastview %#v", channelId)
view := &model.ChannelView{ChannelId: channelId}
_, resp := m.Client.ViewChannel(m.User.Id, view)
if resp.Error != nil {
m.logger.Errorf("ChannelView update for %s failed: %s", channelId, resp.Error)
return resp.Error
}
return nil
}

View File

@ -1,297 +0,0 @@
package matterclient
import (
"crypto/md5" //nolint:gosec
"crypto/tls"
"errors"
"fmt"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"time"
"github.com/gorilla/websocket"
"github.com/jpillora/backoff"
"github.com/mattermost/mattermost-server/v5/model"
)
func (m *MMClient) doLogin(firstConnection bool, b *backoff.Backoff) error {
var resp *model.Response
var appErr *model.AppError
var logmsg = "trying login"
var err error
for {
m.logger.Debugf("%s %s %s %s", logmsg, m.Credentials.Team, m.Credentials.Login, m.Credentials.Server)
if m.Credentials.Token != "" {
resp, err = m.doLoginToken()
if err != nil {
return err
}
} else {
m.User, resp = m.Client.Login(m.Credentials.Login, m.Credentials.Pass)
}
appErr = resp.Error
if appErr != nil {
d := b.Duration()
m.logger.Debug(appErr.DetailedError)
if firstConnection {
if appErr.Message == "" {
return errors.New(appErr.DetailedError)
}
return errors.New(appErr.Message)
}
m.logger.Debugf("LOGIN: %s, reconnecting in %s", appErr, d)
time.Sleep(d)
logmsg = "retrying login"
continue
}
break
}
// reset timer
b.Reset()
return nil
}
func (m *MMClient) doLoginToken() (*model.Response, error) {
var resp *model.Response
var logmsg = "trying login"
m.Client.AuthType = model.HEADER_BEARER
m.Client.AuthToken = m.Credentials.Token
if m.Credentials.CookieToken {
m.logger.Debugf(logmsg + " with cookie (MMAUTH) token")
m.Client.HttpClient.Jar = m.createCookieJar(m.Credentials.Token)
} else {
m.logger.Debugf(logmsg + " with personal token")
}
m.User, resp = m.Client.GetMe("")
if resp.Error != nil {
return resp, resp.Error
}
if m.User == nil {
m.logger.Errorf("LOGIN TOKEN: %s is invalid", m.Credentials.Pass)
return resp, errors.New("invalid token")
}
return resp, nil
}
func (m *MMClient) handleLoginToken() error {
switch {
case strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN):
token := strings.Split(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN+"=")
if len(token) != 2 {
return errors.New("incorrect MMAUTHTOKEN. valid input is MMAUTHTOKEN=yourtoken")
}
m.Credentials.Token = token[1]
m.Credentials.CookieToken = true
case strings.Contains(m.Credentials.Pass, "token="):
token := strings.Split(m.Credentials.Pass, "token=")
if len(token) != 2 {
return errors.New("incorrect personal token. valid input is token=yourtoken")
}
m.Credentials.Token = token[1]
}
return nil
}
func (m *MMClient) initClient(firstConnection bool, b *backoff.Backoff) error {
uriScheme := "https://"
if m.NoTLS {
uriScheme = "http://"
}
// login to mattermost
m.Client = model.NewAPIv4Client(uriScheme + m.Credentials.Server)
m.Client.HttpClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, //nolint:gosec
Proxy: http.ProxyFromEnvironment,
}
m.Client.HttpClient.Timeout = time.Second * 10
// handle MMAUTHTOKEN and personal token
if err := m.handleLoginToken(); err != nil {
return err
}
// check if server alive, retry until
if err := m.serverAlive(firstConnection, b); err != nil {
return err
}
return nil
}
// initialize user and teams
func (m *MMClient) initUser() error {
m.Lock()
defer m.Unlock()
// we only load all team data on initial login.
// all other updates are for channels from our (primary) team only.
//m.logger.Debug("initUser(): loading all team data")
teams, resp := m.Client.GetTeamsForUser(m.User.Id, "")
if resp.Error != nil {
return resp.Error
}
for _, team := range teams {
idx := 0
max := 200
usermap := make(map[string]*model.User)
mmusers, resp := m.Client.GetUsersInTeam(team.Id, idx, max, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
for len(mmusers) > 0 {
for _, user := range mmusers {
usermap[user.Id] = user
}
mmusers, resp = m.Client.GetUsersInTeam(team.Id, idx, max, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
idx++
time.Sleep(time.Millisecond * 200)
}
m.logger.Infof("found %d users in team %s", len(usermap), team.Name)
t := &Team{Team: team, Users: usermap, Id: team.Id}
mmchannels, resp := m.Client.GetChannelsForTeamForUser(team.Id, m.User.Id, false, "")
if resp.Error != nil {
return resp.Error
}
t.Channels = mmchannels
mmchannels, resp = m.Client.GetPublicChannelsForTeam(team.Id, 0, 5000, "")
if resp.Error != nil {
return resp.Error
}
t.MoreChannels = mmchannels
m.OtherTeams = append(m.OtherTeams, t)
if team.Name == m.Credentials.Team {
m.Team = t
m.logger.Debugf("initUser(): found our team %s (id: %s)", team.Name, team.Id)
}
// add all users
for k, v := range t.Users {
m.Users[k] = v
}
}
return nil
}
func (m *MMClient) serverAlive(firstConnection bool, b *backoff.Backoff) error {
defer b.Reset()
for {
d := b.Duration()
// bogus call to get the serverversion
_, resp := m.Client.Logout()
if resp.Error != nil {
return fmt.Errorf("%#v", resp.Error.Error())
}
if firstConnection && !m.SkipVersionCheck && !supportedVersion(resp.ServerVersion) {
return fmt.Errorf("unsupported mattermost version: %s", resp.ServerVersion)
}
if !m.SkipVersionCheck {
m.ServerVersion = resp.ServerVersion
if m.ServerVersion == "" {
m.logger.Debugf("Server not up yet, reconnecting in %s", d)
time.Sleep(d)
} else {
m.logger.Infof("Found version %s", m.ServerVersion)
return nil
}
} else {
return nil
}
}
}
func (m *MMClient) wsConnect() {
b := &backoff.Backoff{
Min: time.Second,
Max: 5 * time.Minute,
Jitter: true,
}
m.WsConnected = false
wsScheme := "wss://"
if m.NoTLS {
wsScheme = "ws://"
}
// setup websocket connection
wsurl := wsScheme + m.Credentials.Server + model.API_URL_SUFFIX_V4 + "/websocket"
header := http.Header{}
header.Set(model.HEADER_AUTH, "BEARER "+m.Client.AuthToken)
m.logger.Debugf("WsClient: making connection: %s", wsurl)
for {
wsDialer := &websocket.Dialer{
TLSClientConfig: &tls.Config{InsecureSkipVerify: m.SkipTLSVerify}, //nolint:gosec
Proxy: http.ProxyFromEnvironment,
}
var err error
m.WsClient, _, err = wsDialer.Dial(wsurl, header)
if err != nil {
d := b.Duration()
m.logger.Debugf("WSS: %s, reconnecting in %s", err, d)
time.Sleep(d)
continue
}
break
}
m.logger.Debug("WsClient: connected")
m.WsSequence = 1
m.WsPingChan = make(chan *model.WebSocketResponse)
// only start to parse WS messages when login is completely done
m.WsConnected = true
}
func (m *MMClient) createCookieJar(token string) *cookiejar.Jar {
var cookies []*http.Cookie
jar, _ := cookiejar.New(nil)
firstCookie := &http.Cookie{
Name: "MMAUTHTOKEN",
Value: token,
Path: "/",
Domain: m.Credentials.Server,
}
cookies = append(cookies, firstCookie)
cookieURL, _ := url.Parse("https://" + m.Credentials.Server)
jar.SetCookies(cookieURL, cookies)
return jar
}
func (m *MMClient) checkAlive() error {
// check if session still is valid
_, resp := m.Client.GetMe("")
if resp.Error != nil {
return resp.Error
}
m.logger.Debug("WS PING")
return m.sendWSRequest("ping", nil)
}
func (m *MMClient) sendWSRequest(action string, data map[string]interface{}) error {
req := &model.WebSocketRequest{}
req.Seq = m.WsSequence
req.Action = action
req.Data = data
m.WsSequence++
m.logger.Debugf("sendWsRequest %#v", req)
return m.WsClient.WriteJSON(req)
}
func supportedVersion(version string) bool {
if strings.HasPrefix(version, "3.8.0") ||
strings.HasPrefix(version, "3.9.0") ||
strings.HasPrefix(version, "3.10.0") ||
strings.HasPrefix(version, "4.") ||
strings.HasPrefix(version, "5.") {
return true
}
return false
}
func digestString(s string) string {
return fmt.Sprintf("%x", md5.Sum([]byte(s))) //nolint:gosec
}

View File

@ -1,294 +0,0 @@
package matterclient
import (
"encoding/json"
"fmt"
"strings"
"sync"
"time"
"github.com/gorilla/websocket"
lru "github.com/hashicorp/golang-lru"
"github.com/jpillora/backoff"
prefixed "github.com/matterbridge/logrus-prefixed-formatter"
"github.com/mattermost/mattermost-server/v5/model"
"github.com/sirupsen/logrus"
)
type Credentials struct {
Login string
Team string
Pass string
Token string
CookieToken bool
Server string
NoTLS bool
SkipTLSVerify bool
SkipVersionCheck bool
}
type Message struct {
Raw *model.WebSocketEvent
Post *model.Post
Team string
Channel string
Username string
Text string
Type string
UserID string
}
//nolint:golint
type Team struct {
Team *model.Team
Id string
Channels []*model.Channel
MoreChannels []*model.Channel
Users map[string]*model.User
}
type MMClient struct {
sync.RWMutex
*Credentials
Team *Team
OtherTeams []*Team
Client *model.Client4
User *model.User
Users map[string]*model.User
MessageChan chan *Message
WsClient *websocket.Conn
WsQuit bool
WsAway bool
WsConnected bool
WsSequence int64
WsPingChan chan *model.WebSocketResponse
ServerVersion string
OnWsConnect func()
logger *logrus.Entry
rootLogger *logrus.Logger
lruCache *lru.Cache
allevents bool
}
// New will instantiate a new Matterclient with the specified login details without connecting.
func New(login string, pass string, team string, server string) *MMClient {
rootLogger := logrus.New()
rootLogger.SetFormatter(&prefixed.TextFormatter{
PrefixPadding: 13,
DisableColors: true,
})
cred := &Credentials{
Login: login,
Pass: pass,
Team: team,
Server: server,
}
cache, _ := lru.New(500)
return &MMClient{
Credentials: cred,
MessageChan: make(chan *Message, 100),
Users: make(map[string]*model.User),
rootLogger: rootLogger,
lruCache: cache,
logger: rootLogger.WithFields(logrus.Fields{"prefix": "matterclient"}),
}
}
// SetDebugLog activates debugging logging on all Matterclient log output.
func (m *MMClient) SetDebugLog() {
m.rootLogger.SetFormatter(&prefixed.TextFormatter{
PrefixPadding: 13,
DisableColors: true,
FullTimestamp: false,
ForceFormatting: true,
})
}
// SetLogLevel tries to parse the specified level and if successful sets
// the log level accordingly. Accepted levels are: 'debug', 'info', 'warn',
// 'error', 'fatal' and 'panic'.
func (m *MMClient) SetLogLevel(level string) {
l, err := logrus.ParseLevel(level)
if err != nil {
m.logger.Warnf("Failed to parse specified log-level '%s': %#v", level, err)
} else {
m.rootLogger.SetLevel(l)
}
}
func (m *MMClient) EnableAllEvents() {
m.allevents = true
}
// Login tries to connect the client with the loging details with which it was initialized.
func (m *MMClient) Login() error {
// check if this is a first connect or a reconnection
firstConnection := true
if m.WsConnected {
firstConnection = false
}
m.WsConnected = false
if m.WsQuit {
return nil
}
b := &backoff.Backoff{
Min: time.Second,
Max: 5 * time.Minute,
Jitter: true,
}
// do initialization setup
if err := m.initClient(firstConnection, b); err != nil {
return err
}
if err := m.doLogin(firstConnection, b); err != nil {
return err
}
if err := m.initUser(); err != nil {
return err
}
if m.Team == nil {
validTeamNames := make([]string, len(m.OtherTeams))
for i, t := range m.OtherTeams {
validTeamNames[i] = t.Team.Name
}
return fmt.Errorf("Team '%s' not found in %v", m.Credentials.Team, validTeamNames)
}
m.wsConnect()
return nil
}
// Logout disconnects the client from the chat server.
func (m *MMClient) Logout() error {
m.logger.Debugf("logout as %s (team: %s) on %s", m.Credentials.Login, m.Credentials.Team, m.Credentials.Server)
m.WsQuit = true
m.WsClient.Close()
m.WsClient.UnderlyingConn().Close()
if strings.Contains(m.Credentials.Pass, model.SESSION_COOKIE_TOKEN) {
m.logger.Debug("Not invalidating session in logout, credential is a token")
return nil
}
_, resp := m.Client.Logout()
if resp.Error != nil {
return resp.Error
}
return nil
}
// WsReceiver implements the core loop that manages the connection to the chat server. In
// case of a disconnect it will try to reconnect. A call to this method is blocking until
// the 'WsQuite' field of the MMClient object is set to 'true'.
func (m *MMClient) WsReceiver() {
for {
var rawMsg json.RawMessage
var err error
if m.WsQuit {
m.logger.Debug("exiting WsReceiver")
return
}
if !m.WsConnected {
time.Sleep(time.Millisecond * 100)
continue
}
if _, rawMsg, err = m.WsClient.ReadMessage(); err != nil {
m.logger.Error("error:", err)
// reconnect
m.wsConnect()
}
var event model.WebSocketEvent
if err := json.Unmarshal(rawMsg, &event); err == nil && event.IsValid() {
m.logger.Debugf("WsReceiver event: %#v", event)
msg := &Message{Raw: &event, Team: m.Credentials.Team}
m.parseMessage(msg)
// check if we didn't empty the message
if msg.Text != "" {
m.MessageChan <- msg
continue
}
// if we have file attached but the message is empty, also send it
if msg.Post != nil {
if msg.Text != "" || len(msg.Post.FileIds) > 0 || msg.Post.Type == "slack_attachment" {
m.MessageChan <- msg
continue
}
}
if m.allevents {
m.MessageChan <- msg
continue
}
switch msg.Raw.Event {
case model.WEBSOCKET_EVENT_USER_ADDED,
model.WEBSOCKET_EVENT_USER_REMOVED,
model.WEBSOCKET_EVENT_CHANNEL_CREATED,
model.WEBSOCKET_EVENT_CHANNEL_DELETED:
m.MessageChan <- msg
continue
}
}
var response model.WebSocketResponse
if err := json.Unmarshal(rawMsg, &response); err == nil && response.IsValid() {
m.logger.Debugf("WsReceiver response: %#v", response)
m.parseResponse(response)
}
}
}
// StatusLoop implements a ping-cycle that ensures that the connection to the chat servers
// remains alive. In case of a disconnect it will try to reconnect. A call to this method
// is blocking until the 'WsQuite' field of the MMClient object is set to 'true'.
func (m *MMClient) StatusLoop() {
retries := 0
backoff := time.Second * 60
if m.OnWsConnect != nil {
m.OnWsConnect()
}
m.logger.Debug("StatusLoop:", m.OnWsConnect != nil)
for {
if m.WsQuit {
return
}
if m.WsConnected {
if err := m.checkAlive(); err != nil {
m.logger.Errorf("Connection is not alive: %#v", err)
}
select {
case <-m.WsPingChan:
m.logger.Debug("WS PONG received")
backoff = time.Second * 60
case <-time.After(time.Second * 5):
if retries > 3 {
m.logger.Debug("StatusLoop() timeout")
m.Logout()
m.WsQuit = false
err := m.Login()
if err != nil {
m.logger.Errorf("Login failed: %#v", err)
break
}
if m.OnWsConnect != nil {
m.OnWsConnect()
}
go m.WsReceiver()
} else {
retries++
backoff = time.Second * 5
}
}
}
time.Sleep(backoff)
}
}

View File

@ -1,207 +0,0 @@
package matterclient
import (
"strings"
"github.com/mattermost/mattermost-server/v5/model"
)
func (m *MMClient) parseActionPost(rmsg *Message) {
// add post to cache, if it already exists don't relay this again.
// this should fix reposts
if ok, _ := m.lruCache.ContainsOrAdd(digestString(rmsg.Raw.Data["post"].(string)), true); ok {
m.logger.Debugf("message %#v in cache, not processing again", rmsg.Raw.Data["post"].(string))
rmsg.Text = ""
return
}
data := model.PostFromJson(strings.NewReader(rmsg.Raw.Data["post"].(string)))
// we don't have the user, refresh the userlist
if m.GetUser(data.UserId) == nil {
m.logger.Infof("User '%v' is not known, ignoring message '%#v'",
data.UserId, data)
return
}
rmsg.Username = m.GetUserName(data.UserId)
rmsg.Channel = m.GetChannelName(data.ChannelId)
rmsg.UserID = data.UserId
rmsg.Type = data.Type
teamid, _ := rmsg.Raw.Data["team_id"].(string)
// edit messsages have no team_id for some reason
if teamid == "" {
// we can find the team_id from the channelid
teamid = m.GetChannelTeamId(data.ChannelId)
rmsg.Raw.Data["team_id"] = teamid
}
if teamid != "" {
rmsg.Team = m.GetTeamName(teamid)
}
// direct message
if rmsg.Raw.Data["channel_type"] == "D" {
rmsg.Channel = m.GetUser(data.UserId).Username
}
rmsg.Text = data.Message
rmsg.Post = data
}
func (m *MMClient) parseMessage(rmsg *Message) {
switch rmsg.Raw.Event {
case model.WEBSOCKET_EVENT_POSTED, model.WEBSOCKET_EVENT_POST_EDITED, model.WEBSOCKET_EVENT_POST_DELETED:
m.parseActionPost(rmsg)
case "user_updated":
user := rmsg.Raw.Data["user"].(map[string]interface{})
if _, ok := user["id"].(string); ok {
m.UpdateUser(user["id"].(string))
}
case "group_added":
if err := m.UpdateChannels(); err != nil {
m.logger.Errorf("failed to update channels: %#v", err)
}
/*
case model.ACTION_USER_REMOVED:
m.handleWsActionUserRemoved(&rmsg)
case model.ACTION_USER_ADDED:
m.handleWsActionUserAdded(&rmsg)
*/
}
}
func (m *MMClient) parseResponse(rmsg model.WebSocketResponse) {
if rmsg.Data != nil {
// ping reply
if rmsg.Data["text"].(string) == "pong" {
m.WsPingChan <- &rmsg
}
}
}
func (m *MMClient) DeleteMessage(postId string) error { //nolint:golint
_, resp := m.Client.DeletePost(postId)
if resp.Error != nil {
return resp.Error
}
return nil
}
func (m *MMClient) EditMessage(postId string, text string) (string, error) { //nolint:golint
post := &model.Post{Message: text, Id: postId}
res, resp := m.Client.UpdatePost(postId, post)
if resp.Error != nil {
return "", resp.Error
}
return res.Id, nil
}
func (m *MMClient) GetFileLinks(filenames []string) []string {
uriScheme := "https://"
if m.NoTLS {
uriScheme = "http://"
}
var output []string
for _, f := range filenames {
res, resp := m.Client.GetFileLink(f)
if resp.Error != nil {
// public links is probably disabled, create the link ourselves
output = append(output, uriScheme+m.Credentials.Server+model.API_URL_SUFFIX_V4+"/files/"+f)
continue
}
output = append(output, res)
}
return output
}
func (m *MMClient) GetPosts(channelId string, limit int) *model.PostList { //nolint:golint
res, resp := m.Client.GetPostsForChannel(channelId, 0, limit, "")
if resp.Error != nil {
return nil
}
return res
}
func (m *MMClient) GetPostsSince(channelId string, time int64) *model.PostList { //nolint:golint
res, resp := m.Client.GetPostsSince(channelId, time)
if resp.Error != nil {
return nil
}
return res
}
func (m *MMClient) GetPublicLink(filename string) string {
res, resp := m.Client.GetFileLink(filename)
if resp.Error != nil {
return ""
}
return res
}
func (m *MMClient) GetPublicLinks(filenames []string) []string {
var output []string
for _, f := range filenames {
res, resp := m.Client.GetFileLink(f)
if resp.Error != nil {
continue
}
output = append(output, res)
}
return output
}
func (m *MMClient) PostMessage(channelId string, text string, rootId string) (string, error) { //nolint:golint
post := &model.Post{ChannelId: channelId, Message: text, RootId: rootId}
res, resp := m.Client.CreatePost(post)
if resp.Error != nil {
return "", resp.Error
}
return res.Id, nil
}
func (m *MMClient) PostMessageWithFiles(channelId string, text string, rootId string, fileIds []string) (string, error) { //nolint:golint
post := &model.Post{ChannelId: channelId, Message: text, RootId: rootId, FileIds: fileIds}
res, resp := m.Client.CreatePost(post)
if resp.Error != nil {
return "", resp.Error
}
return res.Id, nil
}
func (m *MMClient) SearchPosts(query string) *model.PostList {
res, resp := m.Client.SearchPosts(m.Team.Id, query, false)
if resp.Error != nil {
return nil
}
return res
}
// SendDirectMessage sends a direct message to specified user
func (m *MMClient) SendDirectMessage(toUserId string, msg string, rootId string) { //nolint:golint
m.SendDirectMessageProps(toUserId, msg, rootId, nil)
}
func (m *MMClient) SendDirectMessageProps(toUserId string, msg string, rootId string, props map[string]interface{}) { //nolint:golint
m.logger.Debugf("SendDirectMessage to %s, msg %s", toUserId, msg)
// create DM channel (only happens on first message)
_, resp := m.Client.CreateDirectChannel(m.User.Id, toUserId)
if resp.Error != nil {
m.logger.Debugf("SendDirectMessage to %#v failed: %s", toUserId, resp.Error)
return
}
channelName := model.GetDMNameFromIds(toUserId, m.User.Id)
// update our channels
if err := m.UpdateChannels(); err != nil {
m.logger.Errorf("failed to update channels: %#v", err)
}
// build & send the message
msg = strings.Replace(msg, "\r", "", -1)
post := &model.Post{ChannelId: m.GetChannelId(channelName, m.Team.Id), Message: msg, RootId: rootId, Props: props}
m.Client.CreatePost(post)
}
func (m *MMClient) UploadFile(data []byte, channelId string, filename string) (string, error) { //nolint:golint
f, resp := m.Client.UploadFile(data, channelId, filename)
if resp.Error != nil {
return "", resp.Error
}
return f.FileInfos[0].Id, nil
}

View File

@ -1,165 +0,0 @@
package matterclient
import (
"errors"
"time"
"github.com/mattermost/mattermost-server/v5/model"
)
func (m *MMClient) GetNickName(userId string) string { //nolint:golint
user := m.GetUser(userId)
if user != nil {
return user.Nickname
}
return ""
}
func (m *MMClient) GetStatus(userId string) string { //nolint:golint
res, resp := m.Client.GetUserStatus(userId, "")
if resp.Error != nil {
return ""
}
if res.Status == model.STATUS_AWAY {
return "away"
}
if res.Status == model.STATUS_ONLINE {
return "online"
}
return "offline"
}
func (m *MMClient) GetStatuses() map[string]string {
var ids []string
statuses := make(map[string]string)
for id := range m.Users {
ids = append(ids, id)
}
res, resp := m.Client.GetUsersStatusesByIds(ids)
if resp.Error != nil {
return statuses
}
for _, status := range res {
statuses[status.UserId] = "offline"
if status.Status == model.STATUS_AWAY {
statuses[status.UserId] = "away"
}
if status.Status == model.STATUS_ONLINE {
statuses[status.UserId] = "online"
}
}
return statuses
}
func (m *MMClient) GetTeamId() string { //nolint:golint
return m.Team.Id
}
// GetTeamName returns the name of the specified teamId
func (m *MMClient) GetTeamName(teamId string) string { //nolint:golint
m.RLock()
defer m.RUnlock()
for _, t := range m.OtherTeams {
if t.Id == teamId {
return t.Team.Name
}
}
return ""
}
func (m *MMClient) GetUser(userId string) *model.User { //nolint:golint
m.Lock()
defer m.Unlock()
_, ok := m.Users[userId]
if !ok {
res, resp := m.Client.GetUser(userId, "")
if resp.Error != nil {
return nil
}
m.Users[userId] = res
}
return m.Users[userId]
}
func (m *MMClient) GetUserName(userId string) string { //nolint:golint
user := m.GetUser(userId)
if user != nil {
return user.Username
}
return ""
}
func (m *MMClient) GetUsers() map[string]*model.User {
users := make(map[string]*model.User)
m.RLock()
defer m.RUnlock()
for k, v := range m.Users {
users[k] = v
}
return users
}
func (m *MMClient) UpdateUsers() error {
idx := 0
max := 200
mmusers, resp := m.Client.GetUsers(idx, max, "")
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
for len(mmusers) > 0 {
m.Lock()
for _, user := range mmusers {
m.Users[user.Id] = user
}
m.Unlock()
mmusers, resp = m.Client.GetUsers(idx, max, "")
time.Sleep(time.Millisecond * 300)
if resp.Error != nil {
return errors.New(resp.Error.DetailedError)
}
idx++
}
return nil
}
func (m *MMClient) UpdateUserNick(nick string) error {
user := m.User
user.Nickname = nick
_, resp := m.Client.UpdateUser(user)
if resp.Error != nil {
return resp.Error
}
return nil
}
func (m *MMClient) UsernamesInChannel(channelId string) []string { //nolint:golint
res, resp := m.Client.GetChannelMembers(channelId, 0, 50000, "")
if resp.Error != nil {
m.logger.Errorf("UsernamesInChannel(%s) failed: %s", channelId, resp.Error)
return []string{}
}
allusers := m.GetUsers()
result := []string{}
for _, member := range *res {
result = append(result, allusers[member.UserId].Nickname)
}
return result
}
func (m *MMClient) UpdateStatus(userId string, status string) error { //nolint:golint
_, resp := m.Client.UpdateUserStatus(userId, &model.Status{Status: status})
if resp.Error != nil {
return resp.Error
}
return nil
}
func (m *MMClient) UpdateUser(userId string) { //nolint:golint
m.Lock()
defer m.Unlock()
res, resp := m.Client.GetUser(userId, "")
if resp.Error != nil {
return
}
m.Users[userId] = res
}

View File

@ -1,21 +1,18 @@
FROM alpine AS builder FROM alpine AS builder
COPY . /go/src/github.com/42wim/matterbridge COPY . /go/src/matterbridge
RUN apk add \ RUN apk add \
go \ go \
git \ git \
gcc \ && cd /go/src/matterbridge \
musl-dev \ && 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
&& 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
FROM alpine FROM alpine
RUN apk --no-cache add \ RUN apk --no-cache add \
ca-certificates \ ca-certificates \
cairo \ cairo \
libjpeg-turbo \ libjpeg-turbo \
libwebp-dev \
mailcap \ mailcap \
py3-webencodings \ py3-webencodings \
python3 \ python3 \

27
vendor/filippo.io/edwards25519/LICENSE generated vendored Normal file
View File

@ -0,0 +1,27 @@
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

14
vendor/filippo.io/edwards25519/README.md generated vendored Normal file
View File

@ -0,0 +1,14 @@
# filippo.io/edwards25519
```
import "filippo.io/edwards25519"
```
This library implements the edwards25519 elliptic curve, exposing the necessary APIs to build a wide array of higher-level primitives.
Read the docs at [pkg.go.dev/filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519).
The code is originally derived from Adam Langley's internal implementation in the Go standard library, and includes George Tankersley's [performance improvements](https://golang.org/cl/71950). It was then further developed by Henry de Valence for use in ristretto255, and was finally [merged back into the Go standard library](https://golang.org/cl/276272) as of Go 1.17. It now tracks the upstream codebase and extends it with additional functionality.
Most users don't need this package, and should instead use `crypto/ed25519` for signatures, `golang.org/x/crypto/curve25519` for Diffie-Hellman, or `github.com/gtank/ristretto255` for prime order group logic. However, for anyone currently using a fork of `crypto/internal/edwards25519`/`crypto/ed25519/internal/edwards25519` or `github.com/agl/edwards25519`, this package should be a safer, faster, and more powerful alternative.
Since this package is meant to curb proliferation of edwards25519 implementations in the Go ecosystem, it welcomes requests for new APIs or reviewable performance improvements.

20
vendor/filippo.io/edwards25519/doc.go generated vendored Normal file
View File

@ -0,0 +1,20 @@
// Copyright (c) 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package edwards25519 implements group logic for the twisted Edwards curve
//
// -x^2 + y^2 = 1 + -(121665/121666)*x^2*y^2
//
// This is better known as the Edwards curve equivalent to Curve25519, and is
// the curve used by the Ed25519 signature scheme.
//
// Most users don't need this package, and should instead use crypto/ed25519 for
// signatures, golang.org/x/crypto/curve25519 for Diffie-Hellman, or
// github.com/gtank/ristretto255 for prime order group logic.
//
// However, developers who do need to interact with low-level edwards25519
// operations can use this package, which is an extended version of
// crypto/internal/edwards25519 from the standard library repackaged as
// an importable module.
package edwards25519

427
vendor/filippo.io/edwards25519/edwards25519.go generated vendored Normal file
View File

@ -0,0 +1,427 @@
// Copyright (c) 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package edwards25519
import (
"errors"
"filippo.io/edwards25519/field"
)
// Point types.
type projP1xP1 struct {
X, Y, Z, T field.Element
}
type projP2 struct {
X, Y, Z field.Element
}
// Point represents a point on the edwards25519 curve.
//
// This type works similarly to math/big.Int, and all arguments and receivers
// are allowed to alias.
//
// The zero value is NOT valid, and it may be used only as a receiver.
type Point struct {
// Make the type not comparable (i.e. used with == or as a map key), as
// equivalent points can be represented by different Go values.
_ incomparable
// The point is internally represented in extended coordinates (X, Y, Z, T)
// where x = X/Z, y = Y/Z, and xy = T/Z per https://eprint.iacr.org/2008/522.
x, y, z, t field.Element
}
type incomparable [0]func()
func checkInitialized(points ...*Point) {
for _, p := range points {
if p.x == (field.Element{}) && p.y == (field.Element{}) {
panic("edwards25519: use of uninitialized Point")
}
}
}
type projCached struct {
YplusX, YminusX, Z, T2d field.Element
}
type affineCached struct {
YplusX, YminusX, T2d field.Element
}
// Constructors.
func (v *projP2) Zero() *projP2 {
v.X.Zero()
v.Y.One()
v.Z.One()
return v
}
// identity is the point at infinity.
var identity, _ = new(Point).SetBytes([]byte{
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0})
// NewIdentityPoint returns a new Point set to the identity.
func NewIdentityPoint() *Point {
return new(Point).Set(identity)
}
// generator is the canonical curve basepoint. See TestGenerator for the
// correspondence of this encoding with the values in RFC 8032.
var generator, _ = new(Point).SetBytes([]byte{
0x58, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66,
0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66})
// NewGeneratorPoint returns a new Point set to the canonical generator.
func NewGeneratorPoint() *Point {
return new(Point).Set(generator)
}
func (v *projCached) Zero() *projCached {
v.YplusX.One()
v.YminusX.One()
v.Z.One()
v.T2d.Zero()
return v
}
func (v *affineCached) Zero() *affineCached {
v.YplusX.One()
v.YminusX.One()
v.T2d.Zero()
return v
}
// Assignments.
// Set sets v = u, and returns v.
func (v *Point) Set(u *Point) *Point {
*v = *u
return v
}
// Encoding.
// Bytes returns the canonical 32-byte encoding of v, according to RFC 8032,
// Section 5.1.2.
func (v *Point) Bytes() []byte {
// This function is outlined to make the allocations inline in the caller
// rather than happen on the heap.
var buf [32]byte
return v.bytes(&buf)
}
func (v *Point) bytes(buf *[32]byte) []byte {
checkInitialized(v)
var zInv, x, y field.Element
zInv.Invert(&v.z) // zInv = 1 / Z
x.Multiply(&v.x, &zInv) // x = X / Z
y.Multiply(&v.y, &zInv) // y = Y / Z
out := copyFieldElement(buf, &y)
out[31] |= byte(x.IsNegative() << 7)
return out
}
var feOne = new(field.Element).One()
// SetBytes sets v = x, where x is a 32-byte encoding of v. If x does not
// represent a valid point on the curve, SetBytes returns nil and an error and
// the receiver is unchanged. Otherwise, SetBytes returns v.
//
// Note that SetBytes accepts all non-canonical encodings of valid points.
// That is, it follows decoding rules that match most implementations in
// the ecosystem rather than RFC 8032.
func (v *Point) SetBytes(x []byte) (*Point, error) {
// Specifically, the non-canonical encodings that are accepted are
// 1) the ones where the field element is not reduced (see the
// (*field.Element).SetBytes docs) and
// 2) the ones where the x-coordinate is zero and the sign bit is set.
//
// Read more at https://hdevalence.ca/blog/2020-10-04-its-25519am,
// specifically the "Canonical A, R" section.
y, err := new(field.Element).SetBytes(x)
if err != nil {
return nil, errors.New("edwards25519: invalid point encoding length")
}
// -x² + y² = 1 + dx²y²
// x² + dx²y² = x²(dy² + 1) = y² - 1
// x² = (y² - 1) / (dy² + 1)
// u = y² - 1
y2 := new(field.Element).Square(y)
u := new(field.Element).Subtract(y2, feOne)
// v = dy² + 1
vv := new(field.Element).Multiply(y2, d)
vv = vv.Add(vv, feOne)
// x = +√(u/v)
xx, wasSquare := new(field.Element).SqrtRatio(u, vv)
if wasSquare == 0 {
return nil, errors.New("edwards25519: invalid point encoding")
}
// Select the negative square root if the sign bit is set.
xxNeg := new(field.Element).Negate(xx)
xx = xx.Select(xxNeg, xx, int(x[31]>>7))
v.x.Set(xx)
v.y.Set(y)
v.z.One()
v.t.Multiply(xx, y) // xy = T / Z
return v, nil
}
func copyFieldElement(buf *[32]byte, v *field.Element) []byte {
copy(buf[:], v.Bytes())
return buf[:]
}
// Conversions.
func (v *projP2) FromP1xP1(p *projP1xP1) *projP2 {
v.X.Multiply(&p.X, &p.T)
v.Y.Multiply(&p.Y, &p.Z)
v.Z.Multiply(&p.Z, &p.T)
return v
}
func (v *projP2) FromP3(p *Point) *projP2 {
v.X.Set(&p.x)
v.Y.Set(&p.y)
v.Z.Set(&p.z)
return v
}
func (v *Point) fromP1xP1(p *projP1xP1) *Point {
v.x.Multiply(&p.X, &p.T)
v.y.Multiply(&p.Y, &p.Z)
v.z.Multiply(&p.Z, &p.T)
v.t.Multiply(&p.X, &p.Y)
return v
}
func (v *Point) fromP2(p *projP2) *Point {
v.x.Multiply(&p.X, &p.Z)
v.y.Multiply(&p.Y, &p.Z)
v.z.Square(&p.Z)
v.t.Multiply(&p.X, &p.Y)
return v
}
// d is a constant in the curve equation.
var d, _ = new(field.Element).SetBytes([]byte{
0xa3, 0x78, 0x59, 0x13, 0xca, 0x4d, 0xeb, 0x75,
0xab, 0xd8, 0x41, 0x41, 0x4d, 0x0a, 0x70, 0x00,
0x98, 0xe8, 0x79, 0x77, 0x79, 0x40, 0xc7, 0x8c,
0x73, 0xfe, 0x6f, 0x2b, 0xee, 0x6c, 0x03, 0x52})
var d2 = new(field.Element).Add(d, d)
func (v *projCached) FromP3(p *Point) *projCached {
v.YplusX.Add(&p.y, &p.x)
v.YminusX.Subtract(&p.y, &p.x)
v.Z.Set(&p.z)
v.T2d.Multiply(&p.t, d2)
return v
}
func (v *affineCached) FromP3(p *Point) *affineCached {
v.YplusX.Add(&p.y, &p.x)
v.YminusX.Subtract(&p.y, &p.x)
v.T2d.Multiply(&p.t, d2)
var invZ field.Element
invZ.Invert(&p.z)
v.YplusX.Multiply(&v.YplusX, &invZ)
v.YminusX.Multiply(&v.YminusX, &invZ)
v.T2d.Multiply(&v.T2d, &invZ)
return v
}
// (Re)addition and subtraction.
// Add sets v = p + q, and returns v.
func (v *Point) Add(p, q *Point) *Point {
checkInitialized(p, q)
qCached := new(projCached).FromP3(q)
result := new(projP1xP1).Add(p, qCached)
return v.fromP1xP1(result)
}
// Subtract sets v = p - q, and returns v.
func (v *Point) Subtract(p, q *Point) *Point {
checkInitialized(p, q)
qCached := new(projCached).FromP3(q)
result := new(projP1xP1).Sub(p, qCached)
return v.fromP1xP1(result)
}
func (v *projP1xP1) Add(p *Point, q *projCached) *projP1xP1 {
var YplusX, YminusX, PP, MM, TT2d, ZZ2 field.Element
YplusX.Add(&p.y, &p.x)
YminusX.Subtract(&p.y, &p.x)
PP.Multiply(&YplusX, &q.YplusX)
MM.Multiply(&YminusX, &q.YminusX)
TT2d.Multiply(&p.t, &q.T2d)
ZZ2.Multiply(&p.z, &q.Z)
ZZ2.Add(&ZZ2, &ZZ2)
v.X.Subtract(&PP, &MM)
v.Y.Add(&PP, &MM)
v.Z.Add(&ZZ2, &TT2d)
v.T.Subtract(&ZZ2, &TT2d)
return v
}
func (v *projP1xP1) Sub(p *Point, q *projCached) *projP1xP1 {
var YplusX, YminusX, PP, MM, TT2d, ZZ2 field.Element
YplusX.Add(&p.y, &p.x)
YminusX.Subtract(&p.y, &p.x)
PP.Multiply(&YplusX, &q.YminusX) // flipped sign
MM.Multiply(&YminusX, &q.YplusX) // flipped sign
TT2d.Multiply(&p.t, &q.T2d)
ZZ2.Multiply(&p.z, &q.Z)
ZZ2.Add(&ZZ2, &ZZ2)
v.X.Subtract(&PP, &MM)
v.Y.Add(&PP, &MM)
v.Z.Subtract(&ZZ2, &TT2d) // flipped sign
v.T.Add(&ZZ2, &TT2d) // flipped sign
return v
}
func (v *projP1xP1) AddAffine(p *Point, q *affineCached) *projP1xP1 {
var YplusX, YminusX, PP, MM, TT2d, Z2 field.Element
YplusX.Add(&p.y, &p.x)
YminusX.Subtract(&p.y, &p.x)
PP.Multiply(&YplusX, &q.YplusX)
MM.Multiply(&YminusX, &q.YminusX)
TT2d.Multiply(&p.t, &q.T2d)
Z2.Add(&p.z, &p.z)
v.X.Subtract(&PP, &MM)
v.Y.Add(&PP, &MM)
v.Z.Add(&Z2, &TT2d)
v.T.Subtract(&Z2, &TT2d)
return v
}
func (v *projP1xP1) SubAffine(p *Point, q *affineCached) *projP1xP1 {
var YplusX, YminusX, PP, MM, TT2d, Z2 field.Element
YplusX.Add(&p.y, &p.x)
YminusX.Subtract(&p.y, &p.x)
PP.Multiply(&YplusX, &q.YminusX) // flipped sign
MM.Multiply(&YminusX, &q.YplusX) // flipped sign
TT2d.Multiply(&p.t, &q.T2d)
Z2.Add(&p.z, &p.z)
v.X.Subtract(&PP, &MM)
v.Y.Add(&PP, &MM)
v.Z.Subtract(&Z2, &TT2d) // flipped sign
v.T.Add(&Z2, &TT2d) // flipped sign
return v
}
// Doubling.
func (v *projP1xP1) Double(p *projP2) *projP1xP1 {
var XX, YY, ZZ2, XplusYsq field.Element
XX.Square(&p.X)
YY.Square(&p.Y)
ZZ2.Square(&p.Z)
ZZ2.Add(&ZZ2, &ZZ2)
XplusYsq.Add(&p.X, &p.Y)
XplusYsq.Square(&XplusYsq)
v.Y.Add(&YY, &XX)
v.Z.Subtract(&YY, &XX)
v.X.Subtract(&XplusYsq, &v.Y)
v.T.Subtract(&ZZ2, &v.Z)
return v
}
// Negation.
// Negate sets v = -p, and returns v.
func (v *Point) Negate(p *Point) *Point {
checkInitialized(p)
v.x.Negate(&p.x)
v.y.Set(&p.y)
v.z.Set(&p.z)
v.t.Negate(&p.t)
return v
}
// Equal returns 1 if v is equivalent to u, and 0 otherwise.
func (v *Point) Equal(u *Point) int {
checkInitialized(v, u)
var t1, t2, t3, t4 field.Element
t1.Multiply(&v.x, &u.z)
t2.Multiply(&u.x, &v.z)
t3.Multiply(&v.y, &u.z)
t4.Multiply(&u.y, &v.z)
return t1.Equal(&t2) & t3.Equal(&t4)
}
// Constant-time operations
// Select sets v to a if cond == 1 and to b if cond == 0.
func (v *projCached) Select(a, b *projCached, cond int) *projCached {
v.YplusX.Select(&a.YplusX, &b.YplusX, cond)
v.YminusX.Select(&a.YminusX, &b.YminusX, cond)
v.Z.Select(&a.Z, &b.Z, cond)
v.T2d.Select(&a.T2d, &b.T2d, cond)
return v
}
// Select sets v to a if cond == 1 and to b if cond == 0.
func (v *affineCached) Select(a, b *affineCached, cond int) *affineCached {
v.YplusX.Select(&a.YplusX, &b.YplusX, cond)
v.YminusX.Select(&a.YminusX, &b.YminusX, cond)
v.T2d.Select(&a.T2d, &b.T2d, cond)
return v
}
// CondNeg negates v if cond == 1 and leaves it unchanged if cond == 0.
func (v *projCached) CondNeg(cond int) *projCached {
v.YplusX.Swap(&v.YminusX, cond)
v.T2d.Select(new(field.Element).Negate(&v.T2d), &v.T2d, cond)
return v
}
// CondNeg negates v if cond == 1 and leaves it unchanged if cond == 0.
func (v *affineCached) CondNeg(cond int) *affineCached {
v.YplusX.Swap(&v.YminusX, cond)
v.T2d.Select(new(field.Element).Negate(&v.T2d), &v.T2d, cond)
return v
}

349
vendor/filippo.io/edwards25519/extra.go generated vendored Normal file
View File

@ -0,0 +1,349 @@
// Copyright (c) 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package edwards25519
// This file contains additional functionality that is not included in the
// upstream crypto/internal/edwards25519 package.
import (
"errors"
"filippo.io/edwards25519/field"
)
// ExtendedCoordinates returns v in extended coordinates (X:Y:Z:T) where
// x = X/Z, y = Y/Z, and xy = T/Z as in https://eprint.iacr.org/2008/522.
func (v *Point) ExtendedCoordinates() (X, Y, Z, T *field.Element) {
// This function is outlined to make the allocations inline in the caller
// rather than happen on the heap. Don't change the style without making
// sure it doesn't increase the inliner cost.
var e [4]field.Element
X, Y, Z, T = v.extendedCoordinates(&e)
return
}
func (v *Point) extendedCoordinates(e *[4]field.Element) (X, Y, Z, T *field.Element) {
checkInitialized(v)
X = e[0].Set(&v.x)
Y = e[1].Set(&v.y)
Z = e[2].Set(&v.z)
T = e[3].Set(&v.t)
return
}
// SetExtendedCoordinates sets v = (X:Y:Z:T) in extended coordinates where
// x = X/Z, y = Y/Z, and xy = T/Z as in https://eprint.iacr.org/2008/522.
//
// If the coordinates are invalid or don't represent a valid point on the curve,
// SetExtendedCoordinates returns nil and an error and the receiver is
// unchanged. Otherwise, SetExtendedCoordinates returns v.
func (v *Point) SetExtendedCoordinates(X, Y, Z, T *field.Element) (*Point, error) {
if !isOnCurve(X, Y, Z, T) {
return nil, errors.New("edwards25519: invalid point coordinates")
}
v.x.Set(X)
v.y.Set(Y)
v.z.Set(Z)
v.t.Set(T)
return v, nil
}
func isOnCurve(X, Y, Z, T *field.Element) bool {
var lhs, rhs field.Element
XX := new(field.Element).Square(X)
YY := new(field.Element).Square(Y)
ZZ := new(field.Element).Square(Z)
TT := new(field.Element).Square(T)
// -x² + y² = 1 + dx²y²
// -(X/Z)² + (Y/Z)² = 1 + d(T/Z)²
// -X² + Y² = Z² + dT²
lhs.Subtract(YY, XX)
rhs.Multiply(d, TT).Add(&rhs, ZZ)
if lhs.Equal(&rhs) != 1 {
return false
}
// xy = T/Z
// XY/Z² = T/Z
// XY = TZ
lhs.Multiply(X, Y)
rhs.Multiply(T, Z)
return lhs.Equal(&rhs) == 1
}
// BytesMontgomery converts v to a point on the birationally-equivalent
// Curve25519 Montgomery curve, and returns its canonical 32 bytes encoding
// according to RFC 7748.
//
// Note that BytesMontgomery only encodes the u-coordinate, so v and -v encode
// to the same value. If v is the identity point, BytesMontgomery returns 32
// zero bytes, analogously to the X25519 function.
//
// The lack of an inverse operation (such as SetMontgomeryBytes) is deliberate:
// while every valid edwards25519 point has a unique u-coordinate Montgomery
// encoding, X25519 accepts inputs on the quadratic twist, which don't correspond
// to any edwards25519 point, and every other X25519 input corresponds to two
// edwards25519 points.
func (v *Point) BytesMontgomery() []byte {
// This function is outlined to make the allocations inline in the caller
// rather than happen on the heap.
var buf [32]byte
return v.bytesMontgomery(&buf)
}
func (v *Point) bytesMontgomery(buf *[32]byte) []byte {
checkInitialized(v)
// RFC 7748, Section 4.1 provides the bilinear map to calculate the
// Montgomery u-coordinate
//
// u = (1 + y) / (1 - y)
//
// where y = Y / Z.
var y, recip, u field.Element
y.Multiply(&v.y, y.Invert(&v.z)) // y = Y / Z
recip.Invert(recip.Subtract(feOne, &y)) // r = 1/(1 - y)
u.Multiply(u.Add(feOne, &y), &recip) // u = (1 + y)*r
return copyFieldElement(buf, &u)
}
// MultByCofactor sets v = 8 * p, and returns v.
func (v *Point) MultByCofactor(p *Point) *Point {
checkInitialized(p)
result := projP1xP1{}
pp := (&projP2{}).FromP3(p)
result.Double(pp)
pp.FromP1xP1(&result)
result.Double(pp)
pp.FromP1xP1(&result)
result.Double(pp)
return v.fromP1xP1(&result)
}
// Given k > 0, set s = s**(2*i).
func (s *Scalar) pow2k(k int) {
for i := 0; i < k; i++ {
s.Multiply(s, s)
}
}
// Invert sets s to the inverse of a nonzero scalar v, and returns s.
//
// If t is zero, Invert returns zero.
func (s *Scalar) Invert(t *Scalar) *Scalar {
// Uses a hardcoded sliding window of width 4.
var table [8]Scalar
var tt Scalar
tt.Multiply(t, t)
table[0] = *t
for i := 0; i < 7; i++ {
table[i+1].Multiply(&table[i], &tt)
}
// Now table = [t**1, t**3, t**5, t**7, t**9, t**11, t**13, t**15]
// so t**k = t[k/2] for odd k
// To compute the sliding window digits, use the following Sage script:
// sage: import itertools
// sage: def sliding_window(w,k):
// ....: digits = []
// ....: while k > 0:
// ....: if k % 2 == 1:
// ....: kmod = k % (2**w)
// ....: digits.append(kmod)
// ....: k = k - kmod
// ....: else:
// ....: digits.append(0)
// ....: k = k // 2
// ....: return digits
// Now we can compute s roughly as follows:
// sage: s = 1
// sage: for coeff in reversed(sliding_window(4,l-2)):
// ....: s = s*s
// ....: if coeff > 0 :
// ....: s = s*t**coeff
// This works on one bit at a time, with many runs of zeros.
// The digits can be collapsed into [(count, coeff)] as follows:
// sage: [(len(list(group)),d) for d,group in itertools.groupby(sliding_window(4,l-2))]
// Entries of the form (k, 0) turn into pow2k(k)
// Entries of the form (1, coeff) turn into a squaring and then a table lookup.
// We can fold the squaring into the previous pow2k(k) as pow2k(k+1).
*s = table[1/2]
s.pow2k(127 + 1)
s.Multiply(s, &table[1/2])
s.pow2k(4 + 1)
s.Multiply(s, &table[9/2])
s.pow2k(3 + 1)
s.Multiply(s, &table[11/2])
s.pow2k(3 + 1)
s.Multiply(s, &table[13/2])
s.pow2k(3 + 1)
s.Multiply(s, &table[15/2])
s.pow2k(4 + 1)
s.Multiply(s, &table[7/2])
s.pow2k(4 + 1)
s.Multiply(s, &table[15/2])
s.pow2k(3 + 1)
s.Multiply(s, &table[5/2])
s.pow2k(3 + 1)
s.Multiply(s, &table[1/2])
s.pow2k(4 + 1)
s.Multiply(s, &table[15/2])
s.pow2k(4 + 1)
s.Multiply(s, &table[15/2])
s.pow2k(4 + 1)
s.Multiply(s, &table[7/2])
s.pow2k(3 + 1)
s.Multiply(s, &table[3/2])
s.pow2k(4 + 1)
s.Multiply(s, &table[11/2])
s.pow2k(5 + 1)
s.Multiply(s, &table[11/2])
s.pow2k(9 + 1)
s.Multiply(s, &table[9/2])
s.pow2k(3 + 1)
s.Multiply(s, &table[3/2])
s.pow2k(4 + 1)
s.Multiply(s, &table[3/2])
s.pow2k(4 + 1)
s.Multiply(s, &table[3/2])
s.pow2k(4 + 1)
s.Multiply(s, &table[9/2])
s.pow2k(3 + 1)
s.Multiply(s, &table[7/2])
s.pow2k(3 + 1)
s.Multiply(s, &table[3/2])
s.pow2k(3 + 1)
s.Multiply(s, &table[13/2])
s.pow2k(3 + 1)
s.Multiply(s, &table[7/2])
s.pow2k(4 + 1)
s.Multiply(s, &table[9/2])
s.pow2k(3 + 1)
s.Multiply(s, &table[15/2])
s.pow2k(4 + 1)
s.Multiply(s, &table[11/2])
return s
}
// MultiScalarMult sets v = sum(scalars[i] * points[i]), and returns v.
//
// Execution time depends only on the lengths of the two slices, which must match.
func (v *Point) MultiScalarMult(scalars []*Scalar, points []*Point) *Point {
if len(scalars) != len(points) {
panic("edwards25519: called MultiScalarMult with different size inputs")
}
checkInitialized(points...)
// Proceed as in the single-base case, but share doublings
// between each point in the multiscalar equation.
// Build lookup tables for each point
tables := make([]projLookupTable, len(points))
for i := range tables {
tables[i].FromP3(points[i])
}
// Compute signed radix-16 digits for each scalar
digits := make([][64]int8, len(scalars))
for i := range digits {
digits[i] = scalars[i].signedRadix16()
}
// Unwrap first loop iteration to save computing 16*identity
multiple := &projCached{}
tmp1 := &projP1xP1{}
tmp2 := &projP2{}
// Lookup-and-add the appropriate multiple of each input point
for j := range tables {
tables[j].SelectInto(multiple, digits[j][63])
tmp1.Add(v, multiple) // tmp1 = v + x_(j,63)*Q in P1xP1 coords
v.fromP1xP1(tmp1) // update v
}
tmp2.FromP3(v) // set up tmp2 = v in P2 coords for next iteration
for i := 62; i >= 0; i-- {
tmp1.Double(tmp2) // tmp1 = 2*(prev) in P1xP1 coords
tmp2.FromP1xP1(tmp1) // tmp2 = 2*(prev) in P2 coords
tmp1.Double(tmp2) // tmp1 = 4*(prev) in P1xP1 coords
tmp2.FromP1xP1(tmp1) // tmp2 = 4*(prev) in P2 coords
tmp1.Double(tmp2) // tmp1 = 8*(prev) in P1xP1 coords
tmp2.FromP1xP1(tmp1) // tmp2 = 8*(prev) in P2 coords
tmp1.Double(tmp2) // tmp1 = 16*(prev) in P1xP1 coords
v.fromP1xP1(tmp1) // v = 16*(prev) in P3 coords
// Lookup-and-add the appropriate multiple of each input point
for j := range tables {
tables[j].SelectInto(multiple, digits[j][i])
tmp1.Add(v, multiple) // tmp1 = v + x_(j,i)*Q in P1xP1 coords
v.fromP1xP1(tmp1) // update v
}
tmp2.FromP3(v) // set up tmp2 = v in P2 coords for next iteration
}
return v
}
// VarTimeMultiScalarMult sets v = sum(scalars[i] * points[i]), and returns v.
//
// Execution time depends on the inputs.
func (v *Point) VarTimeMultiScalarMult(scalars []*Scalar, points []*Point) *Point {
if len(scalars) != len(points) {
panic("edwards25519: called VarTimeMultiScalarMult with different size inputs")
}
checkInitialized(points...)
// Generalize double-base NAF computation to arbitrary sizes.
// Here all the points are dynamic, so we only use the smaller
// tables.
// Build lookup tables for each point
tables := make([]nafLookupTable5, len(points))
for i := range tables {
tables[i].FromP3(points[i])
}
// Compute a NAF for each scalar
nafs := make([][256]int8, len(scalars))
for i := range nafs {
nafs[i] = scalars[i].nonAdjacentForm(5)
}
multiple := &projCached{}
tmp1 := &projP1xP1{}
tmp2 := &projP2{}
tmp2.Zero()
// Move from high to low bits, doubling the accumulator
// at each iteration and checking whether there is a nonzero
// coefficient to look up a multiple of.
//
// Skip trying to find the first nonzero coefficent, because
// searching might be more work than a few extra doublings.
for i := 255; i >= 0; i-- {
tmp1.Double(tmp2)
for j := range nafs {
if nafs[j][i] > 0 {
v.fromP1xP1(tmp1)
tables[j].SelectInto(multiple, nafs[j][i])
tmp1.Add(v, multiple)
} else if nafs[j][i] < 0 {
v.fromP1xP1(tmp1)
tables[j].SelectInto(multiple, -nafs[j][i])
tmp1.Sub(v, multiple)
}
}
tmp2.FromP1xP1(tmp1)
}
v.fromP2(tmp2)
return v
}

420
vendor/filippo.io/edwards25519/field/fe.go generated vendored Normal file
View File

@ -0,0 +1,420 @@
// Copyright (c) 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package field implements fast arithmetic modulo 2^255-19.
package field
import (
"crypto/subtle"
"encoding/binary"
"errors"
"math/bits"
)
// Element represents an element of the field GF(2^255-19). Note that this
// is not a cryptographically secure group, and should only be used to interact
// with edwards25519.Point coordinates.
//
// This type works similarly to math/big.Int, and all arguments and receivers
// are allowed to alias.
//
// The zero value is a valid zero element.
type Element struct {
// An element t represents the integer
// t.l0 + t.l1*2^51 + t.l2*2^102 + t.l3*2^153 + t.l4*2^204
//
// Between operations, all limbs are expected to be lower than 2^52.
l0 uint64
l1 uint64
l2 uint64
l3 uint64
l4 uint64
}
const maskLow51Bits uint64 = (1 << 51) - 1
var feZero = &Element{0, 0, 0, 0, 0}
// Zero sets v = 0, and returns v.
func (v *Element) Zero() *Element {
*v = *feZero
return v
}
var feOne = &Element{1, 0, 0, 0, 0}
// One sets v = 1, and returns v.
func (v *Element) One() *Element {
*v = *feOne
return v
}
// reduce reduces v modulo 2^255 - 19 and returns it.
func (v *Element) reduce() *Element {
v.carryPropagate()
// After the light reduction we now have a field element representation
// v < 2^255 + 2^13 * 19, but need v < 2^255 - 19.
// If v >= 2^255 - 19, then v + 19 >= 2^255, which would overflow 2^255 - 1,
// generating a carry. That is, c will be 0 if v < 2^255 - 19, and 1 otherwise.
c := (v.l0 + 19) >> 51
c = (v.l1 + c) >> 51
c = (v.l2 + c) >> 51
c = (v.l3 + c) >> 51
c = (v.l4 + c) >> 51
// If v < 2^255 - 19 and c = 0, this will be a no-op. Otherwise, it's
// effectively applying the reduction identity to the carry.
v.l0 += 19 * c
v.l1 += v.l0 >> 51
v.l0 = v.l0 & maskLow51Bits
v.l2 += v.l1 >> 51
v.l1 = v.l1 & maskLow51Bits
v.l3 += v.l2 >> 51
v.l2 = v.l2 & maskLow51Bits
v.l4 += v.l3 >> 51
v.l3 = v.l3 & maskLow51Bits
// no additional carry
v.l4 = v.l4 & maskLow51Bits
return v
}
// Add sets v = a + b, and returns v.
func (v *Element) Add(a, b *Element) *Element {
v.l0 = a.l0 + b.l0
v.l1 = a.l1 + b.l1
v.l2 = a.l2 + b.l2
v.l3 = a.l3 + b.l3
v.l4 = a.l4 + b.l4
// Using the generic implementation here is actually faster than the
// assembly. Probably because the body of this function is so simple that
// the compiler can figure out better optimizations by inlining the carry
// propagation.
return v.carryPropagateGeneric()
}
// Subtract sets v = a - b, and returns v.
func (v *Element) Subtract(a, b *Element) *Element {
// We first add 2 * p, to guarantee the subtraction won't underflow, and
// then subtract b (which can be up to 2^255 + 2^13 * 19).
v.l0 = (a.l0 + 0xFFFFFFFFFFFDA) - b.l0
v.l1 = (a.l1 + 0xFFFFFFFFFFFFE) - b.l1
v.l2 = (a.l2 + 0xFFFFFFFFFFFFE) - b.l2
v.l3 = (a.l3 + 0xFFFFFFFFFFFFE) - b.l3
v.l4 = (a.l4 + 0xFFFFFFFFFFFFE) - b.l4
return v.carryPropagate()
}
// Negate sets v = -a, and returns v.
func (v *Element) Negate(a *Element) *Element {
return v.Subtract(feZero, a)
}
// Invert sets v = 1/z mod p, and returns v.
//
// If z == 0, Invert returns v = 0.
func (v *Element) Invert(z *Element) *Element {
// Inversion is implemented as exponentiation with exponent p 2. It uses the
// same sequence of 255 squarings and 11 multiplications as [Curve25519].
var z2, z9, z11, z2_5_0, z2_10_0, z2_20_0, z2_50_0, z2_100_0, t Element
z2.Square(z) // 2
t.Square(&z2) // 4
t.Square(&t) // 8
z9.Multiply(&t, z) // 9
z11.Multiply(&z9, &z2) // 11
t.Square(&z11) // 22
z2_5_0.Multiply(&t, &z9) // 31 = 2^5 - 2^0
t.Square(&z2_5_0) // 2^6 - 2^1
for i := 0; i < 4; i++ {
t.Square(&t) // 2^10 - 2^5
}
z2_10_0.Multiply(&t, &z2_5_0) // 2^10 - 2^0
t.Square(&z2_10_0) // 2^11 - 2^1
for i := 0; i < 9; i++ {
t.Square(&t) // 2^20 - 2^10
}
z2_20_0.Multiply(&t, &z2_10_0) // 2^20 - 2^0
t.Square(&z2_20_0) // 2^21 - 2^1
for i := 0; i < 19; i++ {
t.Square(&t) // 2^40 - 2^20
}
t.Multiply(&t, &z2_20_0) // 2^40 - 2^0
t.Square(&t) // 2^41 - 2^1
for i := 0; i < 9; i++ {
t.Square(&t) // 2^50 - 2^10
}
z2_50_0.Multiply(&t, &z2_10_0) // 2^50 - 2^0
t.Square(&z2_50_0) // 2^51 - 2^1
for i := 0; i < 49; i++ {
t.Square(&t) // 2^100 - 2^50
}
z2_100_0.Multiply(&t, &z2_50_0) // 2^100 - 2^0
t.Square(&z2_100_0) // 2^101 - 2^1
for i := 0; i < 99; i++ {
t.Square(&t) // 2^200 - 2^100
}
t.Multiply(&t, &z2_100_0) // 2^200 - 2^0
t.Square(&t) // 2^201 - 2^1
for i := 0; i < 49; i++ {
t.Square(&t) // 2^250 - 2^50
}
t.Multiply(&t, &z2_50_0) // 2^250 - 2^0
t.Square(&t) // 2^251 - 2^1
t.Square(&t) // 2^252 - 2^2
t.Square(&t) // 2^253 - 2^3
t.Square(&t) // 2^254 - 2^4
t.Square(&t) // 2^255 - 2^5
return v.Multiply(&t, &z11) // 2^255 - 21
}
// Set sets v = a, and returns v.
func (v *Element) Set(a *Element) *Element {
*v = *a
return v
}
// SetBytes sets v to x, where x is a 32-byte little-endian encoding. If x is
// not of the right length, SetBytes returns nil and an error, and the
// receiver is unchanged.
//
// Consistent with RFC 7748, the most significant bit (the high bit of the
// last byte) is ignored, and non-canonical values (2^255-19 through 2^255-1)
// are accepted. Note that this is laxer than specified by RFC 8032, but
// consistent with most Ed25519 implementations.
func (v *Element) SetBytes(x []byte) (*Element, error) {
if len(x) != 32 {
return nil, errors.New("edwards25519: invalid field element input size")
}
// Bits 0:51 (bytes 0:8, bits 0:64, shift 0, mask 51).
v.l0 = binary.LittleEndian.Uint64(x[0:8])
v.l0 &= maskLow51Bits
// Bits 51:102 (bytes 6:14, bits 48:112, shift 3, mask 51).
v.l1 = binary.LittleEndian.Uint64(x[6:14]) >> 3
v.l1 &= maskLow51Bits
// Bits 102:153 (bytes 12:20, bits 96:160, shift 6, mask 51).
v.l2 = binary.LittleEndian.Uint64(x[12:20]) >> 6
v.l2 &= maskLow51Bits
// Bits 153:204 (bytes 19:27, bits 152:216, shift 1, mask 51).
v.l3 = binary.LittleEndian.Uint64(x[19:27]) >> 1
v.l3 &= maskLow51Bits
// Bits 204:255 (bytes 24:32, bits 192:256, shift 12, mask 51).
// Note: not bytes 25:33, shift 4, to avoid overread.
v.l4 = binary.LittleEndian.Uint64(x[24:32]) >> 12
v.l4 &= maskLow51Bits
return v, nil
}
// Bytes returns the canonical 32-byte little-endian encoding of v.
func (v *Element) Bytes() []byte {
// This function is outlined to make the allocations inline in the caller
// rather than happen on the heap.
var out [32]byte
return v.bytes(&out)
}
func (v *Element) bytes(out *[32]byte) []byte {
t := *v
t.reduce()
var buf [8]byte
for i, l := range [5]uint64{t.l0, t.l1, t.l2, t.l3, t.l4} {
bitsOffset := i * 51
binary.LittleEndian.PutUint64(buf[:], l<<uint(bitsOffset%8))
for i, bb := range buf {
off := bitsOffset/8 + i
if off >= len(out) {
break
}
out[off] |= bb
}
}
return out[:]
}
// Equal returns 1 if v and u are equal, and 0 otherwise.
func (v *Element) Equal(u *Element) int {
sa, sv := u.Bytes(), v.Bytes()
return subtle.ConstantTimeCompare(sa, sv)
}
// mask64Bits returns 0xffffffff if cond is 1, and 0 otherwise.
func mask64Bits(cond int) uint64 { return ^(uint64(cond) - 1) }
// Select sets v to a if cond == 1, and to b if cond == 0.
func (v *Element) Select(a, b *Element, cond int) *Element {
m := mask64Bits(cond)
v.l0 = (m & a.l0) | (^m & b.l0)
v.l1 = (m & a.l1) | (^m & b.l1)
v.l2 = (m & a.l2) | (^m & b.l2)
v.l3 = (m & a.l3) | (^m & b.l3)
v.l4 = (m & a.l4) | (^m & b.l4)
return v
}
// Swap swaps v and u if cond == 1 or leaves them unchanged if cond == 0, and returns v.
func (v *Element) Swap(u *Element, cond int) {
m := mask64Bits(cond)
t := m & (v.l0 ^ u.l0)
v.l0 ^= t
u.l0 ^= t
t = m & (v.l1 ^ u.l1)
v.l1 ^= t
u.l1 ^= t
t = m & (v.l2 ^ u.l2)
v.l2 ^= t
u.l2 ^= t
t = m & (v.l3 ^ u.l3)
v.l3 ^= t
u.l3 ^= t
t = m & (v.l4 ^ u.l4)
v.l4 ^= t
u.l4 ^= t
}
// IsNegative returns 1 if v is negative, and 0 otherwise.
func (v *Element) IsNegative() int {
return int(v.Bytes()[0] & 1)
}
// Absolute sets v to |u|, and returns v.
func (v *Element) Absolute(u *Element) *Element {
return v.Select(new(Element).Negate(u), u, u.IsNegative())
}
// Multiply sets v = x * y, and returns v.
func (v *Element) Multiply(x, y *Element) *Element {
feMul(v, x, y)
return v
}
// Square sets v = x * x, and returns v.
func (v *Element) Square(x *Element) *Element {
feSquare(v, x)
return v
}
// Mult32 sets v = x * y, and returns v.
func (v *Element) Mult32(x *Element, y uint32) *Element {
x0lo, x0hi := mul51(x.l0, y)
x1lo, x1hi := mul51(x.l1, y)
x2lo, x2hi := mul51(x.l2, y)
x3lo, x3hi := mul51(x.l3, y)
x4lo, x4hi := mul51(x.l4, y)
v.l0 = x0lo + 19*x4hi // carried over per the reduction identity
v.l1 = x1lo + x0hi
v.l2 = x2lo + x1hi
v.l3 = x3lo + x2hi
v.l4 = x4lo + x3hi
// The hi portions are going to be only 32 bits, plus any previous excess,
// so we can skip the carry propagation.
return v
}
// mul51 returns lo + hi * 2⁵¹ = a * b.
func mul51(a uint64, b uint32) (lo uint64, hi uint64) {
mh, ml := bits.Mul64(a, uint64(b))
lo = ml & maskLow51Bits
hi = (mh << 13) | (ml >> 51)
return
}
// Pow22523 set v = x^((p-5)/8), and returns v. (p-5)/8 is 2^252-3.
func (v *Element) Pow22523(x *Element) *Element {
var t0, t1, t2 Element
t0.Square(x) // x^2
t1.Square(&t0) // x^4
t1.Square(&t1) // x^8
t1.Multiply(x, &t1) // x^9
t0.Multiply(&t0, &t1) // x^11
t0.Square(&t0) // x^22
t0.Multiply(&t1, &t0) // x^31
t1.Square(&t0) // x^62
for i := 1; i < 5; i++ { // x^992
t1.Square(&t1)
}
t0.Multiply(&t1, &t0) // x^1023 -> 1023 = 2^10 - 1
t1.Square(&t0) // 2^11 - 2
for i := 1; i < 10; i++ { // 2^20 - 2^10
t1.Square(&t1)
}
t1.Multiply(&t1, &t0) // 2^20 - 1
t2.Square(&t1) // 2^21 - 2
for i := 1; i < 20; i++ { // 2^40 - 2^20
t2.Square(&t2)
}
t1.Multiply(&t2, &t1) // 2^40 - 1
t1.Square(&t1) // 2^41 - 2
for i := 1; i < 10; i++ { // 2^50 - 2^10
t1.Square(&t1)
}
t0.Multiply(&t1, &t0) // 2^50 - 1
t1.Square(&t0) // 2^51 - 2
for i := 1; i < 50; i++ { // 2^100 - 2^50
t1.Square(&t1)
}
t1.Multiply(&t1, &t0) // 2^100 - 1
t2.Square(&t1) // 2^101 - 2
for i := 1; i < 100; i++ { // 2^200 - 2^100
t2.Square(&t2)
}
t1.Multiply(&t2, &t1) // 2^200 - 1
t1.Square(&t1) // 2^201 - 2
for i := 1; i < 50; i++ { // 2^250 - 2^50
t1.Square(&t1)
}
t0.Multiply(&t1, &t0) // 2^250 - 1
t0.Square(&t0) // 2^251 - 2
t0.Square(&t0) // 2^252 - 4
return v.Multiply(&t0, x) // 2^252 - 3 -> x^(2^252-3)
}
// sqrtM1 is 2^((p-1)/4), which squared is equal to -1 by Euler's Criterion.
var sqrtM1 = &Element{1718705420411056, 234908883556509,
2233514472574048, 2117202627021982, 765476049583133}
// SqrtRatio sets r to the non-negative square root of the ratio of u and v.
//
// If u/v is square, SqrtRatio returns r and 1. If u/v is not square, SqrtRatio
// sets r according to Section 4.3 of draft-irtf-cfrg-ristretto255-decaf448-00,
// and returns r and 0.
func (r *Element) SqrtRatio(u, v *Element) (R *Element, wasSquare int) {
t0 := new(Element)
// r = (u * v3) * (u * v7)^((p-5)/8)
v2 := new(Element).Square(v)
uv3 := new(Element).Multiply(u, t0.Multiply(v2, v))
uv7 := new(Element).Multiply(uv3, t0.Square(v2))
rr := new(Element).Multiply(uv3, t0.Pow22523(uv7))
check := new(Element).Multiply(v, t0.Square(rr)) // check = v * r^2
uNeg := new(Element).Negate(u)
correctSignSqrt := check.Equal(u)
flippedSignSqrt := check.Equal(uNeg)
flippedSignSqrtI := check.Equal(t0.Multiply(uNeg, sqrtM1))
rPrime := new(Element).Multiply(rr, sqrtM1) // r_prime = SQRT_M1 * r
// r = CT_SELECT(r_prime IF flipped_sign_sqrt | flipped_sign_sqrt_i ELSE r)
rr.Select(rPrime, rr, flippedSignSqrt|flippedSignSqrtI)
r.Absolute(rr) // Choose the nonnegative square root.
return r, correctSignSqrt | flippedSignSqrt
}

16
vendor/filippo.io/edwards25519/field/fe_amd64.go generated vendored Normal file
View File

@ -0,0 +1,16 @@
// Code generated by command: go run fe_amd64_asm.go -out ../fe_amd64.s -stubs ../fe_amd64.go -pkg field. DO NOT EDIT.
//go:build amd64 && gc && !purego
// +build amd64,gc,!purego
package field
// feMul sets out = a * b. It works like feMulGeneric.
//
//go:noescape
func feMul(out *Element, a *Element, b *Element)
// feSquare sets out = a * a. It works like feSquareGeneric.
//
//go:noescape
func feSquare(out *Element, a *Element)

379
vendor/filippo.io/edwards25519/field/fe_amd64.s generated vendored Normal file
View File

@ -0,0 +1,379 @@
// Code generated by command: go run fe_amd64_asm.go -out ../fe_amd64.s -stubs ../fe_amd64.go -pkg field. DO NOT EDIT.
//go:build amd64 && gc && !purego
// +build amd64,gc,!purego
#include "textflag.h"
// func feMul(out *Element, a *Element, b *Element)
TEXT ·feMul(SB), NOSPLIT, $0-24
MOVQ a+8(FP), CX
MOVQ b+16(FP), BX
// r0 = a0×b0
MOVQ (CX), AX
MULQ (BX)
MOVQ AX, DI
MOVQ DX, SI
// r0 += 19×a1×b4
MOVQ 8(CX), AX
IMUL3Q $0x13, AX, AX
MULQ 32(BX)
ADDQ AX, DI
ADCQ DX, SI
// r0 += 19×a2×b3
MOVQ 16(CX), AX
IMUL3Q $0x13, AX, AX
MULQ 24(BX)
ADDQ AX, DI
ADCQ DX, SI
// r0 += 19×a3×b2
MOVQ 24(CX), AX
IMUL3Q $0x13, AX, AX
MULQ 16(BX)
ADDQ AX, DI
ADCQ DX, SI
// r0 += 19×a4×b1
MOVQ 32(CX), AX
IMUL3Q $0x13, AX, AX
MULQ 8(BX)
ADDQ AX, DI
ADCQ DX, SI
// r1 = a0×b1
MOVQ (CX), AX
MULQ 8(BX)
MOVQ AX, R9
MOVQ DX, R8
// r1 += a1×b0
MOVQ 8(CX), AX
MULQ (BX)
ADDQ AX, R9
ADCQ DX, R8
// r1 += 19×a2×b4
MOVQ 16(CX), AX
IMUL3Q $0x13, AX, AX
MULQ 32(BX)
ADDQ AX, R9
ADCQ DX, R8
// r1 += 19×a3×b3
MOVQ 24(CX), AX
IMUL3Q $0x13, AX, AX
MULQ 24(BX)
ADDQ AX, R9
ADCQ DX, R8
// r1 += 19×a4×b2
MOVQ 32(CX), AX
IMUL3Q $0x13, AX, AX
MULQ 16(BX)
ADDQ AX, R9
ADCQ DX, R8
// r2 = a0×b2
MOVQ (CX), AX
MULQ 16(BX)
MOVQ AX, R11
MOVQ DX, R10
// r2 += a1×b1
MOVQ 8(CX), AX
MULQ 8(BX)
ADDQ AX, R11
ADCQ DX, R10
// r2 += a2×b0
MOVQ 16(CX), AX
MULQ (BX)
ADDQ AX, R11
ADCQ DX, R10
// r2 += 19×a3×b4
MOVQ 24(CX), AX
IMUL3Q $0x13, AX, AX
MULQ 32(BX)
ADDQ AX, R11
ADCQ DX, R10
// r2 += 19×a4×b3
MOVQ 32(CX), AX
IMUL3Q $0x13, AX, AX
MULQ 24(BX)
ADDQ AX, R11
ADCQ DX, R10
// r3 = a0×b3
MOVQ (CX), AX
MULQ 24(BX)
MOVQ AX, R13
MOVQ DX, R12
// r3 += a1×b2
MOVQ 8(CX), AX
MULQ 16(BX)
ADDQ AX, R13
ADCQ DX, R12
// r3 += a2×b1
MOVQ 16(CX), AX
MULQ 8(BX)
ADDQ AX, R13
ADCQ DX, R12
// r3 += a3×b0
MOVQ 24(CX), AX
MULQ (BX)
ADDQ AX, R13
ADCQ DX, R12
// r3 += 19×a4×b4
MOVQ 32(CX), AX
IMUL3Q $0x13, AX, AX
MULQ 32(BX)
ADDQ AX, R13
ADCQ DX, R12
// r4 = a0×b4
MOVQ (CX), AX
MULQ 32(BX)
MOVQ AX, R15
MOVQ DX, R14
// r4 += a1×b3
MOVQ 8(CX), AX
MULQ 24(BX)
ADDQ AX, R15
ADCQ DX, R14
// r4 += a2×b2
MOVQ 16(CX), AX
MULQ 16(BX)
ADDQ AX, R15
ADCQ DX, R14
// r4 += a3×b1
MOVQ 24(CX), AX
MULQ 8(BX)
ADDQ AX, R15
ADCQ DX, R14
// r4 += a4×b0
MOVQ 32(CX), AX
MULQ (BX)
ADDQ AX, R15
ADCQ DX, R14
// First reduction chain
MOVQ $0x0007ffffffffffff, AX
SHLQ $0x0d, DI, SI
SHLQ $0x0d, R9, R8
SHLQ $0x0d, R11, R10
SHLQ $0x0d, R13, R12
SHLQ $0x0d, R15, R14
ANDQ AX, DI
IMUL3Q $0x13, R14, R14
ADDQ R14, DI
ANDQ AX, R9
ADDQ SI, R9
ANDQ AX, R11
ADDQ R8, R11
ANDQ AX, R13
ADDQ R10, R13
ANDQ AX, R15
ADDQ R12, R15
// Second reduction chain (carryPropagate)
MOVQ DI, SI
SHRQ $0x33, SI
MOVQ R9, R8
SHRQ $0x33, R8
MOVQ R11, R10
SHRQ $0x33, R10
MOVQ R13, R12
SHRQ $0x33, R12
MOVQ R15, R14
SHRQ $0x33, R14
ANDQ AX, DI
IMUL3Q $0x13, R14, R14
ADDQ R14, DI
ANDQ AX, R9
ADDQ SI, R9
ANDQ AX, R11
ADDQ R8, R11
ANDQ AX, R13
ADDQ R10, R13
ANDQ AX, R15
ADDQ R12, R15
// Store output
MOVQ out+0(FP), AX
MOVQ DI, (AX)
MOVQ R9, 8(AX)
MOVQ R11, 16(AX)
MOVQ R13, 24(AX)
MOVQ R15, 32(AX)
RET
// func feSquare(out *Element, a *Element)
TEXT ·feSquare(SB), NOSPLIT, $0-16
MOVQ a+8(FP), CX
// r0 = l0×l0
MOVQ (CX), AX
MULQ (CX)
MOVQ AX, SI
MOVQ DX, BX
// r0 += 38×l1×l4
MOVQ 8(CX), AX
IMUL3Q $0x26, AX, AX
MULQ 32(CX)
ADDQ AX, SI
ADCQ DX, BX
// r0 += 38×l2×l3
MOVQ 16(CX), AX
IMUL3Q $0x26, AX, AX
MULQ 24(CX)
ADDQ AX, SI
ADCQ DX, BX
// r1 = 2×l0×l1
MOVQ (CX), AX
SHLQ $0x01, AX
MULQ 8(CX)
MOVQ AX, R8
MOVQ DX, DI
// r1 += 38×l2×l4
MOVQ 16(CX), AX
IMUL3Q $0x26, AX, AX
MULQ 32(CX)
ADDQ AX, R8
ADCQ DX, DI
// r1 += 19×l3×l3
MOVQ 24(CX), AX
IMUL3Q $0x13, AX, AX
MULQ 24(CX)
ADDQ AX, R8
ADCQ DX, DI
// r2 = 2×l0×l2
MOVQ (CX), AX
SHLQ $0x01, AX
MULQ 16(CX)
MOVQ AX, R10
MOVQ DX, R9
// r2 += l1×l1
MOVQ 8(CX), AX
MULQ 8(CX)
ADDQ AX, R10
ADCQ DX, R9
// r2 += 38×l3×l4
MOVQ 24(CX), AX
IMUL3Q $0x26, AX, AX
MULQ 32(CX)
ADDQ AX, R10
ADCQ DX, R9
// r3 = 2×l0×l3
MOVQ (CX), AX
SHLQ $0x01, AX
MULQ 24(CX)
MOVQ AX, R12
MOVQ DX, R11
// r3 += 2×l1×l2
MOVQ 8(CX), AX
IMUL3Q $0x02, AX, AX
MULQ 16(CX)
ADDQ AX, R12
ADCQ DX, R11
// r3 += 19×l4×l4
MOVQ 32(CX), AX
IMUL3Q $0x13, AX, AX
MULQ 32(CX)
ADDQ AX, R12
ADCQ DX, R11
// r4 = 2×l0×l4
MOVQ (CX), AX
SHLQ $0x01, AX
MULQ 32(CX)
MOVQ AX, R14
MOVQ DX, R13
// r4 += 2×l1×l3
MOVQ 8(CX), AX
IMUL3Q $0x02, AX, AX
MULQ 24(CX)
ADDQ AX, R14
ADCQ DX, R13
// r4 += l2×l2
MOVQ 16(CX), AX
MULQ 16(CX)
ADDQ AX, R14
ADCQ DX, R13
// First reduction chain
MOVQ $0x0007ffffffffffff, AX
SHLQ $0x0d, SI, BX
SHLQ $0x0d, R8, DI
SHLQ $0x0d, R10, R9
SHLQ $0x0d, R12, R11
SHLQ $0x0d, R14, R13
ANDQ AX, SI
IMUL3Q $0x13, R13, R13
ADDQ R13, SI
ANDQ AX, R8
ADDQ BX, R8
ANDQ AX, R10
ADDQ DI, R10
ANDQ AX, R12
ADDQ R9, R12
ANDQ AX, R14
ADDQ R11, R14
// Second reduction chain (carryPropagate)
MOVQ SI, BX
SHRQ $0x33, BX
MOVQ R8, DI
SHRQ $0x33, DI
MOVQ R10, R9
SHRQ $0x33, R9
MOVQ R12, R11
SHRQ $0x33, R11
MOVQ R14, R13
SHRQ $0x33, R13
ANDQ AX, SI
IMUL3Q $0x13, R13, R13
ADDQ R13, SI
ANDQ AX, R8
ADDQ BX, R8
ANDQ AX, R10
ADDQ DI, R10
ANDQ AX, R12
ADDQ R9, R12
ANDQ AX, R14
ADDQ R11, R14
// Store output
MOVQ out+0(FP), AX
MOVQ SI, (AX)
MOVQ R8, 8(AX)
MOVQ R10, 16(AX)
MOVQ R12, 24(AX)
MOVQ R14, 32(AX)
RET

12
vendor/filippo.io/edwards25519/field/fe_amd64_noasm.go generated vendored Normal file
View File

@ -0,0 +1,12 @@
// Copyright (c) 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !amd64 || !gc || purego
// +build !amd64 !gc purego
package field
func feMul(v, x, y *Element) { feMulGeneric(v, x, y) }
func feSquare(v, x *Element) { feSquareGeneric(v, x) }

16
vendor/filippo.io/edwards25519/field/fe_arm64.go generated vendored Normal file
View File

@ -0,0 +1,16 @@
// Copyright (c) 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build arm64 && gc && !purego
// +build arm64,gc,!purego
package field
//go:noescape
func carryPropagate(v *Element)
func (v *Element) carryPropagate() *Element {
carryPropagate(v)
return v
}

42
vendor/filippo.io/edwards25519/field/fe_arm64.s generated vendored Normal file
View File

@ -0,0 +1,42 @@
// Copyright (c) 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build arm64 && gc && !purego
#include "textflag.h"
// carryPropagate works exactly like carryPropagateGeneric and uses the
// same AND, ADD, and LSR+MADD instructions emitted by the compiler, but
// avoids loading R0-R4 twice and uses LDP and STP.
//
// See https://golang.org/issues/43145 for the main compiler issue.
//
// func carryPropagate(v *Element)
TEXT ·carryPropagate(SB),NOFRAME|NOSPLIT,$0-8
MOVD v+0(FP), R20
LDP 0(R20), (R0, R1)
LDP 16(R20), (R2, R3)
MOVD 32(R20), R4
AND $0x7ffffffffffff, R0, R10
AND $0x7ffffffffffff, R1, R11
AND $0x7ffffffffffff, R2, R12
AND $0x7ffffffffffff, R3, R13
AND $0x7ffffffffffff, R4, R14
ADD R0>>51, R11, R11
ADD R1>>51, R12, R12
ADD R2>>51, R13, R13
ADD R3>>51, R14, R14
// R4>>51 * 19 + R10 -> R10
LSR $51, R4, R21
MOVD $19, R22
MADD R22, R10, R21, R10
STP (R10, R11), 0(R20)
STP (R12, R13), 16(R20)
MOVD R14, 32(R20)
RET

12
vendor/filippo.io/edwards25519/field/fe_arm64_noasm.go generated vendored Normal file
View File

@ -0,0 +1,12 @@
// Copyright (c) 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !arm64 || !gc || purego
// +build !arm64 !gc purego
package field
func (v *Element) carryPropagate() *Element {
return v.carryPropagateGeneric()
}

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