Compare commits

...

146 Commits

Author SHA1 Message Date
bab3681ac2 Merge pull request 'Merge XMPP reply feature into upstream repo' (#5) from uwaru/matterbridge:master into updatexmppp
Reviewed-on: lug/matterbridge#5
2025-02-23 09:52:49 -08:00
b74b884793
xmpp: Add support for replies 2024-11-04 04:52:16 +01:00
Wim
c4157a4d5b
Update dependencies (#2180)
* 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 6afa93e537 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 f044b948e2.

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 9a8ce9b17e
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
4628 changed files with 5918335 additions and 266275 deletions

View File

@ -1,22 +0,0 @@
# Docs: <https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/customizing-dependency-updates>
version: 2
updates:
- package-ecosystem: gomod
directory: /
schedule: {interval: weekly}
reviewers: [42wim]
assignees: [42wim]
- package-ecosystem: github-actions
directory: /
schedule: {interval: weekly}
reviewers: [42wim]
assignees: [42wim]
- package-ecosystem: docker
directory: /
schedule: {interval: weekly}
reviewers: [42wim]
assignees: [42wim]

View File

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

View File

@ -204,6 +204,17 @@ linters:
- tagliatelle
- errname
- typecheck
- grouper
- decorder
- maintidx
- exhaustruct
- asasalint
- execinquery
- nosnakecase
- exhaustive
- testifylint
- mnd
- depguard
# rules to deal with reported isues
issues:
# List of regexps of issue texts to exclude, empty list by default.

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

View File

@ -58,21 +58,22 @@ And more...
- [Binaries](#binaries)
- [Packages](#packages)
- [Building](#building)
- [Building with whatsapp (beta) multidevice support](#building-with-whatsapp-beta-multidevice-support)
- [Configuration](#configuration)
- [Basic configuration](#basic-configuration)
- [Settings](#settings)
- [Advanced configuration](#advanced-configuration)
- [Examples](#examples)
- [Bridge mattermost (off-topic) - irc (#testing)](#bridge-mattermost-off-topic---irc-testing)
- [Bridge slack (#general) - discord (general)](#bridge-slack-general---discord-general)
- [Running](#running)
- [Docker](#docker)
- [Systemd](#systemd)
- [Changelog](#changelog)
- [FAQ](#faq)
- [Related projects](#related-projects)
- [Articles / Tutorials](#articles--tutorials)
- [Thanks](#thanks)
- [Examples](#examples)
- [Bridge mattermost (off-topic) - irc (#testing)](#bridge-mattermost-off-topic---irc-testing)
- [Bridge slack (#general) - discord (general)](#bridge-slack-general---discord-general)
- [Running](#running)
- [Docker](#docker)
- [Systemd](#systemd)
- [Changelog](#changelog)
- [FAQ](#faq)
- [Related projects](#related-projects)
- [Articles / Tutorials](#articles--tutorials)
- [Thanks](#thanks)
## Features
@ -89,6 +90,7 @@ And more...
- [Discord](https://discordapp.com)
- [Gitter](https://gitter.im)
- [Harmony](https://harmonyapp.io)
- [IRC](http://www.mirc.com/servers.html)
- [Keybase](https://keybase.io)
- [Matrix](https://matrix.org)
@ -105,21 +107,28 @@ And more...
- [Twitch](https://twitch.tv)
- [VK](https://vk.com/)
- [WhatsApp](https://www.whatsapp.com/)
- Whatsapp legacy is natively supported
- Whatsapp multidevice beta is natively supported but you need to build yourself, see [here](#building-with-whatsapp-beta-multidevice-support)
- [XMPP](https://xmpp.org)
- [Zulip](https://zulipchat.com)
### 3rd party via matterbridge api
- [Delta Chat](https://github.com/deltachat-bot/matterdelta)
- [Minecraft](https://github.com/raws/mattercraft)
- [Minecraft](https://gitlab.com/Programie/MatterBukkit)
#### Past 3rd party projects
- [Discourse](https://github.com/DeclanHoare/matterbabble)
- [Facebook messenger](https://github.com/powerjungle/fbridge-asyncio)
- [Facebook messenger](https://github.com/VictorNine/fbridge)
- [Minecraft](https://github.com/elytra/MatterLink)
- [Minecraft](https://github.com/raws/mattercraft)
- [Minecraft](https://gitlab.com/Programie/MatterBukkit)
- [Reddit](https://github.com/bonehurtingjuice/mattereddit)
- [Counter-Strike, half-life and more](https://forums.alliedmods.net/showthread.php?t=319430)
- [MatterAMXX](https://github.com/GabeIggy/MatterAMXX)
- [MatterAMXX](https://github.com/andrewlindberg/MatterAMXX): [Counter-Strike, half-life and more](https://forums.alliedmods.net/showthread.php?t=319430)
- [Vintage Story](https://github.com/NikkyAI/vs-matterbridge)
- [Ultima Online Emulator](https://github.com/kuoushi/ServUO-Matterbridge)
- [Teamspeak](https://github.com/Archeb/ts-matterbridge)
### API
@ -138,6 +147,9 @@ Used by the projects below. Feel free to make a PR to add your project to this l
- [matterbabble](https://github.com/DeclanHoare/matterbabble) (Discourse support)
- [MatterAMXX](https://forums.alliedmods.net/showthread.php?t=319430) (Counter-Strike, half-life and more via AMXX mod)
- [Vintage Story](https://github.com/NikkyAI/vs-matterbridge)
- [ServUO-matterbridge](https://github.com/kuoushi/ServUO-Matterbridge) (A matterbridge connector for ServUO servers)
- [ts-matterbridge](https://github.com/Archeb/ts-matterbridge) (Integrate teamspeak chat with matterbridge)
- [beerchat](https://github.com/mt-mods/beerchat) (Matterbridge link for minetest)
## Chat with us
@ -164,10 +176,10 @@ See <https://github.com/42wim/matterbridge/wiki>
### Binaries
- Latest stable release [v1.23.2](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.
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.20.0-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.
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
@ -180,7 +192,12 @@ To install or upgrade just download the latest [binary](https://github.com/42wim
Most people just want to use binaries, you can find those [here](https://github.com/42wim/matterbridge/releases/latest)
If you really want to build from source, follow these instructions:
Go 1.17+ 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:
@ -194,6 +211,38 @@ To install the latest dev run:
go install github.com/42wim/matterbridge@master
```
To install the latest stable run without msteams or zulip bridge:
```bash
go install -tags nomsteams,nozulip github.com/42wim/matterbridge
```
You should now have matterbridge binary in the ~/go/bin directory:
```bash
$ ls ~/go/bin/
matterbridge
```
## Building with whatsapp (beta) multidevice support
Because the library we use for Whatsapp multidevice support includes a GPL3 library we can not provide you binaries.
(as this would require the Matterbridge to change it license to GPL)
Matterbridge can be build without gcc/c-compiler: If you're running on windows first run `set CGO_ENABLED=0` on other platforms you prepend `CGO_ENABLED=0` to the `go build` command. (eg `CGO_ENABLED=0 go install github.com/42wim/matterbridge`)
So this means you have to build it yourself using the instructions below:
```bash
go install -tags whatsappmulti github.com/42wim/matterbridge@master
```
If you're low on memory and don't need msteams:
```bash
go install -tags nomsteams,whatsappmulti github.com/42wim/matterbridge@master
```
You should now have matterbridge binary in the ~/go/bin directory:
```bash
@ -323,6 +372,8 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
- [nextcloud talk](https://github.com/nextcloud/talk_matterbridge) (Integrates matterbridge in Nextcloud Talk)
- [mattercraft](https://github.com/raws/mattercraft) (Minecraft bridge)
- [vs-matterbridge](https://github.com/NikkyAI/vs-matterbridge) (Vintage Story bridge)
- [ServUO-matterbridge](https://github.com/kuoushi/ServUO-Matterbridge) (A matterbridge connector for ServUO servers)
- [ts-matterbridge](https://github.com/Archeb/ts-matterbridge) (Integrate teamspeak chat with matterbridge)
## Articles / Tutorials
@ -352,10 +403,10 @@ Matterbridge wouldn't exist without these libraries:
- discord - <https://github.com/bwmarrin/discordgo>
- echo - <https://github.com/labstack/echo>
- gitter - <https://github.com/sromku/go-gitter>
- gops - <https://github.com/google/gops>
- gozulipbot - <https://github.com/ifo/gozulipbot>
- gumble - <https://github.com/layeh/gumble>
- harmony - <https://github.com/harmony-development/shibshib>
- irc - <https://github.com/lrstanley/girc>
- keybase - <https://github.com/keybase/go-keybase-chat-bot>
- matrix - <https://github.com/matrix-org/gomatrix>
@ -363,6 +414,7 @@ Matterbridge wouldn't exist without these libraries:
- msgraph.go - <https://github.com/yaegashi/msgraph.go>
- mumble - <https://github.com/layeh/gumble>
- nctalk - <https://github.com/gary-kim/go-nc-talk>
- rocketchat - <https://github.com/RocketChat/Rocket.Chat.Go.SDK>
- slack - <https://github.com/nlopes/slack>
- sshchat - <https://github.com/shazow/ssh-chat>
- steam - <https://github.com/Philipp15b/go-steam>
@ -370,6 +422,7 @@ Matterbridge wouldn't exist without these libraries:
- tengo - <https://github.com/d5/tengo>
- vk - <https://github.com/SevereCloud/vksdk>
- whatsapp - <https://github.com/Rhymen/go-whatsapp>
- whatsapp - <https://github.com/tulir/whatsmeow>
- xmpp - <https://github.com/mattn/go-xmpp>
- zulip - <https://github.com/ifo/gozulipbot>

View File

@ -1,17 +1,20 @@
package api
import (
"encoding/base64"
"encoding/json"
"net/http"
"strings"
"sync"
"time"
"gopkg.in/olahol/melody.v1"
"github.com/olahol/melody"
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/mitchellh/mapstructure"
ring "github.com/zfjagann/golang-ring"
)
@ -137,6 +140,36 @@ func (b *API) handlePostMessage(c echo.Context) error {
message.Account = b.Account
message.ID = ""
message.Timestamp = time.Now()
var (
fm map[string]interface{}
ds string
ok bool
)
for i, f := range message.Extra["file"] {
fi := config.FileInfo{}
if fm, ok = f.(map[string]interface{}); !ok {
return echo.NewHTTPError(http.StatusInternalServerError, "invalid format for extra")
}
err := mapstructure.Decode(fm, &fi)
if err != nil {
if !strings.Contains(err.Error(), "got string") {
return err
}
}
// mapstructure doesn't decode base64 into []byte, so it must be done manually for fi.Data
if ds, ok = fm["Data"].(string); !ok {
return echo.NewHTTPError(http.StatusInternalServerError, "invalid format for data")
}
data, err := base64.StdEncoding.DecodeString(ds)
if err != nil {
return err
}
fi.Data = &data
message.Extra["file"][i] = fi
}
b.Log.Debugf("Sending message from %s on %s to gateway", message.Username, "api")
b.Remote <- message
return c.JSON(http.StatusOK, message)
@ -166,15 +199,20 @@ func (b *API) handleStream(c echo.Context) error {
}
c.Response().Flush()
for {
select {
// TODO: this causes issues, messages should be broadcasted to all connected clients
msg := b.Messages.Dequeue()
if msg != nil {
if err := json.NewEncoder(c.Response()).Encode(msg); err != nil {
return err
default:
msg := b.Messages.Dequeue()
if msg != nil {
if err := json.NewEncoder(c.Response()).Encode(msg); err != nil {
return err
}
c.Response().Flush()
}
c.Response().Flush()
time.Sleep(100 * time.Millisecond)
case <-c.Request().Context().Done():
return nil
}
time.Sleep(200 * time.Millisecond)
}
}

View File

@ -121,6 +121,7 @@ type Protocol struct {
MessageLength int // IRC, max length of a message allowed
MessageQueue int // IRC, size of message queue for flood control
MessageSplit bool // IRC, split long messages with newlines on MessageLength instead of clipping
MessageSplitMaxCount int // discord, split long messages into at most this many messages instead of clipping (MessageLength=1950 cannot be configured)
Muc string // xmpp
MxID string // matrix
Name string // all protocols

View File

@ -10,8 +10,8 @@ import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/discord/transmitter"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/bwmarrin/discordgo"
lru "github.com/hashicorp/golang-lru"
"github.com/matterbridge/discordgo"
)
const (
@ -81,17 +81,6 @@ func (b *Bdiscord) Connect() error {
return err
}
b.Log.Info("Connection succeeded")
b.c.AddHandler(b.messageCreate)
b.c.AddHandler(b.messageTyping)
b.c.AddHandler(b.memberUpdate)
b.c.AddHandler(b.messageUpdate)
b.c.AddHandler(b.messageDelete)
b.c.AddHandler(b.messageDeleteBulk)
b.c.AddHandler(b.memberAdd)
b.c.AddHandler(b.memberRemove)
if b.GetInt("debuglevel") == 1 {
b.c.AddHandler(b.messageEvent)
}
// Add privileged intent for guild member tracking. This is needed to track nicks
// for display names and @mention translation
b.c.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged |
@ -101,7 +90,7 @@ func (b *Bdiscord) Connect() error {
if err != nil {
return err
}
guilds, err := b.c.UserGuilds(100, "", "")
guilds, err := b.c.UserGuilds(100, "", "", false)
if err != nil {
return err
}
@ -233,6 +222,19 @@ func (b *Bdiscord) Connect() error {
b.nickMemberMap[member.Nick] = member
}
}
b.c.AddHandler(b.messageCreate)
b.c.AddHandler(b.messageTyping)
b.c.AddHandler(b.messageUpdate)
b.c.AddHandler(b.messageDelete)
b.c.AddHandler(b.messageDeleteBulk)
b.c.AddHandler(b.memberAdd)
b.c.AddHandler(b.memberRemove)
b.c.AddHandler(b.memberUpdate)
if b.GetInt("debuglevel") == 1 {
b.c.AddHandler(b.messageEvent)
}
return nil
}
@ -272,7 +274,6 @@ func (b *Bdiscord) Send(msg config.Message) (string, error) {
// Handle prefix hint for unthreaded messages.
if msg.ParentNotFound() {
msg.ParentID = ""
msg.Text = fmt.Sprintf("[thread]: %s", msg.Text)
}
// Use webhook to send the message
@ -315,6 +316,7 @@ func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (st
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(msg, b.General) {
// TODO: Use ClipOrSplitMessage
rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength, b.GetString("MessageClipped"))
if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil {
b.Log.Errorf("Could not send message %#v: %s", rmsg, err)
@ -326,35 +328,53 @@ func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (st
}
}
msg.Text = helper.ClipMessage(msg.Text, MessageLength, b.GetString("MessageClipped"))
msg.Text = b.replaceUserMentions(msg.Text)
// Edit message
if msg.ID != "" {
_, err := b.c.ChannelMessageEdit(channelID, msg.ID, msg.Username+msg.Text)
return msg.ID, err
}
m := discordgo.MessageSend{
Content: msg.Username + msg.Text,
AllowedMentions: b.getAllowedMentions(),
}
if msg.ParentValid() {
m.Reference = &discordgo.MessageReference{
MessageID: msg.ParentID,
ChannelID: channelID,
GuildID: b.guildID,
// Exploit that a discord message ID is actually just a large number, and we encode a list of IDs by separating them with ";".
msgIds := strings.Split(msg.ID, ";")
msgParts := helper.ClipOrSplitMessage(b.replaceUserMentions(msg.Text), MessageLength, b.GetString("MessageClipped"), len(msgIds))
for len(msgParts) < len(msgIds) {
msgParts = append(msgParts, "((obsoleted by edit))")
}
for i := range msgParts {
// In case of split-messages where some parts remain the same (i.e. only a typo-fix in a huge message), this causes some noop-updates.
// TODO: Optimize away noop-updates of un-edited messages
// TODO: Use RemoteNickFormat instead of this broken concatenation
_, err := b.c.ChannelMessageEdit(channelID, msgIds[i], msg.Username+msgParts[i])
if err != nil {
return "", err
}
}
return msg.ID, nil
}
// Post normal message
res, err := b.c.ChannelMessageSendComplex(channelID, &m)
if err != nil {
return "", err
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)
}
return res.ID, nil
// 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

View File

@ -2,11 +2,15 @@ package bdiscord
import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/bwmarrin/discordgo"
"github.com/davecgh/go-spew/spew"
"github.com/matterbridge/discordgo"
)
func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelete) { //nolint:unparam
if m.GuildID != b.guildID {
b.Log.Debugf("Ignoring messageDelete because it originates from a different guild")
return
}
rmsg := config.Message{Account: b.Account, ID: m.ID, Event: config.EventMsgDelete, Text: config.EventMsgDelete}
rmsg.Channel = b.getChannelName(m.ChannelID)
@ -17,6 +21,10 @@ func (b *Bdiscord) messageDelete(s *discordgo.Session, m *discordgo.MessageDelet
// TODO(qaisjp): if other bridges support bulk deletions, it could be fanned out centrally
func (b *Bdiscord) messageDeleteBulk(s *discordgo.Session, m *discordgo.MessageDeleteBulk) { //nolint:unparam
if m.GuildID != b.guildID {
b.Log.Debugf("Ignoring messageDeleteBulk because it originates from a different guild")
return
}
for _, msgID := range m.Messages {
rmsg := config.Message{
Account: b.Account,
@ -37,6 +45,10 @@ func (b *Bdiscord) messageEvent(s *discordgo.Session, m *discordgo.Event) {
}
func (b *Bdiscord) messageTyping(s *discordgo.Session, m *discordgo.TypingStart) {
if m.GuildID != b.guildID {
b.Log.Debugf("Ignoring messageTyping because it originates from a different guild")
return
}
if !b.GetBool("ShowUserTyping") {
return
}
@ -52,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
if m.GuildID != b.guildID {
b.Log.Debugf("Ignoring messageUpdate because it originates from a different guild")
return
}
if b.GetBool("EditDisable") {
return
}
// only when message is actually edited
if m.Message.EditedTimestamp != "" {
if m.Message.EditedTimestamp != nil {
b.Log.Debugf("Sending edit message")
m.Content += b.GetString("EditSuffix")
msg := &discordgo.MessageCreate{
@ -67,6 +83,10 @@ func (b *Bdiscord) messageUpdate(s *discordgo.Session, m *discordgo.MessageUpdat
}
func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { //nolint:unparam
if m.GuildID != b.guildID {
b.Log.Debugf("Ignoring messageCreate because it originates from a different guild")
return
}
var err error
// not relay our own messages
@ -144,6 +164,10 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
}
func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUpdate) {
if m.GuildID != b.guildID {
b.Log.Debugf("Ignoring memberUpdate because it originates from a different guild")
return
}
if m.Member == nil {
b.Log.Warnf("Received member update with no member information: %#v", m)
}
@ -171,6 +195,13 @@ func (b *Bdiscord) memberUpdate(s *discordgo.Session, m *discordgo.GuildMemberUp
}
func (b *Bdiscord) memberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd) {
if m.GuildID != b.guildID {
b.Log.Debugf("Ignoring memberAdd because it originates from a different guild")
return
}
if b.GetBool("nosendjoinpart") {
return
}
if m.Member == nil {
b.Log.Warnf("Received member update with no member information: %#v", m)
return
@ -192,6 +223,13 @@ func (b *Bdiscord) memberAdd(s *discordgo.Session, m *discordgo.GuildMemberAdd)
}
func (b *Bdiscord) memberRemove(s *discordgo.Session, m *discordgo.GuildMemberRemove) {
if m.GuildID != b.guildID {
b.Log.Debugf("Ignoring memberRemove because it originates from a different guild")
return
}
if b.GetBool("nosendjoinpart") {
return
}
if m.Member == nil {
b.Log.Warnf("Received member update with no member information: %#v", m)
return

View File

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

View File

@ -6,7 +6,7 @@ import (
"strings"
"unicode"
"github.com/matterbridge/discordgo"
"github.com/bwmarrin/discordgo"
)
func (b *Bdiscord) getAllowedMentions() *discordgo.MessageAllowedMentions {
@ -68,7 +68,7 @@ func (b *Bdiscord) getGuildMemberByNick(nick string) (*discordgo.Member, error)
b.membersMutex.RLock()
defer b.membersMutex.RUnlock()
if member, ok := b.nickMemberMap[nick]; ok {
if member, ok := b.nickMemberMap[strings.TrimSpace(nick)]; ok {
return member, nil
}
return nil, errors.New("Couldn't find guild member with nick " + nick) // This will most likely get ignored by the caller

View File

@ -20,7 +20,7 @@ import (
"sync"
"time"
"github.com/matterbridge/discordgo"
"github.com/bwmarrin/discordgo"
log "github.com/sirupsen/logrus"
)

View File

@ -1,7 +1,7 @@
package transmitter
import (
"github.com/matterbridge/discordgo"
"github.com/bwmarrin/discordgo"
)
// isDiscordPermissionError returns false for nil, and true if a Discord RESTError with code discordgo.ErrorCodeMissionPermissions

View File

@ -2,10 +2,11 @@ package bdiscord
import (
"bytes"
"strings"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/matterbridge/discordgo"
"github.com/bwmarrin/discordgo"
)
// shouldMessageUseWebhooks checks if have a channel specific webhook, if we're not using auto webhooks
@ -42,12 +43,65 @@ func (b *Bdiscord) maybeGetLocalAvatar(msg *config.Message) string {
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) (*discordgo.Message, error) {
func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (string, error) {
var (
res *discordgo.Message
res string
err error
)
@ -60,45 +114,13 @@ func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordg
// We can't send empty messages.
if msg.Text != "" {
res, err = b.transmitter.Send(
channelID,
&discordgo.WebhookParams{
Content: msg.Text,
Username: msg.Username,
AvatarURL: msg.Avatar,
AllowedMentions: b.getAllowedMentions(),
},
)
if err != nil {
b.Log.Errorf("Could not send text (%s) for message %#v: %s", msg.Text, msg, err)
}
res, err = b.webhookSendTextOnly(msg, channelID)
}
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 := fi.Comment
_, e2 := b.transmitter.Send(
channelID,
&discordgo.WebhookParams{
Username: msg.Username,
AvatarURL: msg.Avatar,
File: &file,
Content: content,
AllowedMentions: b.getAllowedMentions(),
},
)
if e2 != nil {
b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, e2)
}
}
if err == nil && msg.Extra != nil {
err = b.webhookSendFilesOnly(msg, channelID)
}
return res, err
}
@ -114,35 +136,44 @@ func (b *Bdiscord) handleEventWebhook(msg *config.Message, channelID string) (st
return "", nil
}
msg.Text = helper.ClipMessage(msg.Text, MessageLength, b.GetString("MessageClipped"))
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 != "" {
// 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")
err := b.transmitter.Edit(channelID, msg.ID, &discordgo.WebhookParams{
Content: msg.Text,
Username: msg.Username,
AllowedMentions: b.getAllowedMentions(),
})
if err == nil {
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", err)
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)
discordMsg, err := b.webhookSend(msg, channelID)
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", msg, err)
b.Log.Errorf("Could not broadcast via webhook for message %#v: %s", msgID, err)
return "", err
}
if discordMsg == nil {
return "", nil
}
return discordMsg.ID, nil
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
}

View File

@ -86,6 +86,12 @@ func GetSubLines(message string, maxLineLength int, clippingMessage string) []st
var lines []string
for _, line := range strings.Split(strings.TrimSpace(message), "\n") {
if line == "" {
// Prevent sending empty messages, so we'll skip this line
// if it has no content.
continue
}
if maxLineLength == 0 || len([]byte(line)) <= maxLineLength {
lines = append(lines, line)
continue
@ -205,14 +211,51 @@ func ClipMessage(text string, length int, clippingMessage string) string {
if len(text) > length {
text = text[:length-len(clippingMessage)]
if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError {
text = text[:len(text)-size]
for len(text) > 0 {
if r, _ := utf8.DecodeLastRuneInString(text); r == utf8.RuneError {
text = text[:len(text)-1]
// Note: DecodeLastRuneInString only returns the constant value "1" in
// case of an error. We do not yet know whether the last rune is now
// actually valid. Example: "€" is 0xE2 0x82 0xAC. If we happen to split
// the string just before 0xAC, and go back only one byte, that would
// leave us with a string that ends in the byte 0xE2, which is not a valid
// rune, so we need to try again.
} else {
break
}
}
text += clippingMessage
}
return text
}
func ClipOrSplitMessage(text string, length int, clippingMessage string, splitMax int) []string {
var msgParts []string
remainingText := text
// Invariant of this splitting loop: No text is lost (msgParts+remainingText is the original text),
// and all parts is guaranteed to satisfy the length requirement.
for len(msgParts) < splitMax-1 && len(remainingText) > length {
// Decision: The text needs to be split (again).
var chunk string
wasted := 0
// The longest UTF-8 encoding of a valid rune is 4 bytes (0xF4 0x8F 0xBF 0xBF, encoding U+10FFFF),
// so we should never need to waste 4 or more bytes at a time.
for wasted < 4 && wasted < length {
chunk = remainingText[:length-wasted]
if r, _ := utf8.DecodeLastRuneInString(chunk); r == utf8.RuneError {
wasted += 1
} else {
break
}
}
// Note: At this point, "chunk" might still be invalid, if "text" is very broken.
msgParts = append(msgParts, chunk)
remainingText = remainingText[len(chunk):]
}
msgParts = append(msgParts, ClipMessage(remainingText, length, clippingMessage))
return msgParts
}
// ParseMarkdown takes in an input string as markdown and parses it to html
func ParseMarkdown(input string) string {
extensions := parser.HardLineBreak | parser.NoIntraEmphasis | parser.FencedCode

View File

@ -88,6 +88,15 @@ var lineSplittingTestCases = map[string]struct {
},
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) {
@ -125,3 +134,105 @@ func TestConvertWebPToPNG(t *testing.T) {
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

@ -1,5 +1,4 @@
//go:build cgo
// +build cgo
//go:build cgolottie
package helper

View File

@ -1,4 +1,4 @@
// +build !cgo
//go:build !cgolottie
package helper
@ -6,6 +6,7 @@ import (
"io/ioutil"
"os"
"os/exec"
"github.com/sirupsen/logrus"
)

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/helper"
"github.com/lrstanley/girc"
"github.com/missdeer/golib/ic"
"github.com/paulrosania/go-charset/charset"
"github.com/saintfish/chardet"
@ -24,12 +23,12 @@ func (b *Birc) handleCharset(msg *config.Message) error {
if b.GetString("Charset") != "" {
switch b.GetString("Charset") {
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
msg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), msg.Text)
msg.Text = toUTF8(b.GetString("Charset"), msg.Text)
default:
buf := new(bytes.Buffer)
w, err := charset.NewWriter(b.GetString("Charset"), buf)
if err != nil {
b.Log.Errorf("charset from utf-8 conversion failed: %s", err)
b.Log.Errorf("charset to utf-8 conversion failed: %s", err)
return err
}
fmt.Fprint(w, msg.Text)
@ -123,8 +122,18 @@ func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) {
i := b.i
b.Nick = event.Params[0]
b.Log.Debug("Clearing handlers before adding in case of BNC reconnect")
i.Handlers.Clear("PRIVMSG")
i.Handlers.Clear("CTCP_ACTION")
i.Handlers.Clear(girc.RPL_TOPICWHOTIME)
i.Handlers.Clear(girc.NOTICE)
i.Handlers.Clear("JOIN")
i.Handlers.Clear("PART")
i.Handlers.Clear("QUIT")
i.Handlers.Clear("KICK")
i.Handlers.Clear("INVITE")
i.Handlers.AddBg("PRIVMSG", b.handlePrivMsg)
i.Handlers.AddBg("CTCP_ACTION", b.handlePrivMsg)
i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
i.Handlers.AddBg(girc.NOTICE, b.handleNotice)
i.Handlers.AddBg("JOIN", b.handleJoinPart)
@ -196,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)
// 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
}
@ -227,7 +240,7 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
}
switch mycharset {
case "gbk", "gb18030", "gb2312", "big5", "euc-kr", "euc-jp", "shift-jis", "iso-2022-jp":
rmsg.Text = ic.ConvertString("utf-8", b.GetString("Charset"), rmsg.Text)
rmsg.Text = toUTF8(b.GetString("Charset"), rmsg.Text)
default:
r, err := charset.NewReader(mycharset, strings.NewReader(rmsg.Text))
if err != nil {
@ -244,6 +257,7 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
func (b *Birc) handleRunCommands() {
for _, cmd := range b.GetStringSlice("RunCommands") {
cmd = strings.ReplaceAll(cmd, "{BOTNICK}", b.Nick)
if err := b.i.Cmd.SendRaw(cmd); err != nil {
b.Log.Errorf("RunCommands %s failed: %s", cmd, err)
}

View File

@ -362,8 +362,10 @@ func (b *Birc) skipPrivMsg(event girc.Event) bool {
return true
}
// don't forward message from ourself
if event.Source.Name == b.Nick {
return true
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 {

View File

@ -8,7 +8,7 @@ import (
"strings"
"time"
matrix "github.com/matrix-org/gomatrix"
matrix "github.com/matterbridge/gomatrix"
)
func newMatrixUsername(username string) *matrixUsername {
@ -51,7 +51,7 @@ func interface2Struct(in interface{}, out interface{}) error {
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 {
if b.GetBool("UseUserName") {
return mxid[1:]

View File

@ -12,7 +12,7 @@ import (
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
matrix "github.com/matrix-org/gomatrix"
matrix "github.com/matterbridge/gomatrix"
)
var (
@ -148,12 +148,37 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
username := newMatrixUsername(msg.Username)
body := username.plain + msg.Text
formattedBody := username.formatted + helper.ParseMarkdown(msg.Text)
if b.GetBool("SpoofUsername") {
// https://spec.matrix.org/v1.3/client-server-api/#mroommember
type stateMember struct {
AvatarURL string `json:"avatar_url,omitempty"`
DisplayName string `json:"displayname"`
Membership string `json:"membership"`
}
// TODO: reset username afterwards with DisplayName: null ?
m := stateMember{
AvatarURL: "",
DisplayName: username.plain,
Membership: "join",
}
_, err := b.mc.SendStateEvent(channel, "m.room.member", b.UserID, m)
if err == nil {
body = msg.Text
formattedBody = helper.ParseMarkdown(msg.Text)
}
}
// Make a action /me of the message
if msg.Event == config.EventUserAction {
m := matrix.TextMessage{
MsgType: "m.emote",
Body: username.plain + msg.Text,
FormattedBody: username.formatted + helper.ParseMarkdown(msg.Text),
Body: body,
FormattedBody: formattedBody,
Format: "org.matrix.custom.html",
}
@ -224,10 +249,10 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
if msg.ID != "" {
rmsg := EditedMessage{
TextMessage: matrix.TextMessage{
Body: username.plain + msg.Text,
Body: body,
MsgType: "m.text",
Format: "org.matrix.custom.html",
FormattedBody: username.formatted + helper.ParseMarkdown(msg.Text),
FormattedBody: formattedBody,
},
}
@ -266,8 +291,8 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
if msg.Event == config.EventJoinLeave {
m := matrix.TextMessage{
MsgType: "m.notice",
Body: username.plain + msg.Text,
FormattedBody: username.formatted + msg.Text,
Body: body,
FormattedBody: formattedBody,
Format: "org.matrix.custom.html",
}
@ -297,8 +322,8 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
m := ReplyMessage{
TextMessage: matrix.TextMessage{
MsgType: "m.text",
Body: username.plain + msg.Text,
FormattedBody: username.formatted + helper.ParseMarkdown(msg.Text),
Body: body,
FormattedBody: formattedBody,
Format: "org.matrix.custom.html",
},
}
@ -338,7 +363,7 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
)
err = b.retry(func() error {
resp, err = b.mc.SendText(channel, username.plain+msg.Text)
resp, err = b.mc.SendText(channel, body)
return err
})
@ -356,8 +381,7 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
)
err = b.retry(func() error {
resp, err = b.mc.SendFormattedText(channel, username.plain+msg.Text,
username.formatted+helper.ParseMarkdown(msg.Text))
resp, err = b.mc.SendFormattedText(channel, body, formattedBody)
return err
})
@ -428,12 +452,15 @@ func (b *Bmatrix) handleReply(ev *matrix.Event, rmsg config.Message) bool {
}
body := rmsg.Text
for strings.HasPrefix(body, "> ") {
lineIdx := strings.IndexRune(body, '\n')
if lineIdx == -1 {
body = ""
} else {
body = body[(lineIdx + 1):]
if !b.GetBool("keepquotedreply") {
for strings.HasPrefix(body, "> ") {
lineIdx := strings.IndexRune(body, '\n')
if lineIdx == -1 {
body = ""
} else {
body = body[(lineIdx + 1):]
}
}
}

View File

@ -1,12 +1,12 @@
package bmattermost
import (
"context"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterclient"
matterclient6 "github.com/matterbridge/matterclient"
"github.com/mattermost/mattermost-server/v5/model"
model6 "github.com/mattermost/mattermost-server/v6/model"
"github.com/matterbridge/matterclient"
"github.com/mattermost/mattermost/server/public/model"
)
// handleDownloadAvatar downloads the avatar of userid from channel
@ -26,20 +26,11 @@ func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
var (
data []byte
err error
resp *model.Response
)
if b.mc6 != nil {
data, _, err = b.mc6.Client.GetProfileImage(userid, "")
if err != nil {
b.Log.Errorf("ProfileImage download failed for %#v %s", userid, err)
return
}
} else {
data, resp = b.mc.Client.GetProfileImage(userid, "")
if resp.Error != nil {
b.Log.Errorf("ProfileImage download failed for %#v %s", userid, resp.Error)
return
}
data, _, err = b.mc.Client.GetProfileImage(context.TODO(), userid, "")
if err != nil {
b.Log.Errorf("ProfileImage download failed for %#v %s", userid, err)
return
}
err = helper.HandleDownloadSize(b.Log, &rmsg, userid+".png", int64(len(data)), b.General)
@ -52,33 +43,10 @@ func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
}
}
// handleDownloadFile handles file download
//nolint:wrapcheck
func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error {
if b.mc6 != nil {
return b.handleDownloadFile6(rmsg, id)
}
url, _ := b.mc.Client.GetFileLink(id)
finfo, resp := b.mc.Client.GetFileInfo(id)
if resp.Error != nil {
return resp.Error
}
err := helper.HandleDownloadSize(b.Log, rmsg, finfo.Name, finfo.Size, b.General)
if err != nil {
return err
}
data, resp := b.mc.Client.DownloadFile(id, true)
if resp.Error != nil {
return resp.Error
}
helper.HandleDownloadData(b.Log, rmsg, finfo.Name, rmsg.Text, url, &data, b.General)
return nil
}
// nolint:wrapcheck
func (b *Bmattermost) handleDownloadFile6(rmsg *config.Message, id string) error {
url, _, _ := b.mc6.Client.GetFileLink(id)
finfo, _, err := b.mc6.Client.GetFileInfo(id)
url, _, _ := b.mc.Client.GetFileLink(context.TODO(), id)
finfo, _, err := b.mc.Client.GetFileInfo(context.TODO(), id)
if err != nil {
return err
}
@ -86,7 +54,7 @@ func (b *Bmattermost) handleDownloadFile6(rmsg *config.Message, id string) error
if err != nil {
return err
}
data, _, err := b.mc6.Client.DownloadFile(id, true)
data, _, err := b.mc.Client.DownloadFile(context.TODO(), id, true)
if err != nil {
return err
}
@ -125,15 +93,10 @@ func (b *Bmattermost) handleMatter() {
}
}
//nolint:cyclop
func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
if b.mc6 != nil {
b.Log.Debug("starting matterclient6")
b.handleMatterClient6(messages)
return
}
for message := range b.mc.MessageChan {
b.Log.Debugf("%#v", message.Raw.Data)
b.Log.Debugf("%#v %#v", message.Raw.GetData(), message.Raw.EventType())
if b.skipMessage(message) {
b.Log.Debugf("Skipped message: %#v", message)
@ -166,11 +129,11 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
b.handleProps(rmsg, message)
// create a text for bridges that don't support native editing
if message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED && !b.GetBool("EditDisable") {
if message.Raw.EventType() == model.WebsocketEventPostEdited && !b.GetBool("EditDisable") {
rmsg.Text = message.Text + b.GetString("EditSuffix")
}
if message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED {
if message.Raw.EventType() == model.WebsocketEventPostDeleted {
rmsg.Event = config.EventMsgDelete
}
@ -192,68 +155,6 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
}
}
// nolint:cyclop
func (b *Bmattermost) handleMatterClient6(messages chan *config.Message) {
for message := range b.mc6.MessageChan {
b.Log.Debugf("%#v %#v", message.Raw.GetData(), message.Raw.EventType())
if b.skipMessage6(message) {
b.Log.Debugf("Skipped message: %#v", message)
continue
}
channelName := b.getChannelName(message.Post.ChannelId)
if channelName == "" {
channelName = message.Channel
}
// only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" {
b.handleDownloadAvatar(message.UserID, channelName)
}
b.Log.Debugf("== Receiving event %#v", message)
rmsg := &config.Message{
Username: message.Username,
UserID: message.UserID,
Channel: channelName,
Text: message.Text,
ID: message.Post.Id,
ParentID: message.Post.RootId, // ParentID is obsolete with mattermost
Extra: make(map[string][]interface{}),
}
// handle mattermost post properties (override username and attachments)
b.handleProps6(rmsg, message)
// create a text for bridges that don't support native editing
if message.Raw.EventType() == model6.WebsocketEventPostEdited && !b.GetBool("EditDisable") {
rmsg.Text = message.Text + b.GetString("EditSuffix")
}
if message.Raw.EventType() == model6.WebsocketEventPostDeleted {
rmsg.Event = config.EventMsgDelete
}
for _, id := range message.Post.FileIds {
err := b.handleDownloadFile(rmsg, id)
if err != nil {
b.Log.Errorf("download failed: %s", err)
}
}
// Use nickname instead of username if defined
if !b.GetBool("useusername") {
if nick := b.mc6.GetNickName(rmsg.UserID); nick != "" {
rmsg.Username = nick
}
}
messages <- rmsg
}
}
func (b *Bmattermost) handleMatterHook(messages chan *config.Message) {
for {
message := b.mh.Receive()
@ -268,12 +169,7 @@ func (b *Bmattermost) handleMatterHook(messages chan *config.Message) {
}
}
// handleUploadFile handles native upload of files
func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
if b.mc6 != nil {
return b.handleUploadFile6(msg)
}
var err error
var res, id string
channelID := b.getChannelID(msg.Channel)
@ -292,53 +188,8 @@ func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
return res, err
}
// nolint:forcetypeassert,wrapcheck
func (b *Bmattermost) handleUploadFile6(msg *config.Message) (string, error) {
var err error
var res, id string
channelID := b.getChannelID(msg.Channel)
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
id, err = b.mc6.UploadFile(*fi.Data, channelID, fi.Name)
if err != nil {
return "", err
}
msg.Text = fi.Comment
if b.GetBool("PrefixMessagesWithNick") {
msg.Text = msg.Username + msg.Text
}
res, err = b.mc6.PostMessageWithFiles(channelID, msg.Text, msg.ParentID, []string{id})
}
return res, err
}
//nolint:forcetypeassert
func (b *Bmattermost) handleProps(rmsg *config.Message, message *matterclient.Message) {
props := message.Post.Props
if props == nil {
return
}
if _, ok := props["override_username"].(string); ok {
rmsg.Username = props["override_username"].(string)
}
if _, ok := props["attachments"].([]interface{}); ok {
rmsg.Extra["attachments"] = props["attachments"].([]interface{})
if rmsg.Text == "" {
for _, attachment := range rmsg.Extra["attachments"] {
attach := attachment.(map[string]interface{})
if attach["text"].(string) != "" {
rmsg.Text += attach["text"].(string)
continue
}
if attach["fallback"].(string) != "" {
rmsg.Text += attach["fallback"].(string)
}
}
}
}
}
// nolint:forcetypeassert
func (b *Bmattermost) handleProps6(rmsg *config.Message, message *matterclient6.Message) {
props := message.Post.Props
if props == nil {
return

View File

@ -6,11 +6,9 @@ import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterclient"
"github.com/42wim/matterbridge/matterhook"
matterclient6 "github.com/matterbridge/matterclient"
"github.com/mattermost/mattermost-server/v5/model"
model6 "github.com/mattermost/mattermost-server/v6/model"
"github.com/matterbridge/matterclient"
"github.com/mattermost/mattermost/server/public/model"
)
func (b *Bmattermost) doConnectWebhookBind() error {
@ -24,33 +22,15 @@ func (b *Bmattermost) doConnectWebhookBind() error {
})
case b.GetString("Token") != "":
b.Log.Info("Connecting using token (sending)")
b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
if b.v6 {
err := b.apiLogin6()
if err != nil {
return err
}
} else {
err := b.apiLogin()
if err != nil {
return err
}
err := b.apiLogin()
if err != nil {
return err
}
case b.GetString("Login") != "":
b.Log.Info("Connecting using login/password (sending)")
b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
if b.v6 {
err := b.apiLogin6()
if err != nil {
return err
}
} else {
err := b.apiLogin()
if err != nil {
return err
}
err := b.apiLogin()
if err != nil {
return err
}
default:
b.Log.Info("Connecting using webhookbindaddress (receiving)")
@ -72,45 +52,28 @@ func (b *Bmattermost) doConnectWebhookURL() error {
})
if b.GetString("Token") != "" {
b.Log.Info("Connecting using token (receiving)")
b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
if b.v6 {
err := b.apiLogin6()
if err != nil {
return err
}
} else {
err := b.apiLogin()
if err != nil {
return err
}
err := b.apiLogin()
if err != nil {
return err
}
} else if b.GetString("Login") != "" {
b.Log.Info("Connecting using login/password (receiving)")
b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
if b.v6 {
err := b.apiLogin6()
if err != nil {
return err
}
} else {
err := b.apiLogin()
if err != nil {
return err
}
err := b.apiLogin()
if err != nil {
return err
}
}
return nil
}
//nolint:wrapcheck
func (b *Bmattermost) apiLogin() error {
password := b.GetString("Password")
if b.GetString("Token") != "" {
password = "token=" + b.GetString("Token")
}
b.mc = matterclient.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server"))
b.mc = matterclient.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server"), "")
if b.GetBool("debug") {
b.mc.SetLogLevel("debug")
}
@ -118,39 +81,13 @@ func (b *Bmattermost) apiLogin() error {
b.mc.SkipVersionCheck = b.GetBool("SkipVersionCheck")
b.mc.NoTLS = b.GetBool("NoTLS")
b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server"))
err := b.mc.Login()
if err != nil {
return err
}
b.Log.Info("Connection succeeded")
b.TeamID = b.mc.GetTeamId()
go b.mc.WsReceiver()
go b.mc.StatusLoop()
return nil
}
// nolint:wrapcheck
func (b *Bmattermost) apiLogin6() error {
password := b.GetString("Password")
if b.GetString("Token") != "" {
password = "token=" + b.GetString("Token")
}
b.mc6 = matterclient6.New(b.GetString("Login"), password, b.GetString("Team"), b.GetString("Server"), "")
if b.GetBool("debug") {
b.mc6.SetLogLevel("debug")
}
b.mc6.SkipTLSVerify = b.GetBool("SkipTLSVerify")
b.mc6.SkipVersionCheck = b.GetBool("SkipVersionCheck")
b.mc6.NoTLS = b.GetBool("NoTLS")
b.Log.Infof("Connecting %s (team: %s) on %s", b.GetString("Login"), b.GetString("Team"), b.GetString("Server"))
if err := b.mc6.Login(); err != nil {
if err := b.mc.Login(); err != nil {
return err
}
b.Log.Info("Connection succeeded")
b.TeamID = b.mc6.GetTeamID()
b.TeamID = b.mc.GetTeamID()
return nil
}
@ -183,6 +120,7 @@ func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
if b.GetBool("PrefixMessagesWithNick") {
msg.Text = msg.Username + msg.Text
}
if msg.Extra != nil {
// this sends a message only if we received a config.EVENT_FILE_FAILURE_SIZE
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
@ -206,7 +144,7 @@ func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
if fi.URL != "" {
msg.Text += fi.URL
msg.Text += " " + fi.URL
}
}
}
@ -233,11 +171,23 @@ func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
}
// skipMessages returns true if this message should not be handled
//
//nolint:gocyclo,cyclop
func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
// Handle join/leave
if message.Type == "system_join_leave" ||
message.Type == "system_join_channel" ||
message.Type == "system_leave_channel" {
skipJoinMessageTypes := map[string]struct{}{
"system_join_leave": {}, // deprecated for system_add_to_channel
"system_leave_channel": {}, // deprecated for system_remove_from_channel
"system_join_channel": {},
"system_add_to_channel": {},
"system_remove_from_channel": {},
"system_add_to_team": {},
"system_remove_from_team": {},
}
// dirty hack to efficiently check if this element is in the map without writing a contains func
// can be replaced with native slice.contains with go 1.21
if _, ok := skipJoinMessageTypes[message.Type]; ok {
if b.GetBool("nosendjoinpart") {
return true
}
@ -259,76 +209,7 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
}
// Handle edited messages
if (message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED) && b.GetBool("EditDisable") {
return true
}
// Ignore non-post messages
if message.Post == nil {
b.Log.Debugf("ignoring nil message.Post: %#v", message)
return true
}
// Ignore messages sent from matterbridge
if message.Post.Props != nil {
if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok {
b.Log.Debugf("sent by matterbridge, ignoring")
return true
}
}
// Ignore messages sent from a user logged in as the bot
if b.mc.User.Username == message.Username {
return true
}
// if the message has reactions don't repost it (for now, until we can correlate reaction with message)
if message.Post.HasReactions {
return true
}
// ignore messages from other teams than ours
if message.Raw.Data["team_id"].(string) != b.TeamID {
return true
}
// only handle posted, edited or deleted events
if !(message.Raw.Event == "posted" || message.Raw.Event == model.WEBSOCKET_EVENT_POST_EDITED ||
message.Raw.Event == model.WEBSOCKET_EVENT_POST_DELETED) {
return true
}
return false
}
// skipMessages returns true if this message should not be handled
// nolint:gocyclo,cyclop
func (b *Bmattermost) skipMessage6(message *matterclient6.Message) bool {
// Handle join/leave
if message.Type == "system_join_leave" ||
message.Type == "system_join_channel" ||
message.Type == "system_leave_channel" {
if b.GetBool("nosendjoinpart") {
return true
}
channelName := b.getChannelName(message.Post.ChannelId)
if channelName == "" {
channelName = message.Channel
}
b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
b.Remote <- config.Message{
Username: "system",
Text: message.Text,
Channel: channelName,
Account: b.Account,
Event: config.EventJoinLeave,
}
return true
}
// Handle edited messages
if (message.Raw.EventType() == model6.WebsocketEventPostEdited) && b.GetBool("EditDisable") {
if (message.Raw.EventType() == model.WebsocketEventPostEdited) && b.GetBool("EditDisable") {
return true
}
@ -347,7 +228,7 @@ func (b *Bmattermost) skipMessage6(message *matterclient6.Message) bool {
}
// Ignore messages sent from a user logged in as the bot
if b.mc6.User.Username == message.Username {
if b.mc.User.Username == message.Username {
b.Log.Debug("message from same user as bot, ignoring")
return true
}
@ -364,8 +245,8 @@ func (b *Bmattermost) skipMessage6(message *matterclient6.Message) bool {
}
// only handle posted, edited or deleted events
if !(message.Raw.EventType() == "posted" || message.Raw.EventType() == model6.WebsocketEventPostEdited ||
message.Raw.EventType() == model6.WebsocketEventPostDeleted) {
if !(message.Raw.EventType() == "posted" || message.Raw.EventType() == model.WebsocketEventPostEdited ||
message.Raw.EventType() == model.WebsocketEventPostDeleted) {
return true
}
return false
@ -395,11 +276,7 @@ func (b *Bmattermost) getChannelID(name string) string {
return idcheck[1]
}
if b.mc6 != nil {
return b.mc6.GetChannelID(name, b.TeamID)
}
return b.mc.GetChannelId(name, b.TeamID)
return b.mc.GetChannelID(name, b.TeamID)
}
func (b *Bmattermost) getChannelName(id string) string {

View File

@ -1,6 +1,7 @@
package bmattermost
import (
"context"
"errors"
"fmt"
"strings"
@ -9,16 +10,14 @@ import (
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterclient"
"github.com/42wim/matterbridge/matterhook"
matterclient6 "github.com/matterbridge/matterclient"
"github.com/matterbridge/matterclient"
"github.com/rs/xid"
)
type Bmattermost struct {
mh *matterhook.Client
mc *matterclient.MMClient
mc6 *matterclient6.Client
mc *matterclient.Client
v6 bool
uuid string
TeamID string
@ -52,7 +51,7 @@ func (b *Bmattermost) Connect() error {
return nil
}
if strings.HasPrefix(b.getVersion(), "6.") {
if strings.HasPrefix(b.getVersion(), "6.") || strings.HasPrefix(b.getVersion(), "7.") {
if !b.v6 {
b.v6 = true
}
@ -74,34 +73,17 @@ func (b *Bmattermost) Connect() error {
return nil
case b.GetString("Token") != "":
b.Log.Info("Connecting using token (sending and receiving)")
b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
if b.v6 {
err := b.apiLogin6()
if err != nil {
return err
}
} else {
err := b.apiLogin()
if err != nil {
return err
}
err := b.apiLogin()
if err != nil {
return err
}
go b.handleMatter()
case b.GetString("Login") != "":
b.Log.Info("Connecting using login/password (sending and receiving)")
b.Log.Infof("Using mattermost v6 methods: %t", b.v6)
if b.v6 {
err := b.apiLogin6()
if err != nil {
return err
}
} else {
err := b.apiLogin()
if err != nil {
return err
}
err := b.apiLogin()
if err != nil {
return err
}
go b.handleMatter()
}
@ -132,10 +114,6 @@ func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error {
return fmt.Errorf("Could not find channel ID for channel %s", channel.Name)
}
if b.mc6 != nil {
return b.mc6.JoinChannel(id) // nolint:wrapcheck
}
return b.mc.JoinChannel(id)
}
@ -168,9 +146,6 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
if msg.ID == "" {
return "", nil
}
if b.mc6 != nil {
return msg.ID, b.mc6.DeleteMessage(msg.ID) // nolint:wrapcheck
}
return msg.ID, b.mc.DeleteMessage(msg.ID)
}
@ -183,36 +158,20 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
// we only can reply to the root of the thread, not to a specific ID (like discord for example does)
if msg.ParentID != "" {
if b.mc6 != nil {
post, _, err := b.mc6.Client.GetPost(msg.ParentID, "")
if err != nil {
b.Log.Errorf("getting post %s failed: %s", msg.ParentID, err)
}
if post.RootId != "" {
msg.ParentID = post.RootId
}
} else {
post, res := b.mc.Client.GetPost(msg.ParentID, "")
if res.Error != nil {
b.Log.Errorf("getting post %s failed: %s", msg.ParentID, res.Error.DetailedError)
}
if post.RootId != "" {
msg.ParentID = post.RootId
}
post, _, err := b.mc.Client.GetPost(context.TODO(), msg.ParentID, "")
if err != nil {
b.Log.Errorf("getting post %s failed: %s", msg.ParentID, err)
}
if post != nil && post.RootId != "" {
msg.ParentID = post.RootId
}
}
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
if b.mc6 != nil {
if _, err := b.mc6.PostMessage(b.getChannelID(rmsg.Channel), rmsg.Username+rmsg.Text, msg.ParentID); err != nil {
b.Log.Errorf("PostMessage failed: %s", err)
}
} else {
if _, err := b.mc.PostMessage(b.getChannelID(rmsg.Channel), rmsg.Username+rmsg.Text, msg.ParentID); err != nil {
b.Log.Errorf("PostMessage failed: %s", err)
}
if _, err := b.mc.PostMessage(b.getChannelID(rmsg.Channel), rmsg.Username+rmsg.Text, msg.ParentID); err != nil {
b.Log.Errorf("PostMessage failed: %s", err)
}
}
if len(msg.Extra["file"]) > 0 {
@ -227,17 +186,9 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
// Edit message if we have an ID
if msg.ID != "" {
if b.mc6 != nil {
return b.mc6.EditMessage(msg.ID, msg.Text) // nolint:wrapcheck
}
return b.mc.EditMessage(msg.ID, msg.Text)
}
// Post normal message
if b.mc6 != nil {
return b.mc6.PostMessage(b.getChannelID(msg.Channel), msg.Text, msg.ParentID) // nolint:wrapcheck
}
return b.mc.PostMessage(b.getChannelID(msg.Channel), msg.Text, msg.ParentID)
}

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

@ -42,7 +42,14 @@ func (b *Bmumble) handleTextMessage(event *gumble.TextMessageEvent) {
if part.Image == nil {
rmsg.Text = part.Text
} 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{})
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")
@ -62,7 +69,6 @@ func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) {
}
// No need to talk or listen
event.Client.Self.SetSelfDeafened(true)
event.Client.Self.SetSelfMuted(true)
// if the Channel variable is set, this is a reconnect -> rejoin channel
if b.Channel != nil {
if err := b.doJoin(event.Client, *b.Channel); err != nil {
@ -78,19 +84,75 @@ func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) {
}
}
func (b *Bmumble) handleUserChange(event *gumble.UserChangeEvent) {
// Only care about changes to self
if event.User != event.Client.Self {
func (b *Bmumble) handleJoinLeave(event *gumble.UserChangeEvent) {
// Ignore events happening before setup is done
if b.Channel == nil {
return
}
// Someone attempted to move the user out of the configured channel; attempt to join back
if b.Channel != nil {
if b.GetBool("nosendjoinpart") {
return
}
b.Log.Debugf("Received gumble user change event: %+v", event)
text := ""
switch {
case event.Type&gumble.UserChangeKicked > 0:
text = " was kicked"
case event.Type&gumble.UserChangeBanned > 0:
text = " was banned"
case event.Type&gumble.UserChangeDisconnected > 0:
if event.User.Channel != nil && event.User.Channel.ID == *b.Channel {
text = " left"
}
case event.Type&gumble.UserChangeConnected > 0:
if event.User.Channel != nil && event.User.Channel.ID == *b.Channel {
text = " joined"
}
case event.Type&gumble.UserChangeChannel > 0:
// Treat Mumble channel changes the same as connects/disconnects; as far as matterbridge is concerned, they are identical
if event.User.Channel != nil && event.User.Channel.ID == *b.Channel {
text = " joined"
} else {
text = " left"
}
}
if text != "" {
b.Remote <- config.Message{
Username: "system",
Text: event.User.Name + text,
Channel: strconv.FormatUint(uint64(*b.Channel), 10),
Account: b.Account,
Event: config.EventJoinLeave,
}
}
}
func (b *Bmumble) handleUserModified(event *gumble.UserChangeEvent) {
// Ignore events happening before setup is done
if b.Channel == nil {
return
}
if event.Type&gumble.UserChangeChannel > 0 {
// Someone attempted to move the user out of the configured channel; attempt to join back
if err := b.doJoin(event.Client, *b.Channel); err != nil {
b.Log.Error(err)
}
}
}
func (b *Bmumble) handleUserChange(event *gumble.UserChangeEvent) {
// The UserChangeEvent is used for both the gumble client itself as well as other clients
if event.User != event.Client.Self {
// other users
b.handleJoinLeave(event)
} else {
// gumble user
b.handleUserModified(event)
}
}
func (b *Bmumble) handleDisconnect(event *gumble.DisconnectEvent) {
b.connected <- *event
}

View File

@ -93,7 +93,7 @@ func (b *Bmumble) JoinChannel(channel config.ChannelInfo) error {
func (b *Bmumble) Send(msg config.Message) (string, error) {
// Only process text messages
b.Log.Debugf("=> Received local message %#v", msg)
if msg.Event != "" && msg.Event != config.EventUserAction {
if msg.Event != "" && msg.Event != config.EventUserAction && msg.Event != config.EventJoinLeave {
return "", nil
}
@ -185,6 +185,7 @@ func (b *Bmumble) doConnect() error {
gumbleConfig.Password = password
}
registerNullCodecAsOpus()
client, err := gumble.DialWithDialer(new(net.Dialer), b.GetString("Server"), gumbleConfig, &b.tlsConfig)
if err != nil {
return err
@ -249,7 +250,12 @@ func (b *Bmumble) processMessage(msg *config.Message) {
// If there is a maximum message length, split and truncate the lines
var msgLines []string
if maxLength := b.serverConfig.MaximumMessageLength; maxLength != nil {
msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username), b.GetString("MessageClipped"))
if *maxLength != 0 { // Some servers will have unlimited message lengths.
// Not doing this makes underflows happen.
msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username), b.GetString("MessageClipped"))
} else {
msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
}
} else {
msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
}

View File

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

View File

@ -80,7 +80,7 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) {
case *slack.FileDeletedEvent:
rmsg, err := b.handleFileDeletedEvent(ev)
if err != nil {
b.Log.Errorf("%#v", err)
b.Log.Printf("%#v", err)
continue
}
messages <- rmsg
@ -282,6 +282,13 @@ func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message)
return false
}
func getMessageTitle(attach *slack.Attachment) string {
if attach.TitleLink != "" {
return fmt.Sprintf("[%s](%s)\n", attach.Title, attach.TitleLink)
}
return attach.Title
}
func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message) {
// File comments are set by the system (because there is no username given).
if ev.SubType == sFileComment {
@ -290,12 +297,15 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message)
// See if we have some text in the attachments.
if rmsg.Text == "" {
for _, attach := range ev.Attachments {
for i, attach := range ev.Attachments {
if attach.Text != "" {
if attach.Title != "" {
rmsg.Text = attach.Title + "\n"
rmsg.Text = getMessageTitle(&ev.Attachments[i])
}
rmsg.Text += attach.Text
if attach.Footer != "" {
rmsg.Text += "\n\n" + attach.Footer
}
} else {
rmsg.Text = attach.Fallback
}

View File

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

View File

@ -321,7 +321,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
}
// Upload a file if it exists.
if msg.Extra != nil {
if len(msg.Extra) > 0 {
extraMsgs := helper.HandleExtra(&msg, b.General)
for i := range extraMsgs {
rmsg := &extraMsgs[i]
@ -332,7 +332,7 @@ func (b *Bslack) sendRTM(msg config.Message) (string, error) {
}
}
// Upload files if necessary (from Slack, Telegram or Mattermost).
b.uploadFile(&msg, channelInfo.ID)
return b.uploadFile(&msg, channelInfo.ID)
}
// Post message.
@ -443,7 +443,8 @@ func (b *Bslack) postMessage(msg *config.Message, channelInfo *slack.Channel) (s
}
// uploadFile handles native upload of files
func (b *Bslack) uploadFile(msg *config.Message, channelID string) {
func (b *Bslack) uploadFile(msg *config.Message, channelID string) (string, error) {
var messageID string
for _, f := range msg.Extra["file"] {
fi, ok := f.(config.FileInfo)
if !ok {
@ -471,13 +472,22 @@ func (b *Bslack) uploadFile(msg *config.Message, channelID string) {
})
if err != nil {
b.Log.Errorf("uploadfile %#v", err)
return
return "", err
}
if res.ID != "" {
b.Log.Debugf("Adding file ID %s to cache with timestamp %s", res.ID, ts.String())
b.cache.Add("file"+res.ID, ts)
// search for message id by uploaded file in private/public channels, get thread timestamp from uploaded file
if v, ok := res.Shares.Private[channelID]; ok && len(v) > 0 {
messageID = v[0].Ts
}
if v, ok := res.Shares.Public[channelID]; ok && len(v) > 0 {
messageID = v[0].Ts
}
}
}
return messageID, nil
}
func (b *Bslack) prepareMessageOptions(msg *config.Message) []slack.MsgOption {

View File

@ -11,7 +11,7 @@ import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/davecgh/go-spew/spew"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
tgbotapi "github.com/matterbridge/telegram-bot-api/v6"
)
func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message {
@ -20,6 +20,11 @@ func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *
if posted.Text == "/chatId" {
chatID := strconv.FormatInt(posted.Chat.ID, 10)
// Handle chat topics
if posted.IsTopicMessage {
chatID = chatID + "/" + strconv.Itoa(posted.MessageThreadID)
}
_, err := b.Send(config.Message{
Channel: chatID,
Text: fmt.Sprintf("ID of this chat: %s", chatID),
@ -57,6 +62,11 @@ func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Mess
return
}
if message.ForwardFromChat != nil && message.ForwardFrom == nil {
rmsg.Text = "Forwarded from " + message.ForwardFromChat.Title + ": " + rmsg.Text
return
}
if message.ForwardFrom == nil {
rmsg.Text = "Forwarded from " + unknownUser + ": " + rmsg.Text
return
@ -66,6 +76,9 @@ func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Mess
if b.GetBool("UseFirstName") {
usernameForward = message.ForwardFrom.FirstName
}
if b.GetBool("UseFullName") {
usernameForward = message.ForwardFrom.FirstName + " " + message.ForwardFrom.LastName
}
if usernameForward == "" {
usernameForward = message.ForwardFrom.UserName
@ -83,12 +96,16 @@ func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Mess
// handleQuoting handles quoting of previous messages
func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Message) {
if message.ReplyToMessage != nil {
// Used to check if the message was a reply to the root topic
if message.ReplyToMessage != nil && (!message.IsTopicMessage || message.ReplyToMessage.MessageID != message.MessageThreadID) { //nolint:nestif
usernameReply := ""
if message.ReplyToMessage.From != nil {
if b.GetBool("UseFirstName") {
usernameReply = message.ReplyToMessage.From.FirstName
}
if b.GetBool("UseFullName") {
usernameReply = message.ReplyToMessage.From.FirstName + " " + message.ReplyToMessage.From.LastName
}
if usernameReply == "" {
usernameReply = message.ReplyToMessage.From.UserName
if usernameReply == "" {
@ -100,7 +117,11 @@ func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Messag
usernameReply = unknownUser
}
if !b.GetBool("QuoteDisable") {
rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, message.ReplyToMessage.Text)
quote := message.ReplyToMessage.Text
if quote == "" {
quote = message.ReplyToMessage.Caption
}
rmsg.Text = b.handleQuote(rmsg.Text, usernameReply, quote)
}
}
}
@ -112,6 +133,11 @@ func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Messa
if b.GetBool("UseFirstName") {
rmsg.Username = message.From.FirstName
}
if b.GetBool("UseFullName") {
if message.From.FirstName != "" && message.From.LastName != "" {
rmsg.Username = message.From.FirstName + " " + message.From.LastName
}
}
if rmsg.Username == "" {
rmsg.Username = message.From.UserName
if rmsg.Username == "" {
@ -129,6 +155,11 @@ func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Messa
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
@ -143,6 +174,11 @@ func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Messa
}
}
// Fallback on author signature (used in "channel" type of chat)
if rmsg.Username == "" && message.AuthorSignature != "" {
rmsg.Username = message.AuthorSignature
}
// if we really didn't find a username, set it to unknown
if rmsg.Username == "" {
rmsg.Username = unknownUser
@ -155,7 +191,7 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
if update.Message == nil && update.ChannelPost == nil &&
update.EditedMessage == nil && update.EditedChannelPost == nil {
b.Log.Error("Getting nil messages, this shouldn't happen.")
b.Log.Info("Received event without messages, skipping.")
continue
}
@ -163,6 +199,8 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
spew.Dump(update.Message)
}
b.handleGroupUpdate(update)
var message *tgbotapi.Message
rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})}
@ -181,6 +219,19 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
// set the ID's from the channel or group message
rmsg.ID = strconv.Itoa(message.MessageID)
rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)
if message.IsTopicMessage {
rmsg.Channel += "/" + strconv.Itoa(message.MessageThreadID)
}
// preserve threading from telegram reply
if message.ReplyToMessage != nil &&
// Used to check if the message was a reply to the root topic
(!message.IsTopicMessage || message.ReplyToMessage.MessageID != message.MessageThreadID) {
rmsg.ParentID = strconv.Itoa(message.ReplyToMessage.MessageID)
}
// handle entities (adding URLs)
b.handleEntities(&rmsg, message)
// handle username
b.handleUsername(&rmsg, message)
@ -197,11 +248,9 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
// quote the previous message
b.handleQuoting(&rmsg, message)
// handle entities (adding URLs)
b.handleEntities(&rmsg, message)
if rmsg.Text != "" || len(rmsg.Extra) > 0 {
rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
// Comment the next line out due to avoid removing empty lines in Telegram
// rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
// channels don't have (always?) user information. see #410
if message.From != nil {
rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.FormatInt(message.From.ID, 10), b.General)
@ -214,6 +263,50 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
}
}
func (b *Btelegram) handleGroupUpdate(update tgbotapi.Update) {
if msg := update.Message; msg != nil {
switch {
case msg.NewChatMembers != nil:
b.handleUserJoin(update)
case msg.LeftChatMember != nil:
b.handleUserLeave(update)
}
}
}
func (b *Btelegram) handleUserJoin(update tgbotapi.Update) {
msg := update.Message
for _, user := range msg.NewChatMembers {
rmsg := config.Message{
UserID: strconv.FormatInt(user.ID, 10),
Username: user.FirstName, // for some reason all the other name felids are empty on this event (at least for me)
Channel: strconv.FormatInt(msg.Chat.ID, 10),
Account: b.Account,
Protocol: b.Protocol,
Event: config.EventJoinLeave,
Text: "joined chat",
}
b.Remote <- rmsg
}
}
func (b *Btelegram) handleUserLeave(update tgbotapi.Update) {
msg := update.Message
user := msg.LeftChatMember
rmsg := config.Message{
UserID: strconv.FormatInt(user.ID, 10),
Username: user.FirstName, // for some reason all the other name felids are empty on this event (at least for me)
Channel: strconv.FormatInt(msg.Chat.ID, 10),
Account: b.Account,
Protocol: b.Protocol,
Event: config.EventJoinLeave,
Text: "left chat",
}
b.Remote <- rmsg
}
// handleDownloadAvatar downloads the avatar of userid from channel
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
// logs an error message if it fails
@ -290,12 +383,12 @@ func (b *Btelegram) maybeConvertWebp(name *string, data *[]byte) {
// handleDownloadFile handles file download
func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error {
size := 0
size := int64(0)
var url, name, text string
switch {
case message.Sticker != nil:
text, name, url = b.getDownloadInfo(message.Sticker.FileID, ".webp", true)
size = message.Sticker.FileSize
size = int64(message.Sticker.FileSize)
case message.Voice != nil:
text, name, url = b.getDownloadInfo(message.Voice.FileID, ".ogg", true)
size = message.Voice.FileSize
@ -312,7 +405,7 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa
text = " " + message.Document.FileName + " : " + url
case message.Photo != nil:
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)
}
@ -358,7 +451,7 @@ func (b *Btelegram) getDownloadInfo(id string, suffix string, urlpart bool) (str
urlPart := strings.Split(url, "/")
name = urlPart[len(urlPart)-1]
}
if suffix != "" && !strings.HasSuffix(name, suffix) {
if suffix != "" && !strings.HasSuffix(name, suffix) && !strings.HasSuffix(name, ".webm") {
name += suffix
}
text := " " + url
@ -377,7 +470,7 @@ func (b *Btelegram) handleDelete(msg *config.Message, chatid int64) (string, err
}
cfg := tgbotapi.NewDeleteMessage(chatid, msgid)
_, err = b.c.Send(cfg)
_, err = b.c.Request(cfg)
return "", err
}
@ -416,42 +509,57 @@ func (b *Btelegram) handleEdit(msg *config.Message, chatid int64) (string, error
}
// handleUploadFile handles native upload of files
func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64) string {
var c tgbotapi.Chattable
func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64, threadid int, parentID int) (string, error) {
var media []interface{}
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
file := tgbotapi.FileBytes{
Name: fi.Name,
Bytes: *fi.Data,
}
if b.GetString("MessageFormat") == HTMLFormat {
fi.Comment = makeHTML(html.EscapeString(fi.Comment))
}
switch filepath.Ext(fi.Name) {
case ".jpg", ".jpe", ".png":
pc := tgbotapi.NewPhoto(chatid, file)
pc.Caption, pc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
c = pc
pc := tgbotapi.NewInputMediaPhoto(file)
if fi.Comment != "" {
pc.Caption, pc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
}
media = append(media, pc)
case ".mp4", ".m4v":
vc := tgbotapi.NewVideo(chatid, file)
vc.Caption, vc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
c = vc
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.NewAudio(chatid, file)
ac.Caption, ac.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
c = ac
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)
c = voc
voc.ReplyToMessageID = parentID
res, err := b.c.Send(voc)
if err != nil {
return "", err
}
return strconv.Itoa(res.MessageID), nil
default:
dc := tgbotapi.NewDocument(chatid, file)
dc.Caption, dc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
c = dc
}
_, err := b.c.Send(c)
if err != nil {
b.Log.Errorf("file upload failed: %#v", err)
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 {
@ -480,34 +588,59 @@ func (b *Btelegram) handleEntities(rmsg *config.Message, message *tgbotapi.Messa
}
indexMovedBy := 0
prevLinkOffset := -1
// for now only do URL replacements
for _, e := range message.Entities {
asRunes := utf16.Encode([]rune(rmsg.Text))
if e.Type == "text_link" {
offset := e.Offset + indexMovedBy
url, err := e.ParseURL()
if err != nil {
b.Log.Errorf("entity text_link url parse failed: %s", err)
continue
}
utfEncodedString := utf16.Encode([]rune(rmsg.Text))
if e.Offset+e.Length > len(utfEncodedString) {
b.Log.Errorf("entity length is too long %d > %d", e.Offset+e.Length, len(utfEncodedString))
if offset+e.Length > len(utfEncodedString) {
b.Log.Errorf("entity length is too long %d > %d", offset+e.Length, len(utfEncodedString))
continue
}
link := utf16.Decode(utfEncodedString[e.Offset : e.Offset+e.Length])
rmsg.Text = strings.Replace(rmsg.Text, string(link), url.String(), 1)
rmsg.Text = string(utf16.Decode(asRunes[:offset+e.Length])) + " (" + url.String() + ")" + string(utf16.Decode(asRunes[offset+e.Length:]))
indexMovedBy += len(url.String()) + 3
prevLinkOffset = e.Offset
}
if e.Offset == prevLinkOffset {
continue
}
if e.Type == "code" {
offset := e.Offset + indexMovedBy
rmsg.Text = rmsg.Text[:offset] + "`" + rmsg.Text[offset:offset+e.Length] + "`" + rmsg.Text[offset+e.Length:]
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 = rmsg.Text[:offset] + "```\n" + rmsg.Text[offset:offset+e.Length] + "\n```" + rmsg.Text[offset+e.Length:]
rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "```\n" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "```\n" + string(utf16.Decode(asRunes[offset+e.Length:]))
indexMovedBy += 8
}
if e.Type == "bold" {
offset := e.Offset + indexMovedBy
rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "*" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "*" + string(utf16.Decode(asRunes[offset+e.Length:]))
indexMovedBy += 2
}
if e.Type == "italic" {
offset := e.Offset + indexMovedBy
rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "_" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "_" + string(utf16.Decode(asRunes[offset+e.Length:]))
indexMovedBy += 2
}
if e.Type == "strike" {
offset := e.Offset + indexMovedBy
rmsg.Text = string(utf16.Decode(asRunes[:offset])) + "~" + string(utf16.Decode(asRunes[offset:offset+e.Length])) + "~" + string(utf16.Decode(asRunes[offset+e.Length:]))
indexMovedBy += 2
}
}
}

View File

@ -2,7 +2,6 @@ package btelegram
import (
"bytes"
"html"
"github.com/russross/blackfriday"
)
@ -24,10 +23,16 @@ func (options *customHTML) Paragraph(out *bytes.Buffer, text func() bool) {
func (options *customHTML) BlockCode(out *bytes.Buffer, text []byte, lang string) {
out.WriteString("<pre>")
out.WriteString(html.EscapeString(string(text)))
out.WriteString(string(text))
out.WriteString("</pre>\n")
}
func (options *customHTML) CodeSpan(out *bytes.Buffer, text []byte) {
out.WriteString("<code>")
out.WriteString(string(text))
out.WriteString("</code>")
}
func (options *customHTML) Header(out *bytes.Buffer, text func() bool, level int, id string) {
options.Paragraph(out, text)
}
@ -42,6 +47,10 @@ func (options *customHTML) BlockQuote(out *bytes.Buffer, text []byte) {
out.WriteByte('\n')
}
func (options *customHTML) LineBreak(out *bytes.Buffer) {
out.WriteByte('\n')
}
func (options *customHTML) List(out *bytes.Buffer, text func() bool, flags int) {
options.Paragraph(out, text)
}

View File

@ -1,6 +1,7 @@
package btelegram
import (
"fmt"
"html"
"log"
"strconv"
@ -9,7 +10,7 @@ import (
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
tgbotapi "github.com/matterbridge/telegram-bot-api/v6"
)
const (
@ -85,11 +86,41 @@ func TGGetParseMode(b *Btelegram, username string, text string) (textout string,
return textout, parsemode
}
func (b *Btelegram) getIds(channel string) (int64, int, error) {
var chatid int64
topicid := 0
// get the chatid
if strings.Contains(channel, "/") { //nolint:nestif
s := strings.Split(channel, "/")
if len(s) < 2 {
b.Log.Errorf("Invalid channel format: %#v\n", channel)
return 0, 0, nil
}
id, err := strconv.ParseInt(s[0], 10, 64)
if err != nil {
return 0, 0, err
}
chatid = id
tid, err := strconv.Atoi(s[1])
if err != nil {
return 0, 0, err
}
topicid = tid
} else {
id, err := strconv.ParseInt(channel, 10, 64)
if err != nil {
return 0, 0, err
}
chatid = id
}
return chatid, topicid, nil
}
func (b *Btelegram) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg)
// get the chatid
chatid, err := strconv.ParseInt(msg.Channel, 10, 64)
chatid, topicid, err := b.getIds(msg.Channel)
if err != nil {
return "", err
}
@ -100,7 +131,7 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
}
if b.GetString("MessageFormat") == HTMLFormat {
msg.Text = makeHTML(msg.Text)
msg.Text = makeHTML(html.EscapeString(msg.Text))
}
// Delete message
@ -108,16 +139,27 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
return b.handleDelete(&msg, chatid)
}
// Handle prefix hint for unthreaded messages.
if msg.ParentNotFound() {
msg.ParentID = ""
msg.Text = fmt.Sprintf("[reply]: %s", msg.Text)
}
var parentID int
if msg.ParentID != "" {
parentID, _ = b.intParentID(msg.ParentID)
}
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
if _, msgErr := b.sendMessage(chatid, rmsg.Username, rmsg.Text); msgErr != nil {
if _, msgErr := b.sendMessage(chatid, topicid, rmsg.Username, rmsg.Text, parentID); msgErr != nil {
b.Log.Errorf("sendMessage failed: %s", msgErr)
}
}
// check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 {
b.handleUploadFile(&msg, chatid)
return b.handleUploadFile(&msg, chatid, topicid, parentID)
}
}
@ -131,7 +173,7 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
// Ignore empty text field needs for prevent double messages from whatsapp to telegram
// when sending media with text caption
if msg.Text != "" {
return b.sendMessage(chatid, msg.Username, msg.Text)
return b.sendMessage(chatid, topicid, msg.Username, msg.Text, parentID)
}
return "", nil
@ -145,10 +187,13 @@ func (b *Btelegram) getFileDirectURL(id string) string {
return res
}
func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, error) {
func (b *Btelegram) sendMessage(chatid int64, topicid int, username, text string, parentID int) (string, error) {
m := tgbotapi.NewMessage(chatid, "")
m.Text, m.ParseMode = TGGetParseMode(b, username, text)
if topicid != 0 {
m.BaseChat.MessageThreadID = topicid
}
m.ReplyToMessageID = parentID
m.DisableWebPagePreview = b.GetBool("DisableWebPagePreview")
res, err := b.c.Send(m)
@ -158,6 +203,37 @@ func (b *Btelegram) sendMessage(chatid int64, username, text string) (string, er
return strconv.Itoa(res.MessageID), nil
}
// sendMediaFiles native upload media files via media group
func (b *Btelegram) sendMediaFiles(msg *config.Message, chatid int64, threadid int, parentID int, media []interface{}) (string, error) {
if len(media) == 0 {
return "", nil
}
mg := tgbotapi.MediaGroupConfig{
BaseChat: tgbotapi.BaseChat{
ChatID: chatid,
MessageThreadID: threadid,
ChannelUsername: msg.Username,
ReplyToMessageID: parentID,
},
Media: media,
}
messages, err := b.c.SendMediaGroup(mg)
if err != nil {
return "", err
}
// return first message id
return strconv.Itoa(messages[0].MessageID), nil
}
// intParentID return integer parent id for telegram message
func (b *Btelegram) intParentID(parentID string) (int, error) {
pid, err := strconv.Atoi(parentID)
if err != nil {
return 0, err
}
return pid, nil
}
func (b *Btelegram) cacheAvatar(msg *config.Message) (string, error) {
fi := msg.Extra["file"][0].(config.FileInfo)
/* if we have a sha we have successfully uploaded the file to the media server,

View File

@ -64,7 +64,7 @@ func (b *Bvk) Connect() error {
go func() {
err := b.lp.Run()
if err != nil {
b.Log.Fatal("Enable longpoll in group management")
b.Log.WithError(err).Fatal("Enable longpoll in group management")
}
}()
@ -223,7 +223,7 @@ func (b *Bvk) uploadFiles(extra map[string][]interface{}, peerID int) (string, s
}
a, err := b.uploadFile(fi, peerID)
if err != nil {
b.Log.Error("File upload error ", fi.Name)
b.Log.WithError(err).Error("File upload error ", fi.Name)
}
attachments = append(attachments, a)
@ -237,7 +237,8 @@ func (b *Bvk) uploadFile(file config.FileInfo, peerID int) (string, error) {
photoRE := regexp.MustCompile(".(jpg|jpe|png)$")
if photoRE.MatchString(file.Name) {
p, err := b.c.UploadMessagesPhoto(peerID, r)
// BUG(VK): for community chat peerID=0
p, err := b.c.UploadMessagesPhoto(0, r)
if err != nil {
return "", err
}

View File

@ -1,3 +1,4 @@
// nolint:goconst
package bwhatsapp
import (
@ -134,6 +135,7 @@ func (b *Bwhatsapp) HandleTextMessage(message whatsapp.TextMessage) {
}
// HandleImageMessage sent from WhatsApp, relay it to the brige
// nolint:funlen
func (b *Bwhatsapp) HandleImageMessage(message whatsapp.ImageMessage) {
if message.Info.FromMe || message.Info.Timestamp < b.startedAt {
return

View File

@ -111,8 +111,7 @@ func (b *Bwhatsapp) getSenderName(senderJid string) string {
}
// try to reload this contact
_, err := b.conn.Contacts()
if err != nil {
if _, err := b.conn.Contacts(); err != nil {
b.Log.Errorf("error on update of contacts: %v", err)
}

View File

@ -40,6 +40,11 @@ type Bwhatsapp struct {
func New(cfg *bridge.Config) bridge.Bridger {
number := cfg.GetString(cfgNumber)
cfg.Log.Warn("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
cfg.Log.Warn("This bridge is deprecated and not supported anymore. Use the new multidevice whatsapp bridge")
cfg.Log.Warn("See https://github.com/42wim/matterbridge#building-with-whatsapp-beta-multidevice-support for more info")
cfg.Log.Warn("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!")
if number == "" {
cfg.Log.Fatalf("Missing configuration for WhatsApp bridge: Number")
}
@ -293,7 +298,11 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
if msg.ID != "" {
b.Log.Debugf("updating message with id %s", msg.ID)
msg.Text += " (edited)"
if b.GetString("editsuffix") != "" {
msg.Text += b.GetString("EditSuffix")
} else {
msg.Text += " (edited)"
}
}
// Handle Upload a file

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

@ -14,6 +14,7 @@ import (
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
lru "github.com/hashicorp/golang-lru"
"github.com/jpillora/backoff"
"github.com/matterbridge/go-xmpp"
"github.com/rs/xid"
@ -28,13 +29,20 @@ type Bxmpp struct {
connected bool
sync.RWMutex
StanzaIDs *lru.Cache
OriginIDs *lru.Cache
avatarAvailability map[string]bool
avatarMap map[string]string
}
func New(cfg *bridge.Config) bridge.Bridger {
stanzaIDs, _ := lru.New(5000)
originIDs, _ := lru.New(5000)
return &Bxmpp{
Config: cfg,
StanzaIDs: stanzaIDs,
OriginIDs: originIDs,
xmppMap: make(map[string]string),
avatarAvailability: make(map[string]bool),
avatarMap: make(map[string]string),
@ -124,12 +132,20 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
return "", nil
}
if msg.ParentNotFound() {
msg.ParentID = ""
}
// Post normal message.
var msgReplaceID string
msgID := xid.New().String()
if msg.ID != "" {
msgReplaceID = msg.ID
}
var replyID string
if res, ok := b.StanzaIDs.Get(msg.ParentID); ok {
replyID, _ = res.(string)
}
b.Log.Debugf("=> Sending message %#v", msg)
if _, err := b.xc.Send(xmpp.Chat{
Type: "groupchat",
@ -137,6 +153,7 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
Text: msg.Username + msg.Text,
ID: msgID,
ReplaceID: msgReplaceID,
ReplyID: replyID,
}); err != nil {
return "", err
}
@ -297,6 +314,11 @@ func (b *Bxmpp) handleXMPP() error {
if v.Type == "groupchat" {
b.Log.Debugf("== Receiving %#v", v)
if v.ID != "" && v.StanzaID != "" {
b.StanzaIDs.Add(v.ID, v.StanzaID)
b.OriginIDs.Add(v.StanzaID, v.ID)
}
// Skip invalid messages.
if b.skipMessage(v) {
continue
@ -321,6 +343,12 @@ func (b *Bxmpp) handleXMPP() error {
if v.ReplaceID != "" {
msgID = v.ReplaceID
}
var parentID string
if res, ok := b.OriginIDs.Get(v.ReplyID); ok {
parentID, _ = res.(string)
}
rmsg := config.Message{
Username: b.parseNick(v.Remote),
Text: v.Text,
@ -328,6 +356,7 @@ func (b *Bxmpp) handleXMPP() error {
Account: b.Account,
Avatar: avatar,
UserID: v.Remote,
ParentID: parentID,
ID: msgID,
Event: event,
}

View File

@ -106,30 +106,31 @@ func (b *Bzulip) getChannel(id int) string {
func (b *Bzulip) handleQueue() error {
for {
messages, err := b.q.GetEvents()
switch err {
case gzb.BackoffError:
time.Sleep(time.Second * 5)
case gzb.NoJSONError:
b.Log.Error("Response wasn't JSON, server down or restarting? sleeping 10 seconds")
time.Sleep(time.Second * 10)
case gzb.BadEventQueueError:
b.Log.Info("got a bad event queue id error, reconnecting")
b.bot.Queues = nil
for {
b.q, err = b.bot.RegisterAll()
if err != nil {
b.Log.Errorf("reconnecting failed: %s. Sleeping 10 seconds", err)
time.Sleep(time.Second * 10)
}
break
}
case gzb.HeartbeatError:
b.Log.Debug("heartbeat received.")
default:
b.Log.Debugf("receiving error: %#v", err)
time.Sleep(time.Second * 10)
}
if err != nil {
switch err {
case gzb.BackoffError:
time.Sleep(time.Second * 5)
case gzb.NoJSONError:
b.Log.Error("Response wasn't JSON, server down or restarting? sleeping 10 seconds")
time.Sleep(time.Second * 10)
case gzb.BadEventQueueError:
b.Log.Info("got a bad event queue id error, reconnecting")
b.bot.Queues = nil
for {
b.q, err = b.bot.RegisterAll()
if err != nil {
b.Log.Errorf("reconnecting failed: %s. Sleeping 10 seconds", err)
time.Sleep(time.Second * 10)
}
break
}
case gzb.HeartbeatError:
b.Log.Debug("heartbeat received.")
default:
b.Log.Debugf("receiving error: %#v", err)
time.Sleep(time.Second * 10)
}
continue
}
for _, m := range messages {

View File

@ -1,3 +1,154 @@
# v1.26.0
## New features
- irc: Allow substitution of bot's nick in RunCommands (irc) (#1890)
- matrix: Add Matrix username spoofing (#1875)
## Enhancements
- general: Update dependencies (#1951)
- mattermost: Remove mattermost 5 support (#1936)
- mumble: Implement sending of EventJoinLeave both to and from Mumble (#1915)
- whatsappmulti: Improve attachment handling (whatsapp) (#1928)
- whatsappmulti: Handle incoming document captions from whatsapp (#1935)
## Bugfix
- irc: Fix empty messages on IRC (#1897)
- telegram: Fix message html entities escaping when sending to Telegram (#1855)
- telegram/slack: Fix error messages in telegram and slack bridges (#1862)
- telegram: Fix telegram attachment comment formatting and escaping (#1920)
- telegram: Make the cgo lottie a build tag (-tag cgolottie) (#1955)
- whatsappmulti: Update dependencies and fix whatsmeow API changes (#1887)
- whatsappmulti: Fix the "Someone" nickname problem (whatsapp) (#1931)
This release couldn't exist without the following contributors:
@s3lph, @sas1024, @Glandos, @jx11r, @Lucki, @BuckarooBanzay, @ilmaisin, @Kufat
# v1.25.2
## Enhancements
- general: Update dependencies (#1851,#1841)
- mattermost: Support mattermost v7.x (#1852)
## Bugfix
- discord: Fix Unwanted join notifications from one Discord server to another (#1612)
- discord: Ignore events from other guilds, add nosendjoinpart support (#1846)
This release couldn't exist without the following contributors:
@wlcx
# v1.25.1
## Enhancements
- matrix: Add KeepQuotedReply option for matrix to fix regression (#1823)
- slack: Improve Slack attachments formatting (slack) (#1807)
## Bugfix
- general: Update dependencies (#1813,#1822,#1833)
- mattermost: Add space between filename and URL (mattermost). Fixes #1820
- matrix: Update matterbridge/gomatrix. Fixes #1772 (#1803)
- telegram: Do not modify .webm files (telegram). Fixes #17**88 (#1802)
- telegram: Do not apply any markup to URL entities (telegram) (#1808)
- telegram: Fix telegram message deletion request (#1818)
- vk: Fix UploadMessagesPhoto for vk community chat (vk) (#1812)
This release couldn't exist without the following contributors:
@bd808, @chugunov, @sas1024, @SevereCloud, @ValdikSS
# v1.25.0
## Breaking changes
- whatsapp: deprecated, the library <https://github.com/Rhymen/go-whatsapp> isn't maintained anymore.
We're switching to <https://github.com/tulir/whatsmeow> but as this uses a GPL3 licensed library we can't provide you with binaries.
You'll need to build it yourself. More information about this can be found here: <https://github.com/42wim/matterbridge#building-with-whatsapp-beta-multidevice-support>
## New features
- whatsappmulti: whatsapp multidevice support added - more info <https://github.com/42wim/matterbridge#building-with-whatsapp-beta-multidevice-support>
- general: Add Dockerfile_whatsappmulti for building with WhatsApp Multi-Device support (Whatsmeow) (#1774)
- telegram: Add UseFullName option (telegram) (#1777)
- slack: Use slack real name as user name (slack) (#1775)
## Enhancements
- general: Ignore sending file with comment, if comment contains IgnoreMessages value (#1783)
- general: Update dependencies (#1784)
- irc: Update lrstanley/girc dep (#1773)
- slack: Preserve threading for messages with files (slack) (#1781)
- telegram: Preserve threading from telegram replies (telegram) (#1776)
- telegram: Multiple media in one message (telegram) (#1779)
- whatsapp: Add whatsapp deprecation warning (#1792)
## Bugfix
- discord: Change discord non-native threading behaviour (discord) (#1791)
This release couldn't exist without the following contributors:
@sas1024, @tpxtron
# v1.24.1
## Enhancements
- discord: Switch to discordgo upstream again (#1759)
- general: Update dependencies and vendor (#1761)
- general: Create inmessage-logger.tengo (#1688) (#1747)
- general: Add OpenRC service file (#1746)
- irc: Refactor utf-8 conversion (irc) (#1767)
## Bugfixes
- irc: Fix panic in irc. Closes #1751 (#1760)
- mumble: Implement a workaround to signal Opus support (mumble) (#1764)
- telegram: Fix for complex-formatted Telegram text (#1765)
- telegram: Fix Telegram channel title in forwards (#1753)
- telegram: Fix Telegram Problem (unforwarded formatting and skipping of linebreaks) (#1749)
This release couldn't exist without the following contributors:
@s3lph, @ValdikSS, @reckel-jm, @CyberTailor
# v1.24.0
## New features
- harmony: new protocol added: Add support for Harmony (#1656)
- irc: Allow binding to IP on IRC (#1640)
- irc: Add support for client certificate (irc) (#1710)
- mattermost: Add UseUsername option (mattermost). Fixes #1665 (#1714)
- mattermost: Add support for using ID in channel config (mattermost) (#1715)
- matrix: Reply support for Matrix (#1664)
- telegram: Add Telegram Bot Command /chatId (telegram) (#1703)
## Enhancements
- general: Update dependencies/vendor (#1659)
- discord: Add more debug options for discord (#1712)
- docker: Use Alpine stable again in Dockerfile (#1643)
- mattermost: Log eventtype in debug (mattermost) (#1676)
- mattermost: Add more ignore debug messages (mattermost) (#1678)
- slack: Add support for deleting files from slack to discord. Fixes #1705 (#1709)
- telegram: Add support for code blocks in telegram (#1650)
- telegram: Update telegram-bot-api to v5 (#1660)
- telegram: Add comments to messages (telegram) (#1652)
- telegram: Add support for sender_chat (telegram) (#1677)
- vk: Remove GroupID (vk) (#1668)
## Bugfix
- mattermost: Use current parentID if rootId is not set (mattermost) (#1675)
- matrix: Make HTMLDisable work correct (matrix) (#1716)
- whatsapp: Make EditSuffix option actually work (whatsapp). Fixes #1510 (#1728)
This release couldn't exist without the following contributors:
@DavyJohnesev, @GoliathLabs, @pontaoski, @PeGaSuS-Coder, @dependabot[bot], @vpzomtrrfrt, @SevereCloud, @soloam, @YashRE42, @danwalmsley, @SuperSandro2000, @inzanity
# v1.23.2
If you're running whatsapp you should update.
@ -6,6 +157,9 @@ If you're running whatsapp you should update.
- whatsapp: Update go-whatsapp version (#1630)
This release couldn't exist without the following contributors:
@snikpic
# v1.23.1
If you're running mattermost 6 you should update.
@ -20,6 +174,9 @@ If you're running mattermost 6 you should update.
- xmpp: Use a new msgID when replacing messages (xmpp). Fixes #1584 (#1623)
- zulip: Add better error handling on Zulip (#1589)
This release couldn't exist without the following contributors:
@Polynomdivision, @minecraftchest1, @alexmv
# v1.23.0
## New features

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

View File

@ -1,4 +1,5 @@
// +build !nowhatsapp
// +build !whatsappmulti
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

@ -299,13 +299,30 @@ func (gw *Gateway) ignoreMessage(msg *config.Message) bool {
igNicks := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreNicks"))
igMessages := strings.Fields(gw.Bridges[msg.Account].GetString("IgnoreMessages"))
if gw.ignoreTextEmpty(msg) || gw.ignoreText(msg.Username, igNicks) || gw.ignoreText(msg.Text, igMessages) {
if gw.ignoreTextEmpty(msg) || gw.ignoreText(msg.Username, igNicks) || gw.ignoreText(msg.Text, igMessages) || gw.ignoreFilesComment(msg.Extra, igMessages) {
return true
}
return false
}
// ignoreFilesComment returns true if we need to ignore a file with matched comment.
func (gw *Gateway) ignoreFilesComment(extra map[string][]interface{}, igMessages []string) bool {
if extra == nil {
return false
}
for _, f := range extra["file"] {
fi, ok := f.(config.FileInfo)
if !ok {
continue
}
if gw.ignoreText(fi.Comment, igMessages) {
return true
}
}
return false
}
func (gw *Gateway) modifyUsername(msg *config.Message, dest *bridge.Bridge) string {
if dest.GetBool("StripNick") {
re := regexp.MustCompile("[^a-zA-Z0-9]+")

View File

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

194
go.mod
View File

@ -1,141 +1,153 @@
module github.com/42wim/matterbridge
require (
github.com/42wim/go-gitter v0.0.0-20170828205020-017310c2d557
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f
github.com/Benau/tgsconverter v0.0.0-20210809170556-99f4a4f6337f
github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560
github.com/Rhymen/go-whatsapp v0.1.2-0.20211102134409-31a2e740845c
github.com/SevereCloud/vksdk/v2 v2.13.0
github.com/d5/tengo/v2 v2.10.0
github.com/davecgh/go-spew v1.1.1
github.com/fsnotify/fsnotify v1.5.1
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/gomarkdown/markdown v0.0.0-20211207152620-5d6539fd8bfc
github.com/google/gops v0.3.22
github.com/gorilla/schema v1.2.0
github.com/gorilla/websocket v1.4.2
github.com/harmony-development/shibshib v0.0.0-20211127182844-512296f7c548
github.com/hashicorp/golang-lru v0.5.4
github.com/SevereCloud/vksdk/v2 v2.17.0
github.com/bwmarrin/discordgo v0.28.1
github.com/d5/tengo/v2 v2.17.0
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/fsnotify/fsnotify v1.7.0
github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2
github.com/google/gops v0.3.27
github.com/gorilla/schema v1.4.1
github.com/harmony-development/shibshib v0.0.0-20220101224523-c98059d09cfa
github.com/hashicorp/golang-lru v1.0.2
github.com/jpillora/backoff v1.0.0
github.com/keybase/go-keybase-chat-bot v0.0.0-20211201215354-ee4b23828b55
github.com/kyokomi/emoji/v2 v2.2.8
github.com/labstack/echo/v4 v4.6.3
github.com/lrstanley/girc v0.0.0-20211023233735-147f0ff77566
github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16
github.com/keybase/go-keybase-chat-bot v0.0.0-20221220212439-e48d9abd2c20
github.com/kyokomi/emoji/v2 v2.2.13
github.com/labstack/echo/v4 v4.12.0
github.com/lrstanley/girc v0.0.0-20240823210506-80555f2adb03
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20211016222428-79310a412696
github.com/matterbridge/discordgo v0.21.2-0.20210201201054-fb39a175b4f7
github.com/matterbridge/go-xmpp v0.0.0-20211030125215-791a06c5f1be
github.com/matterbridge/go-xmpp v0.0.0-20240523230155-7154bfeb76e8
github.com/matterbridge/gomatrix v0.0.0-20220411225302-271e5088ea27
github.com/matterbridge/gozulipbot v0.0.0-20211023205727-a19d6c1f3b75
github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba
github.com/matterbridge/matterclient v0.0.0-20211107234719-faca3cd42315
github.com/mattermost/mattermost-server/v5 v5.39.3
github.com/mattermost/mattermost-server/v6 v6.3.0
github.com/matterbridge/matterclient v0.0.0-20240817214420-3d4c3aef3dc1
github.com/matterbridge/telegram-bot-api/v6 v6.5.0
github.com/mattermost/mattermost/server/public v0.1.6
github.com/mattn/godown v0.0.1
github.com/missdeer/golib v1.0.4
github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9
github.com/mdp/qrterminal v1.0.1
github.com/mitchellh/mapstructure v1.5.0
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/rs/xid v1.3.0
github.com/rs/xid v1.5.0
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/sirupsen/logrus v1.8.1
github.com/slack-go/slack v0.10.0
github.com/spf13/viper v1.10.1
github.com/stretchr/testify v1.7.0
github.com/sirupsen/logrus v1.9.3
github.com/slack-go/slack v0.14.0
github.com/spf13/viper v1.19.0
github.com/stretchr/testify v1.9.0
github.com/vincent-petithory/dataurl v1.0.0
github.com/writeas/go-strip-markdown v2.0.1+incompatible
github.com/yaegashi/msgraph.go v0.1.4
github.com/zfjagann/golang-ring v0.0.0-20210116075443-7c86fdb43134
golang.org/x/image v0.0.0-20211028202545-6944b10bf410
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
github.com/zfjagann/golang-ring v0.0.0-20220330170733-19bcea1b6289
go.mau.fi/whatsmeow v0.0.0-20240821142752-3d63c6fcc1a7
golang.org/x/image v0.19.0
golang.org/x/oauth2 v0.22.0
golang.org/x/text v0.17.0
gomod.garykim.dev/nc-talk v0.3.0
gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376
layeh.com/gumble v0.0.0-20200818122324-146f9205029b
google.golang.org/protobuf v1.34.2
layeh.com/gumble v0.0.0-20221205141517-d1df60a3cc14
modernc.org/sqlite v1.32.0
)
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 v3.5.1+incompatible // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/dyatlov/go-opengraph v0.0.0-20210112100619-dae8665a5b09 // 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.3 // 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.2 // indirect
github.com/google/uuid v1.3.0 // 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/json-iterator/go v1.1.12 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/kettek/apng v0.0.0-20191108220231-414630eed80f // indirect
github.com/klauspost/compress v1.14.1 // indirect
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
github.com/labstack/gommon v0.3.1 // indirect
github.com/magiconair/properties v1.8.5 // 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-20201202150706-ee0e6284187d // indirect
github.com/mattermost/logr v1.0.13 // indirect
github.com/mattermost/logr/v2 v2.0.15 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // 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/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.0.16 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/monaco-io/request v1.0.5 // indirect
github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d // indirect
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // 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.4 // indirect
github.com/philhofer/fwd v1.1.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.0 // 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.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-20200617195104-da1b6568686e // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // 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.2.0 // indirect
github.com/tinylib/msgp v1.1.6 // 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.1 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // 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/cfg v1.0.2 // indirect
github.com/wiggin77/merror v1.0.3 // indirect
github.com/wiggin77/merror v1.0.5 // indirect
github.com/wiggin77/srslog v1.0.1 // indirect
go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.7.0 // indirect
go.uber.org/zap v1.17.0 // indirect
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 // indirect
golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9 // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // 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.0-20210107192922-496545a6307b // 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
//replace github.com/matrix-org/gomatrix => github.com/matterbridge/gomatrix v0.0.0-20220205235239-607eb9ee6419
go 1.17
go 1.22.0

2418
go.sum

File diff suppressed because it is too large Load Diff

View File

@ -65,7 +65,7 @@ func main() {
if err = r.Start(); err != nil {
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 {}
}

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

@ -122,10 +122,11 @@ RejoinDelay=0
#Only works in IRC right now.
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
#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.
#You can use s for second, m for minute
@ -162,7 +163,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#see replacemessages for syntax
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
@ -187,7 +188,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#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)
ShowJoinPart=false
@ -197,7 +198,7 @@ ShowJoinPart=false
VerboseJoinPart=false
#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)
NoSendJoinPart=false
@ -300,7 +301,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ]
#Nicks you want to replace.
#See ReplaceMessages for syntaxA
#See ReplaceMessages for syntax
#OPTIONAL (default empty)
ReplaceNicks=[ ["user--","user"] ]
@ -324,7 +325,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#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)
ShowJoinPart=false
@ -467,7 +468,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#see replacemessages for syntax
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
@ -491,12 +492,12 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#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)
ShowJoinPart=false
#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)
NoSendJoinPart=false
@ -512,86 +513,9 @@ ShowTopicChange=false
###################################################################
#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
@ -662,7 +586,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#see replacemessages for syntax
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
@ -686,7 +610,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#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)
ShowJoinPart=false
@ -801,7 +725,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#see replacemessages for syntax
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
@ -825,12 +749,12 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#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)
ShowJoinPart=false
#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)
NoSendJoinPart=false
@ -862,6 +786,10 @@ ShowUserTyping=false
#Default "<clipped message>"
MessageClipped="<clipped message>"
#If enabled use the slack "Real Name" as username.
#OPTIONAL (default false)
UseFullName=false
###################################################################
#discord section
###################################################################
@ -997,10 +925,17 @@ ShowTopicChange=false
# Supported from the following bridges: slack
SyncTopic=false
#Message to show when a message is too big
#Default "<clipped message>"
# 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
###################################################################
@ -1036,6 +971,12 @@ DisableWebPagePreview=false
#OPTIONAL (default 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
#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.
@ -1099,7 +1040,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#see replacemessages for syntax
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
@ -1127,7 +1068,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#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)
ShowJoinPart=false
@ -1141,6 +1082,12 @@ StripNick=false
#OPTIONAL (default 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
###################################################################
@ -1234,7 +1181,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#see replacemessages for syntax
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
@ -1258,7 +1205,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#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)
ShowJoinPart=false
@ -1312,6 +1259,15 @@ HTMLDisable=false
# UseUserName shows the username instead of the server nickname
UseUserName=false
# 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.
# But if you for example use mattermost or discord with webhooks you'll need to enable
# this (and keep PreserveThreading disabled) 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
KeepQuotedReply=false
#Nicks you want to ignore.
#Regular expressions supported
#Messages from those users will not be sent to other bridges.
@ -1337,7 +1293,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#see replacemessages for syntax
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
@ -1361,10 +1317,15 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#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)
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
#It will strip other characters from the nick
#OPTIONAL (default false)
@ -1429,7 +1390,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#see replacemessages for syntax
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
@ -1453,7 +1414,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#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)
ShowJoinPart=false
@ -1541,6 +1502,15 @@ SkipTLSVerify=false
#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
###################################################################
@ -1629,7 +1599,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace.
#see replacemessages for syntaxa
#see replacemessages for syntax
#optional (default empty)
ReplaceNicks=[ ["user--","user"] ]
@ -1653,7 +1623,7 @@ Label=""
RemoteNickFormat="[{PROTOCOL}] <{NICK}> "
#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)
ShowJoinPart=false
@ -1916,6 +1886,7 @@ enable=true
# 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
# | 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
# -------------------------------------------------------------------------------------------------------------------------------------

View File

@ -1,7 +1,7 @@
#WARNING: as this file contains credentials, be sure to set correct file permissions
[irc]
[irc.freenode]
Server="irc.freenode.net:6667"
[irc.libera]
Server="irc.libera.chat:6667"
Nick="matterbot"
[mattermost]
@ -17,7 +17,7 @@
name="gateway1"
enable=true
[[gateway.inout]]
account="irc.freenode"
account="irc.libera"
channel="#testing"
[[gateway.inout]]
@ -29,6 +29,6 @@ enable=true
#name="gateway2"
#enable=true
#inout = [
# { account="irc.freenode", channel="#testing", options={key="channelkey"}},
# { account="irc.libera", channel="#testing", options={key="channelkey"}},
# { 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 && rmsg.Raw.Event != model.WEBSOCKET_EVENT_POST_DELETED {
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, "", true)
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, true)
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
}

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
}

View File

@ -8,6 +8,7 @@ package field
import (
"crypto/subtle"
"encoding/binary"
"errors"
"math/bits"
)
@ -92,7 +93,7 @@ func (v *Element) Add(a, b *Element) *Element {
// 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. TODO
// propagation.
return v.carryPropagateGeneric()
}
@ -186,14 +187,17 @@ func (v *Element) Set(a *Element) *Element {
return v
}
// SetBytes sets v to x, which must be a 32-byte little-endian encoding.
// 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.
func (v *Element) SetBytes(x []byte) *Element {
// 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 {
panic("edwards25519: invalid field element input size")
return nil, errors.New("edwards25519: invalid field element input size")
}
// Bits 0:51 (bytes 0:8, bits 0:64, shift 0, mask 51).
@ -208,12 +212,12 @@ func (v *Element) SetBytes(x []byte) *Element {
// 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:251 (bytes 24:32, bits 192:256, shift 12, mask 51).
// 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
return v, nil
}
// Bytes returns the canonical 32-byte little-endian encoding of v.
@ -391,26 +395,26 @@ var sqrtM1 = &Element{1718705420411056, 234908883556509,
// 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) (rr *Element, wasSquare int) {
var a, b Element
func (r *Element) SqrtRatio(u, v *Element) (R *Element, wasSquare int) {
t0 := new(Element)
// r = (u * v3) * (u * v7)^((p-5)/8)
v2 := a.Square(v)
uv3 := b.Multiply(u, b.Multiply(v2, v))
uv7 := a.Multiply(uv3, a.Square(v2))
r.Multiply(uv3, r.Pow22523(uv7))
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 := a.Multiply(v, a.Square(r)) // check = v * r^2
check := new(Element).Multiply(v, t0.Square(rr)) // check = v * r^2
uNeg := b.Negate(u)
uNeg := new(Element).Negate(u)
correctSignSqrt := check.Equal(u)
flippedSignSqrt := check.Equal(uNeg)
flippedSignSqrtI := check.Equal(uNeg.Multiply(uNeg, sqrtM1))
flippedSignSqrtI := check.Equal(t0.Multiply(uNeg, sqrtM1))
rPrime := b.Multiply(r, sqrtM1) // r_prime = SQRT_M1 * r
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)
r.Select(rPrime, r, flippedSignSqrt|flippedSignSqrtI)
rr.Select(rPrime, rr, flippedSignSqrt|flippedSignSqrtI)
r.Absolute(r) // Choose the nonnegative square root.
r.Absolute(rr) // Choose the nonnegative square root.
return r, correctSignSqrt | flippedSignSqrt
}

View File

@ -1,13 +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)

View File

@ -3,7 +3,6 @@
// license that can be found in the LICENSE file.
//go:build arm64 && gc && !purego
// +build arm64,gc,!purego
#include "textflag.h"

50
vendor/filippo.io/edwards25519/field/fe_extra.go generated vendored Normal file
View File

@ -0,0 +1,50 @@
// 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 field
import "errors"
// This file contains additional functionality that is not included in the
// upstream crypto/ed25519/edwards25519/field package.
// SetWideBytes sets v to x, where x is a 64-byte little-endian encoding, which
// is reduced modulo the field order. If x is not of the right length,
// SetWideBytes returns nil and an error, and the receiver is unchanged.
//
// SetWideBytes is not necessary to select a uniformly distributed value, and is
// only provided for compatibility: SetBytes can be used instead as the chance
// of bias is less than 2⁻²⁵⁰.
func (v *Element) SetWideBytes(x []byte) (*Element, error) {
if len(x) != 64 {
return nil, errors.New("edwards25519: invalid SetWideBytes input size")
}
// Split the 64 bytes into two elements, and extract the most significant
// bit of each, which is ignored by SetBytes.
lo, _ := new(Element).SetBytes(x[:32])
loMSB := uint64(x[31] >> 7)
hi, _ := new(Element).SetBytes(x[32:])
hiMSB := uint64(x[63] >> 7)
// The output we want is
//
// v = lo + loMSB * 2²⁵⁵ + hi * 2²⁵⁶ + hiMSB * 2⁵¹¹
//
// which applying the reduction identity comes out to
//
// v = lo + loMSB * 19 + hi * 2 * 19 + hiMSB * 2 * 19²
//
// l0 will be the sum of a 52 bits value (lo.l0), plus a 5 bits value
// (loMSB * 19), a 6 bits value (hi.l0 * 2 * 19), and a 10 bits value
// (hiMSB * 2 * 19²), so it fits in a uint64.
v.l0 = lo.l0 + loMSB*19 + hi.l0*2*19 + hiMSB*2*19*19
v.l1 = lo.l1 + hi.l1*2*19
v.l2 = lo.l2 + hi.l2*2*19
v.l3 = lo.l3 + hi.l3*2*19
v.l4 = lo.l4 + hi.l4*2*19
return v.carryPropagate(), nil
}

View File

@ -156,7 +156,7 @@ func feMulGeneric(v, a, b *Element) {
rr4 := r4.lo&maskLow51Bits + c3
// Now all coefficients fit into 64-bit registers but are still too large to
// be passed around as a Element. We therefore do one last carry chain,
// be passed around as an Element. We therefore do one last carry chain,
// where the carries will be small enough to fit in the wiggle room above 2⁵¹.
*v = Element{rr0, rr1, rr2, rr3, rr4}
v.carryPropagate()
@ -245,8 +245,8 @@ func feSquareGeneric(v, a *Element) {
v.carryPropagate()
}
// carryPropagate brings the limbs below 52 bits by applying the reduction
// identity (a * 2²⁵⁵ + b = a * 19 + b) to the l4 carry. TODO inline
// carryPropagateGeneric brings the limbs below 52 bits by applying the reduction
// identity (a * 2²⁵⁵ + b = a * 19 + b) to the l4 carry.
func (v *Element) carryPropagateGeneric() *Element {
c0 := v.l0 >> 51
c1 := v.l1 >> 51
@ -254,6 +254,8 @@ func (v *Element) carryPropagateGeneric() *Element {
c3 := v.l3 >> 51
c4 := v.l4 >> 51
// c4 is at most 64 - 51 = 13 bits, so c4*19 is at most 18 bits, and
// the final l0 will be at most 52 bits. Similarly for the rest.
v.l0 = v.l0&maskLow51Bits + c4*19
v.l1 = v.l1&maskLow51Bits + c0
v.l2 = v.l2&maskLow51Bits + c1

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

@ -0,0 +1,343 @@
// Copyright (c) 2016 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 (
"encoding/binary"
"errors"
)
// A Scalar is an integer modulo
//
// l = 2^252 + 27742317777372353535851937790883648493
//
// which is the prime order of the edwards25519 group.
//
// 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 Scalar struct {
// s is the scalar in the Montgomery domain, in the format of the
// fiat-crypto implementation.
s fiatScalarMontgomeryDomainFieldElement
}
// The field implementation in scalar_fiat.go is generated by the fiat-crypto
// project (https://github.com/mit-plv/fiat-crypto) at version v0.0.9 (23d2dbc)
// from a formally verified model.
//
// fiat-crypto code comes under the following license.
//
// Copyright (c) 2015-2020 The fiat-crypto 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:
//
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// THIS SOFTWARE IS PROVIDED BY the fiat-crypto authors "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 Berkeley Software Design,
// Inc. 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.
//
// NewScalar returns a new zero Scalar.
func NewScalar() *Scalar {
return &Scalar{}
}
// MultiplyAdd sets s = x * y + z mod l, and returns s. It is equivalent to
// using Multiply and then Add.
func (s *Scalar) MultiplyAdd(x, y, z *Scalar) *Scalar {
// Make a copy of z in case it aliases s.
zCopy := new(Scalar).Set(z)
return s.Multiply(x, y).Add(s, zCopy)
}
// Add sets s = x + y mod l, and returns s.
func (s *Scalar) Add(x, y *Scalar) *Scalar {
// s = 1 * x + y mod l
fiatScalarAdd(&s.s, &x.s, &y.s)
return s
}
// Subtract sets s = x - y mod l, and returns s.
func (s *Scalar) Subtract(x, y *Scalar) *Scalar {
// s = -1 * y + x mod l
fiatScalarSub(&s.s, &x.s, &y.s)
return s
}
// Negate sets s = -x mod l, and returns s.
func (s *Scalar) Negate(x *Scalar) *Scalar {
// s = -1 * x + 0 mod l
fiatScalarOpp(&s.s, &x.s)
return s
}
// Multiply sets s = x * y mod l, and returns s.
func (s *Scalar) Multiply(x, y *Scalar) *Scalar {
// s = x * y + 0 mod l
fiatScalarMul(&s.s, &x.s, &y.s)
return s
}
// Set sets s = x, and returns s.
func (s *Scalar) Set(x *Scalar) *Scalar {
*s = *x
return s
}
// SetUniformBytes sets s = x mod l, where x is a 64-byte little-endian integer.
// If x is not of the right length, SetUniformBytes returns nil and an error,
// and the receiver is unchanged.
//
// SetUniformBytes can be used to set s to a uniformly distributed value given
// 64 uniformly distributed random bytes.
func (s *Scalar) SetUniformBytes(x []byte) (*Scalar, error) {
if len(x) != 64 {
return nil, errors.New("edwards25519: invalid SetUniformBytes input length")
}
// We have a value x of 512 bits, but our fiatScalarFromBytes function
// expects an input lower than l, which is a little over 252 bits.
//
// Instead of writing a reduction function that operates on wider inputs, we
// can interpret x as the sum of three shorter values a, b, and c.
//
// x = a + b * 2^168 + c * 2^336 mod l
//
// We then precompute 2^168 and 2^336 modulo l, and perform the reduction
// with two multiplications and two additions.
s.setShortBytes(x[:21])
t := new(Scalar).setShortBytes(x[21:42])
s.Add(s, t.Multiply(t, scalarTwo168))
t.setShortBytes(x[42:])
s.Add(s, t.Multiply(t, scalarTwo336))
return s, nil
}
// scalarTwo168 and scalarTwo336 are 2^168 and 2^336 modulo l, encoded as a
// fiatScalarMontgomeryDomainFieldElement, which is a little-endian 4-limb value
// in the 2^256 Montgomery domain.
var scalarTwo168 = &Scalar{s: [4]uint64{0x5b8ab432eac74798, 0x38afddd6de59d5d7,
0xa2c131b399411b7c, 0x6329a7ed9ce5a30}}
var scalarTwo336 = &Scalar{s: [4]uint64{0xbd3d108e2b35ecc5, 0x5c3a3718bdf9c90b,
0x63aa97a331b4f2ee, 0x3d217f5be65cb5c}}
// setShortBytes sets s = x mod l, where x is a little-endian integer shorter
// than 32 bytes.
func (s *Scalar) setShortBytes(x []byte) *Scalar {
if len(x) >= 32 {
panic("edwards25519: internal error: setShortBytes called with a long string")
}
var buf [32]byte
copy(buf[:], x)
fiatScalarFromBytes((*[4]uint64)(&s.s), &buf)
fiatScalarToMontgomery(&s.s, (*fiatScalarNonMontgomeryDomainFieldElement)(&s.s))
return s
}
// SetCanonicalBytes sets s = x, where x is a 32-byte little-endian encoding of
// s, and returns s. If x is not a canonical encoding of s, SetCanonicalBytes
// returns nil and an error, and the receiver is unchanged.
func (s *Scalar) SetCanonicalBytes(x []byte) (*Scalar, error) {
if len(x) != 32 {
return nil, errors.New("invalid scalar length")
}
if !isReduced(x) {
return nil, errors.New("invalid scalar encoding")
}
fiatScalarFromBytes((*[4]uint64)(&s.s), (*[32]byte)(x))
fiatScalarToMontgomery(&s.s, (*fiatScalarNonMontgomeryDomainFieldElement)(&s.s))
return s, nil
}
// scalarMinusOneBytes is l - 1 in little endian.
var scalarMinusOneBytes = [32]byte{236, 211, 245, 92, 26, 99, 18, 88, 214, 156, 247, 162, 222, 249, 222, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16}
// isReduced returns whether the given scalar in 32-byte little endian encoded
// form is reduced modulo l.
func isReduced(s []byte) bool {
if len(s) != 32 {
return false
}
for i := len(s) - 1; i >= 0; i-- {
switch {
case s[i] > scalarMinusOneBytes[i]:
return false
case s[i] < scalarMinusOneBytes[i]:
return true
}
}
return true
}
// SetBytesWithClamping applies the buffer pruning described in RFC 8032,
// Section 5.1.5 (also known as clamping) and sets s to the result. The input
// must be 32 bytes, and it is not modified. If x is not of the right length,
// SetBytesWithClamping returns nil and an error, and the receiver is unchanged.
//
// Note that since Scalar values are always reduced modulo the prime order of
// the curve, the resulting value will not preserve any of the cofactor-clearing
// properties that clamping is meant to provide. It will however work as
// expected as long as it is applied to points on the prime order subgroup, like
// in Ed25519. In fact, it is lost to history why RFC 8032 adopted the
// irrelevant RFC 7748 clamping, but it is now required for compatibility.
func (s *Scalar) SetBytesWithClamping(x []byte) (*Scalar, error) {
// The description above omits the purpose of the high bits of the clamping
// for brevity, but those are also lost to reductions, and are also
// irrelevant to edwards25519 as they protect against a specific
// implementation bug that was once observed in a generic Montgomery ladder.
if len(x) != 32 {
return nil, errors.New("edwards25519: invalid SetBytesWithClamping input length")
}
// We need to use the wide reduction from SetUniformBytes, since clamping
// sets the 2^254 bit, making the value higher than the order.
var wideBytes [64]byte
copy(wideBytes[:], x[:])
wideBytes[0] &= 248
wideBytes[31] &= 63
wideBytes[31] |= 64
return s.SetUniformBytes(wideBytes[:])
}
// Bytes returns the canonical 32-byte little-endian encoding of s.
func (s *Scalar) Bytes() []byte {
// This function is outlined to make the allocations inline in the caller
// rather than happen on the heap.
var encoded [32]byte
return s.bytes(&encoded)
}
func (s *Scalar) bytes(out *[32]byte) []byte {
var ss fiatScalarNonMontgomeryDomainFieldElement
fiatScalarFromMontgomery(&ss, &s.s)
fiatScalarToBytes(out, (*[4]uint64)(&ss))
return out[:]
}
// Equal returns 1 if s and t are equal, and 0 otherwise.
func (s *Scalar) Equal(t *Scalar) int {
var diff fiatScalarMontgomeryDomainFieldElement
fiatScalarSub(&diff, &s.s, &t.s)
var nonzero uint64
fiatScalarNonzero(&nonzero, (*[4]uint64)(&diff))
nonzero |= nonzero >> 32
nonzero |= nonzero >> 16
nonzero |= nonzero >> 8
nonzero |= nonzero >> 4
nonzero |= nonzero >> 2
nonzero |= nonzero >> 1
return int(^nonzero) & 1
}
// nonAdjacentForm computes a width-w non-adjacent form for this scalar.
//
// w must be between 2 and 8, or nonAdjacentForm will panic.
func (s *Scalar) nonAdjacentForm(w uint) [256]int8 {
// This implementation is adapted from the one
// in curve25519-dalek and is documented there:
// https://github.com/dalek-cryptography/curve25519-dalek/blob/f630041af28e9a405255f98a8a93adca18e4315b/src/scalar.rs#L800-L871
b := s.Bytes()
if b[31] > 127 {
panic("scalar has high bit set illegally")
}
if w < 2 {
panic("w must be at least 2 by the definition of NAF")
} else if w > 8 {
panic("NAF digits must fit in int8")
}
var naf [256]int8
var digits [5]uint64
for i := 0; i < 4; i++ {
digits[i] = binary.LittleEndian.Uint64(b[i*8:])
}
width := uint64(1 << w)
windowMask := uint64(width - 1)
pos := uint(0)
carry := uint64(0)
for pos < 256 {
indexU64 := pos / 64
indexBit := pos % 64
var bitBuf uint64
if indexBit < 64-w {
// This window's bits are contained in a single u64
bitBuf = digits[indexU64] >> indexBit
} else {
// Combine the current 64 bits with bits from the next 64
bitBuf = (digits[indexU64] >> indexBit) | (digits[1+indexU64] << (64 - indexBit))
}
// Add carry into the current window
window := carry + (bitBuf & windowMask)
if window&1 == 0 {
// If the window value is even, preserve the carry and continue.
// Why is the carry preserved?
// If carry == 0 and window & 1 == 0,
// then the next carry should be 0
// If carry == 1 and window & 1 == 0,
// then bit_buf & 1 == 1 so the next carry should be 1
pos += 1
continue
}
if window < width/2 {
carry = 0
naf[pos] = int8(window)
} else {
carry = 1
naf[pos] = int8(window) - int8(width)
}
pos += w
}
return naf
}
func (s *Scalar) signedRadix16() [64]int8 {
b := s.Bytes()
if b[31] > 127 {
panic("scalar has high bit set illegally")
}
var digits [64]int8
// Compute unsigned radix-16 digits:
for i := 0; i < 32; i++ {
digits[2*i] = int8(b[i] & 15)
digits[2*i+1] = int8((b[i] >> 4) & 15)
}
// Recenter coefficients:
for i := 0; i < 63; i++ {
carry := (digits[i] + 8) >> 4
digits[i] -= carry << 4
digits[i+1] += carry
}
return digits
}

1147
vendor/filippo.io/edwards25519/scalar_fiat.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

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

@ -0,0 +1,214 @@
// 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.
package edwards25519
import "sync"
// basepointTable is a set of 32 affineLookupTables, where table i is generated
// from 256i * basepoint. It is precomputed the first time it's used.
func basepointTable() *[32]affineLookupTable {
basepointTablePrecomp.initOnce.Do(func() {
p := NewGeneratorPoint()
for i := 0; i < 32; i++ {
basepointTablePrecomp.table[i].FromP3(p)
for j := 0; j < 8; j++ {
p.Add(p, p)
}
}
})
return &basepointTablePrecomp.table
}
var basepointTablePrecomp struct {
table [32]affineLookupTable
initOnce sync.Once
}
// ScalarBaseMult sets v = x * B, where B is the canonical generator, and
// returns v.
//
// The scalar multiplication is done in constant time.
func (v *Point) ScalarBaseMult(x *Scalar) *Point {
basepointTable := basepointTable()
// Write x = sum(x_i * 16^i) so x*B = sum( B*x_i*16^i )
// as described in the Ed25519 paper
//
// Group even and odd coefficients
// x*B = x_0*16^0*B + x_2*16^2*B + ... + x_62*16^62*B
// + x_1*16^1*B + x_3*16^3*B + ... + x_63*16^63*B
// x*B = x_0*16^0*B + x_2*16^2*B + ... + x_62*16^62*B
// + 16*( x_1*16^0*B + x_3*16^2*B + ... + x_63*16^62*B)
//
// We use a lookup table for each i to get x_i*16^(2*i)*B
// and do four doublings to multiply by 16.
digits := x.signedRadix16()
multiple := &affineCached{}
tmp1 := &projP1xP1{}
tmp2 := &projP2{}
// Accumulate the odd components first
v.Set(NewIdentityPoint())
for i := 1; i < 64; i += 2 {
basepointTable[i/2].SelectInto(multiple, digits[i])
tmp1.AddAffine(v, multiple)
v.fromP1xP1(tmp1)
}
// Multiply by 16
tmp2.FromP3(v) // tmp2 = v in P2 coords
tmp1.Double(tmp2) // tmp1 = 2*v in P1xP1 coords
tmp2.FromP1xP1(tmp1) // tmp2 = 2*v in P2 coords
tmp1.Double(tmp2) // tmp1 = 4*v in P1xP1 coords
tmp2.FromP1xP1(tmp1) // tmp2 = 4*v in P2 coords
tmp1.Double(tmp2) // tmp1 = 8*v in P1xP1 coords
tmp2.FromP1xP1(tmp1) // tmp2 = 8*v in P2 coords
tmp1.Double(tmp2) // tmp1 = 16*v in P1xP1 coords
v.fromP1xP1(tmp1) // now v = 16*(odd components)
// Accumulate the even components
for i := 0; i < 64; i += 2 {
basepointTable[i/2].SelectInto(multiple, digits[i])
tmp1.AddAffine(v, multiple)
v.fromP1xP1(tmp1)
}
return v
}
// ScalarMult sets v = x * q, and returns v.
//
// The scalar multiplication is done in constant time.
func (v *Point) ScalarMult(x *Scalar, q *Point) *Point {
checkInitialized(q)
var table projLookupTable
table.FromP3(q)
// Write x = sum(x_i * 16^i)
// so x*Q = sum( Q*x_i*16^i )
// = Q*x_0 + 16*(Q*x_1 + 16*( ... + Q*x_63) ... )
// <------compute inside out---------
//
// We use the lookup table to get the x_i*Q values
// and do four doublings to compute 16*Q
digits := x.signedRadix16()
// Unwrap first loop iteration to save computing 16*identity
multiple := &projCached{}
tmp1 := &projP1xP1{}
tmp2 := &projP2{}
table.SelectInto(multiple, digits[63])
v.Set(NewIdentityPoint())
tmp1.Add(v, multiple) // tmp1 = x_63*Q in P1xP1 coords
for i := 62; i >= 0; i-- {
tmp2.FromP1xP1(tmp1) // tmp2 = (prev) in P2 coords
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
table.SelectInto(multiple, digits[i])
tmp1.Add(v, multiple) // tmp1 = x_i*Q + 16*(prev) in P1xP1 coords
}
v.fromP1xP1(tmp1)
return v
}
// basepointNafTable is the nafLookupTable8 for the basepoint.
// It is precomputed the first time it's used.
func basepointNafTable() *nafLookupTable8 {
basepointNafTablePrecomp.initOnce.Do(func() {
basepointNafTablePrecomp.table.FromP3(NewGeneratorPoint())
})
return &basepointNafTablePrecomp.table
}
var basepointNafTablePrecomp struct {
table nafLookupTable8
initOnce sync.Once
}
// VarTimeDoubleScalarBaseMult sets v = a * A + b * B, where B is the canonical
// generator, and returns v.
//
// Execution time depends on the inputs.
func (v *Point) VarTimeDoubleScalarBaseMult(a *Scalar, A *Point, b *Scalar) *Point {
checkInitialized(A)
// Similarly to the single variable-base approach, we compute
// digits and use them with a lookup table. However, because
// we are allowed to do variable-time operations, we don't
// need constant-time lookups or constant-time digit
// computations.
//
// So we use a non-adjacent form of some width w instead of
// radix 16. This is like a binary representation (one digit
// for each binary place) but we allow the digits to grow in
// magnitude up to 2^{w-1} so that the nonzero digits are as
// sparse as possible. Intuitively, this "condenses" the
// "mass" of the scalar onto sparse coefficients (meaning
// fewer additions).
basepointNafTable := basepointNafTable()
var aTable nafLookupTable5
aTable.FromP3(A)
// Because the basepoint is fixed, we can use a wider NAF
// corresponding to a bigger table.
aNaf := a.nonAdjacentForm(5)
bNaf := b.nonAdjacentForm(8)
// Find the first nonzero coefficient.
i := 255
for j := i; j >= 0; j-- {
if aNaf[j] != 0 || bNaf[j] != 0 {
break
}
}
multA := &projCached{}
multB := &affineCached{}
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.
for ; i >= 0; i-- {
tmp1.Double(tmp2)
// Only update v if we have a nonzero coeff to add in.
if aNaf[i] > 0 {
v.fromP1xP1(tmp1)
aTable.SelectInto(multA, aNaf[i])
tmp1.Add(v, multA)
} else if aNaf[i] < 0 {
v.fromP1xP1(tmp1)
aTable.SelectInto(multA, -aNaf[i])
tmp1.Sub(v, multA)
}
if bNaf[i] > 0 {
v.fromP1xP1(tmp1)
basepointNafTable.SelectInto(multB, bNaf[i])
tmp1.AddAffine(v, multB)
} else if bNaf[i] < 0 {
v.fromP1xP1(tmp1)
basepointNafTable.SelectInto(multB, -bNaf[i])
tmp1.SubAffine(v, multB)
}
tmp2.FromP1xP1(tmp1)
}
v.fromP2(tmp2)
return v
}

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

@ -0,0 +1,129 @@
// 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.
package edwards25519
import (
"crypto/subtle"
)
// A dynamic lookup table for variable-base, constant-time scalar muls.
type projLookupTable struct {
points [8]projCached
}
// A precomputed lookup table for fixed-base, constant-time scalar muls.
type affineLookupTable struct {
points [8]affineCached
}
// A dynamic lookup table for variable-base, variable-time scalar muls.
type nafLookupTable5 struct {
points [8]projCached
}
// A precomputed lookup table for fixed-base, variable-time scalar muls.
type nafLookupTable8 struct {
points [64]affineCached
}
// Constructors.
// Builds a lookup table at runtime. Fast.
func (v *projLookupTable) FromP3(q *Point) {
// Goal: v.points[i] = (i+1)*Q, i.e., Q, 2Q, ..., 8Q
// This allows lookup of -8Q, ..., -Q, 0, Q, ..., 8Q
v.points[0].FromP3(q)
tmpP3 := Point{}
tmpP1xP1 := projP1xP1{}
for i := 0; i < 7; i++ {
// Compute (i+1)*Q as Q + i*Q and convert to a projCached
// This is needlessly complicated because the API has explicit
// receivers instead of creating stack objects and relying on RVO
v.points[i+1].FromP3(tmpP3.fromP1xP1(tmpP1xP1.Add(q, &v.points[i])))
}
}
// This is not optimised for speed; fixed-base tables should be precomputed.
func (v *affineLookupTable) FromP3(q *Point) {
// Goal: v.points[i] = (i+1)*Q, i.e., Q, 2Q, ..., 8Q
// This allows lookup of -8Q, ..., -Q, 0, Q, ..., 8Q
v.points[0].FromP3(q)
tmpP3 := Point{}
tmpP1xP1 := projP1xP1{}
for i := 0; i < 7; i++ {
// Compute (i+1)*Q as Q + i*Q and convert to affineCached
v.points[i+1].FromP3(tmpP3.fromP1xP1(tmpP1xP1.AddAffine(q, &v.points[i])))
}
}
// Builds a lookup table at runtime. Fast.
func (v *nafLookupTable5) FromP3(q *Point) {
// Goal: v.points[i] = (2*i+1)*Q, i.e., Q, 3Q, 5Q, ..., 15Q
// This allows lookup of -15Q, ..., -3Q, -Q, 0, Q, 3Q, ..., 15Q
v.points[0].FromP3(q)
q2 := Point{}
q2.Add(q, q)
tmpP3 := Point{}
tmpP1xP1 := projP1xP1{}
for i := 0; i < 7; i++ {
v.points[i+1].FromP3(tmpP3.fromP1xP1(tmpP1xP1.Add(&q2, &v.points[i])))
}
}
// This is not optimised for speed; fixed-base tables should be precomputed.
func (v *nafLookupTable8) FromP3(q *Point) {
v.points[0].FromP3(q)
q2 := Point{}
q2.Add(q, q)
tmpP3 := Point{}
tmpP1xP1 := projP1xP1{}
for i := 0; i < 63; i++ {
v.points[i+1].FromP3(tmpP3.fromP1xP1(tmpP1xP1.AddAffine(&q2, &v.points[i])))
}
}
// Selectors.
// Set dest to x*Q, where -8 <= x <= 8, in constant time.
func (v *projLookupTable) SelectInto(dest *projCached, x int8) {
// Compute xabs = |x|
xmask := x >> 7
xabs := uint8((x + xmask) ^ xmask)
dest.Zero()
for j := 1; j <= 8; j++ {
// Set dest = j*Q if |x| = j
cond := subtle.ConstantTimeByteEq(xabs, uint8(j))
dest.Select(&v.points[j-1], dest, cond)
}
// Now dest = |x|*Q, conditionally negate to get x*Q
dest.CondNeg(int(xmask & 1))
}
// Set dest to x*Q, where -8 <= x <= 8, in constant time.
func (v *affineLookupTable) SelectInto(dest *affineCached, x int8) {
// Compute xabs = |x|
xmask := x >> 7
xabs := uint8((x + xmask) ^ xmask)
dest.Zero()
for j := 1; j <= 8; j++ {
// Set dest = j*Q if |x| = j
cond := subtle.ConstantTimeByteEq(xabs, uint8(j))
dest.Select(&v.points[j-1], dest, cond)
}
// Now dest = |x|*Q, conditionally negate to get x*Q
dest.CondNeg(int(xmask & 1))
}
// Given odd x with 0 < x < 2^4, return x*Q (in variable time).
func (v *nafLookupTable5) SelectInto(dest *projCached, x int8) {
*dest = v.points[x/2]
}
// Given odd x with 0 < x < 2^7, return x*Q (in variable time).
func (v *nafLookupTable8) SelectInto(dest *affineCached, x int8) {
*dest = v.points[x/2]
}

View File

@ -1,3 +0,0 @@
.idea
/test
app.yaml

View File

@ -1,154 +0,0 @@
# gitter
Gitter API in Go
https://developer.gitter.im
#### Install
`go get github.com/sromku/go-gitter`
- [Initialize](#initialize)
- [Users](#users)
- [Rooms](#rooms)
- [Messages](#messages)
- [Stream](#stream)
- [Faye (Experimental)](#faye-experimental)
- [Debug](#debug)
- [App Engine](#app-engine)
##### Initialize
``` Go
api := gitter.New("YOUR_ACCESS_TOKEN")
```
##### Users
- Get current user
``` Go
user, err := api.GetUser()
```
##### Rooms
- Get all rooms
``` Go
rooms, err := api.GetRooms()
```
- Get room by id
``` Go
room, err := api.GetRoom("roomID")
```
- Get rooms of some user
``` Go
rooms, err := api.GetRooms("userID")
```
- Join room
``` Go
room, err := api.JoinRoom("roomID", "userID")
```
- Leave room
``` Go
room, err := api.LeaveRoom("roomID", "userID")
```
- Get room id
``` Go
id, err := api.GetRoomId("room/uri")
```
- Search gitter rooms
``` Go
rooms, err := api.SearchRooms("search/string")
```
##### Messages
- Get messages of room
``` Go
messages, err := api.GetMessages("roomID", nil)
```
- Get one message
``` Go
message, err := api.GetMessage("roomID", "messageID")
```
- Send message
``` Go
err := api.SendMessage("roomID", "free chat text")
```
##### Stream
Create stream to the room and start listening to incoming messages
``` Go
stream := api.Stream(room.Id)
go api.Listen(stream)
for {
event := <-stream.Event
switch ev := event.Data.(type) {
case *gitter.MessageReceived:
fmt.Println(ev.Message.From.Username + ": " + ev.Message.Text)
case *gitter.GitterConnectionClosed:
// connection was closed
}
}
```
Close stream connection
``` Go
stream.Close()
```
##### Faye (Experimental)
``` Go
faye := api.Faye(room.ID)
go faye.Listen()
for {
event := <-faye.Event
switch ev := event.Data.(type) {
case *gitter.MessageReceived:
fmt.Println(ev.Message.From.Username + ": " + ev.Message.Text)
case *gitter.GitterConnectionClosed: //this one is never called in Faye
// connection was closed
}
}
```
##### Debug
You can print the internal errors by enabling debug to true
``` Go
api.SetDebug(true, nil)
```
You can also define your own `io.Writer` in case you want to persist the logs somewhere.
For example keeping the errors on file
``` Go
logFile, err := os.Create("gitter.log")
api.SetDebug(true, logFile)
```
##### App Engine
Initialize app engine client and continue as usual
``` Go
c := appengine.NewContext(r)
client := urlfetch.Client(c)
api := gitter.New("YOUR_ACCESS_TOKEN")
api.SetClient(client)
```
[Documentation](https://godoc.org/github.com/sromku/go-gitter)

View File

@ -1,70 +0,0 @@
package gitter
import (
"encoding/json"
"fmt"
"github.com/mrexodia/wray"
)
type Faye struct {
endpoint string
Event chan Event
client *wray.FayeClient
gitter *Gitter
}
func (gitter *Gitter) Faye(roomID string) *Faye {
wray.RegisterTransports([]wray.Transport{
&wray.HttpTransport{
SendHook: func(data map[string]interface{}) {
if channel, ok := data["channel"]; ok && channel == "/meta/handshake" {
data["ext"] = map[string]interface{}{"token": gitter.config.token}
}
},
},
})
return &Faye{
endpoint: "/api/v1/rooms/" + roomID + "/chatMessages",
Event: make(chan Event),
client: wray.NewFayeClient(fayeBaseURL),
gitter: gitter,
}
}
func (faye *Faye) Listen() {
defer faye.destroy()
faye.client.Subscribe(faye.endpoint, false, func(message wray.Message) {
dataBytes, err := json.Marshal(message.Data["model"])
if err != nil {
fmt.Printf("JSON Marshal error: %v\n", err)
return
}
var gitterMessage Message
err = json.Unmarshal(dataBytes, &gitterMessage)
if err != nil {
fmt.Printf("JSON Unmarshal error: %v\n", err)
return
}
faye.Event <- Event{
Data: &MessageReceived{
Message: gitterMessage,
},
}
})
//TODO: this might be needed in the future
/*go func() {
for {
faye.client.Publish("/api/v1/ping2", map[string]interface{}{"reason": "ping"})
time.Sleep(60 * time.Second)
}
}()*/
faye.client.Listen()
}
func (faye *Faye) destroy() {
close(faye.Event)
}

View File

@ -1,527 +0,0 @@
// Package gitter is a Go client library for the Gitter API.
//
// Author: sromku
package gitter
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
"strconv"
"time"
"github.com/mreiferson/go-httpclient"
)
var (
apiBaseURL = "https://api.gitter.im/v1/"
streamBaseURL = "https://stream.gitter.im/v1/"
fayeBaseURL = "https://ws.gitter.im/faye"
)
type Gitter struct {
config struct {
apiBaseURL string
streamBaseURL string
token string
client *http.Client
}
debug bool
logWriter io.Writer
}
// New initializes the Gitter API client
//
// For example:
// api := gitter.New("YOUR_ACCESS_TOKEN")
func New(token string) *Gitter {
transport := &httpclient.Transport{
ConnectTimeout: 5 * time.Second,
ReadWriteTimeout: 40 * time.Second,
}
defer transport.Close()
s := &Gitter{}
s.config.apiBaseURL = apiBaseURL
s.config.streamBaseURL = streamBaseURL
s.config.token = token
s.config.client = &http.Client{
Transport: transport,
}
return s
}
// SetClient sets a custom http client. Can be useful in App Engine case.
func (gitter *Gitter) SetClient(client *http.Client) {
gitter.config.client = client
}
// GetUser returns the current user
func (gitter *Gitter) GetUser() (*User, error) {
var users []User
response, err := gitter.get(gitter.config.apiBaseURL + "user")
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &users)
if err != nil {
gitter.log(err)
return nil, err
}
if len(users) > 0 {
return &users[0], nil
}
err = APIError{What: "Failed to retrieve current user"}
gitter.log(err)
return nil, err
}
// GetUserRooms returns a list of Rooms the user is part of
func (gitter *Gitter) GetUserRooms(userID string) ([]Room, error) {
var rooms []Room
response, err := gitter.get(gitter.config.apiBaseURL + "user/" + userID + "/rooms")
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &rooms)
if err != nil {
gitter.log(err)
return nil, err
}
return rooms, nil
}
// GetRooms returns a list of rooms the current user is in
func (gitter *Gitter) GetRooms() ([]Room, error) {
var rooms []Room
response, err := gitter.get(gitter.config.apiBaseURL + "rooms")
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &rooms)
if err != nil {
gitter.log(err)
return nil, err
}
return rooms, nil
}
// GetUsersInRoom returns the users in the room with the passed id
func (gitter *Gitter) GetUsersInRoom(roomID string) ([]User, error) {
var users []User
response, err := gitter.get(gitter.config.apiBaseURL + "rooms/" + roomID + "/users")
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &users)
if err != nil {
gitter.log(err)
return nil, err
}
return users, nil
}
// GetRoom returns a room with the passed id
func (gitter *Gitter) GetRoom(roomID string) (*Room, error) {
var room Room
response, err := gitter.get(gitter.config.apiBaseURL + "rooms/" + roomID)
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &room)
if err != nil {
gitter.log(err)
return nil, err
}
return &room, nil
}
// GetMessages returns a list of messages in a room.
// Pagination is optional. You can pass nil or specific pagination params.
func (gitter *Gitter) GetMessages(roomID string, params *Pagination) ([]Message, error) {
var messages []Message
url := gitter.config.apiBaseURL + "rooms/" + roomID + "/chatMessages"
if params != nil {
url += "?" + params.encode()
}
response, err := gitter.get(url)
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &messages)
if err != nil {
gitter.log(err)
return nil, err
}
return messages, nil
}
// GetMessage returns a message in a room.
func (gitter *Gitter) GetMessage(roomID, messageID string) (*Message, error) {
var message Message
response, err := gitter.get(gitter.config.apiBaseURL + "rooms/" + roomID + "/chatMessages/" + messageID)
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &message)
if err != nil {
gitter.log(err)
return nil, err
}
return &message, nil
}
// SendMessage sends a message to a room
func (gitter *Gitter) SendMessage(roomID, text string) (*Message, error) {
message := Message{Text: text}
body, _ := json.Marshal(message)
response, err := gitter.post(gitter.config.apiBaseURL+"rooms/"+roomID+"/chatMessages", body)
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &message)
if err != nil {
gitter.log(err)
return nil, err
}
return &message, nil
}
// UpdateMessage updates a message in a room
func (gitter *Gitter) UpdateMessage(roomID, msgID, text string) (*Message, error) {
message := Message{Text: text}
body, _ := json.Marshal(message)
response, err := gitter.put(gitter.config.apiBaseURL+"rooms/"+roomID+"/chatMessages/"+msgID, body)
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &message)
if err != nil {
gitter.log(err)
return nil, err
}
return &message, nil
}
// JoinRoom joins a room
func (gitter *Gitter) JoinRoom(roomID, userID string) (*Room, error) {
message := Room{ID: roomID}
body, _ := json.Marshal(message)
response, err := gitter.post(gitter.config.apiBaseURL+"user/"+userID+"/rooms", body)
if err != nil {
gitter.log(err)
return nil, err
}
var room Room
err = json.Unmarshal(response, &room)
if err != nil {
gitter.log(err)
return nil, err
}
return &room, nil
}
// LeaveRoom removes a user from the room
func (gitter *Gitter) LeaveRoom(roomID, userID string) error {
_, err := gitter.delete(gitter.config.apiBaseURL + "rooms/" + roomID + "/users/" + userID)
if err != nil {
gitter.log(err)
return err
}
return nil
}
// SetDebug traces errors if it's set to true.
func (gitter *Gitter) SetDebug(debug bool, logWriter io.Writer) {
gitter.debug = debug
gitter.logWriter = logWriter
}
// SearchRooms queries the Rooms resources of gitter API
func (gitter *Gitter) SearchRooms(room string) ([]Room, error) {
var rooms struct {
Results []Room `json:"results"`
}
response, err := gitter.get(gitter.config.apiBaseURL + "rooms?q=" + room)
if err != nil {
gitter.log(err)
return nil, err
}
err = json.Unmarshal(response, &rooms)
if err != nil {
gitter.log(err)
return nil, err
}
return rooms.Results, nil
}
// GetRoomId returns the room ID of a given URI
func (gitter *Gitter) GetRoomId(uri string) (string, error) {
rooms, err := gitter.SearchRooms(uri)
if err != nil {
gitter.log(err)
return "", err
}
for _, element := range rooms {
if element.URI == uri {
return element.ID, nil
}
}
return "", APIError{What: "Room not found."}
}
// Pagination params
type Pagination struct {
// Skip n messages
Skip int
// Get messages before beforeId
BeforeID string
// Get messages after afterId
AfterID string
// Maximum number of messages to return
Limit int
// Search query
Query string
}
func (messageParams *Pagination) encode() string {
values := url.Values{}
if messageParams.AfterID != "" {
values.Add("afterId", messageParams.AfterID)
}
if messageParams.BeforeID != "" {
values.Add("beforeId", messageParams.BeforeID)
}
if messageParams.Skip > 0 {
values.Add("skip", strconv.Itoa(messageParams.Skip))
}
if messageParams.Limit > 0 {
values.Add("limit", strconv.Itoa(messageParams.Limit))
}
return values.Encode()
}
func (gitter *Gitter) getResponse(url string, stream *Stream) (*http.Response, error) {
r, err := http.NewRequest("GET", url, nil)
if err != nil {
gitter.log(err)
return nil, err
}
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Accept", "application/json")
r.Header.Set("Authorization", "Bearer "+gitter.config.token)
if stream != nil {
stream.streamConnection.request = r
}
response, err := gitter.config.client.Do(r)
if err != nil {
gitter.log(err)
return nil, err
}
return response, nil
}
func (gitter *Gitter) get(url string) ([]byte, error) {
resp, err := gitter.getResponse(url, nil)
if err != nil {
gitter.log(err)
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = APIError{What: fmt.Sprintf("Status code: %v", resp.StatusCode)}
gitter.log(err)
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
gitter.log(err)
return nil, err
}
return body, nil
}
func (gitter *Gitter) post(url string, body []byte) ([]byte, error) {
r, err := http.NewRequest("POST", url, bytes.NewBuffer(body))
if err != nil {
gitter.log(err)
return nil, err
}
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Accept", "application/json")
r.Header.Set("Authorization", "Bearer "+gitter.config.token)
resp, err := gitter.config.client.Do(r)
if err != nil {
gitter.log(err)
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = APIError{What: fmt.Sprintf("Status code: %v", resp.StatusCode)}
gitter.log(err)
return nil, err
}
result, err := ioutil.ReadAll(resp.Body)
if err != nil {
gitter.log(err)
return nil, err
}
return result, nil
}
func (gitter *Gitter) put(url string, body []byte) ([]byte, error) {
r, err := http.NewRequest("PUT", url, bytes.NewBuffer(body))
if err != nil {
gitter.log(err)
return nil, err
}
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Accept", "application/json")
r.Header.Set("Authorization", "Bearer "+gitter.config.token)
resp, err := gitter.config.client.Do(r)
if err != nil {
gitter.log(err)
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = APIError{What: fmt.Sprintf("Status code: %v", resp.StatusCode)}
gitter.log(err)
return nil, err
}
result, err := ioutil.ReadAll(resp.Body)
if err != nil {
gitter.log(err)
return nil, err
}
return result, nil
}
func (gitter *Gitter) delete(url string) ([]byte, error) {
r, err := http.NewRequest("delete", url, nil)
if err != nil {
gitter.log(err)
return nil, err
}
r.Header.Set("Content-Type", "application/json")
r.Header.Set("Accept", "application/json")
r.Header.Set("Authorization", "Bearer "+gitter.config.token)
resp, err := gitter.config.client.Do(r)
if err != nil {
gitter.log(err)
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
err = APIError{What: fmt.Sprintf("Status code: %v", resp.StatusCode)}
gitter.log(err)
return nil, err
}
result, err := ioutil.ReadAll(resp.Body)
if err != nil {
gitter.log(err)
return nil, err
}
return result, nil
}
func (gitter *Gitter) log(a interface{}) {
if gitter.debug {
log.Println(a)
if gitter.logWriter != nil {
timestamp := time.Now().Format(time.RFC3339)
msg := fmt.Sprintf("%v: %v", timestamp, a)
fmt.Fprintln(gitter.logWriter, msg)
}
}
}
// APIError holds data of errors returned from the API.
type APIError struct {
What string
}
func (e APIError) Error() string {
return fmt.Sprintf("%v", e.What)
}

View File

@ -1,142 +0,0 @@
package gitter
import "time"
// A Room in Gitter can represent a GitHub Organization, a GitHub Repository, a Gitter Channel or a One-to-one conversation.
// In the case of the Organizations and Repositories, the access control policies are inherited from GitHub.
type Room struct {
// Room ID
ID string `json:"id"`
// Room name
Name string `json:"name"`
// Room topic. (default: GitHub repo description)
Topic string `json:"topic"`
// Room URI on Gitter
URI string `json:"uri"`
// Indicates if the room is a one-to-one chat
OneToOne bool `json:"oneToOne"`
// Count of users in the room
UserCount int `json:"userCount"`
// Number of unread messages for the current user
UnreadItems int `json:"unreadItems"`
// Number of unread mentions for the current user
Mentions int `json:"mentions"`
// Last time the current user accessed the room in ISO format
LastAccessTime time.Time `json:"lastAccessTime"`
// Indicates if the current user has disabled notifications
Lurk bool `json:"lurk"`
// Path to the room on gitter
URL string `json:"url"`
// Type of the room
// - ORG: A room that represents a GitHub Organization.
// - REPO: A room that represents a GitHub Repository.
// - ONETOONE: A one-to-one chat.
// - ORG_CHANNEL: A Gitter channel nested under a GitHub Organization.
// - REPO_CHANNEL A Gitter channel nested under a GitHub Repository.
// - USER_CHANNEL A Gitter channel nested under a GitHub User.
GithubType string `json:"githubType"`
// Tags that define the room
Tags []string `json:"tags"`
RoomMember bool `json:"roomMember"`
// Room version.
Version int `json:"v"`
}
type User struct {
// Gitter User ID
ID string `json:"id"`
// Gitter/GitHub username
Username string `json:"username"`
// Gitter/GitHub user real name
DisplayName string `json:"displayName"`
// Path to the user on Gitter
URL string `json:"url"`
// User avatar URI (small)
AvatarURLSmall string `json:"avatarUrlSmall"`
// User avatar URI (medium)
AvatarURLMedium string `json:"avatarUrlMedium"`
}
type Message struct {
// ID of the message
ID string `json:"id"`
// Original message in plain-text/markdown
Text string `json:"text"`
// HTML formatted message
HTML string `json:"html"`
// ISO formatted date of the message
Sent time.Time `json:"sent"`
// ISO formatted date of the message if edited
EditedAt time.Time `json:"editedAt"`
// User that sent the message
From User `json:"fromUser"`
// Boolean that indicates if the current user has read the message.
Unread bool `json:"unread"`
// Number of users that have read the message
ReadBy int `json:"readBy"`
// List of URLs present in the message
Urls []URL `json:"urls"`
// List of @Mentions in the message
Mentions []Mention `json:"mentions"`
// List of #Issues referenced in the message
Issues []Issue `json:"issues"`
// Version
Version int `json:"v"`
}
// Mention holds data about mentioned user in the message
type Mention struct {
// User's username
ScreenName string `json:"screenName"`
// Gitter User ID
UserID string `json:"userID"`
}
// Issue references issue in the message
type Issue struct {
// Issue number
Number string `json:"number"`
}
// URL presented in the message
type URL struct {
// URL
URL string `json:"url"`
}

View File

@ -1,217 +0,0 @@
package gitter
import (
"bufio"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/mreiferson/go-httpclient"
)
var defaultConnectionWaitTime time.Duration = 3000 // millis
var defaultConnectionMaxRetries = 5
// Stream initialize stream
func (gitter *Gitter) Stream(roomID string) *Stream {
return &Stream{
url: streamBaseURL + "rooms/" + roomID + "/chatMessages",
Event: make(chan Event),
gitter: gitter,
streamConnection: gitter.newStreamConnection(
defaultConnectionWaitTime,
defaultConnectionMaxRetries),
}
}
// Implemented to conform with https://developer.gitter.im/docs/streaming-api
func (gitter *Gitter) Listen(stream *Stream) {
defer stream.destroy()
var reader *bufio.Reader
var gitterMessage Message
lastKeepalive := time.Now().Unix()
// connect
stream.connect()
Loop:
for {
// if closed then stop trying
if stream.isClosed() {
stream.Event <- Event{
Data: &GitterConnectionClosed{},
}
break Loop
}
resp := stream.getResponse()
if resp.StatusCode != 200 {
gitter.log(fmt.Sprintf("Unexpected response code %v", resp.StatusCode))
continue
}
//"The JSON stream returns messages as JSON objects that are delimited by carriage return (\r)" <- Not true crap it's (\n) only
reader = bufio.NewReader(resp.Body)
line, err := reader.ReadBytes('\n')
if err != nil {
gitter.log("ReadBytes error: " + err.Error())
stream.connect()
continue
}
//Check if the line only consists of whitespace
onlyWhitespace := true
for _, b := range line {
if b != ' ' && b != '\t' && b != '\r' && b != '\n' {
onlyWhitespace = false
}
}
if onlyWhitespace {
//"Parsers must be tolerant of occasional extra newline characters placed between messages."
currentKeepalive := time.Now().Unix() //interesting behavior of 100+ keepalives per seconds was observed
if currentKeepalive-lastKeepalive > 10 {
lastKeepalive = currentKeepalive
gitter.log("Keepalive was received")
}
continue
} else if stream.isClosed() {
gitter.log("Stream closed")
continue
}
// unmarshal the streamed data
err = json.Unmarshal(line, &gitterMessage)
if err != nil {
gitter.log("JSON Unmarshal error: " + err.Error())
continue
}
// we are here, then we got the good message. pipe it forward.
stream.Event <- Event{
Data: &MessageReceived{
Message: gitterMessage,
},
}
}
gitter.log("Listening was completed")
}
// Stream holds stream data.
type Stream struct {
url string
Event chan Event
streamConnection *streamConnection
gitter *Gitter
}
func (stream *Stream) destroy() {
close(stream.Event)
stream.streamConnection.currentRetries = 0
}
type Event struct {
Data interface{}
}
type GitterConnectionClosed struct {
}
type MessageReceived struct {
Message Message
}
// connect and try to reconnect with
func (stream *Stream) connect() {
if stream.streamConnection.retries == stream.streamConnection.currentRetries {
stream.Close()
stream.gitter.log("Number of retries exceeded the max retries number, we are done here")
return
}
res, err := stream.gitter.getResponse(stream.url, stream)
if err != nil || res.StatusCode != 200 {
stream.gitter.log("Failed to get response, trying reconnect")
if res != nil {
stream.gitter.log(fmt.Sprintf("Status code: %v", res.StatusCode))
}
stream.gitter.log(err)
// sleep and wait
stream.streamConnection.currentRetries++
time.Sleep(time.Millisecond * stream.streamConnection.wait * time.Duration(stream.streamConnection.currentRetries))
// connect again
stream.Close()
stream.connect()
} else {
stream.gitter.log("Response was received")
stream.streamConnection.currentRetries = 0
stream.streamConnection.closed = false
stream.streamConnection.response = res
}
}
type streamConnection struct {
// connection was closed
closed bool
// wait time till next try
wait time.Duration
// max tries to recover
retries int
// current streamed response
response *http.Response
// current request
request *http.Request
// current status
currentRetries int
}
// Close the stream connection and stop receiving streamed data
func (stream *Stream) Close() {
conn := stream.streamConnection
conn.closed = true
if conn.response != nil {
stream.gitter.log("Stream connection close response")
defer conn.response.Body.Close()
}
if conn.request != nil {
stream.gitter.log("Stream connection close request")
switch transport := stream.gitter.config.client.Transport.(type) {
case *httpclient.Transport:
transport.CancelRequest(conn.request)
default:
}
}
}
func (stream *Stream) isClosed() bool {
return stream.streamConnection.closed
}
func (stream *Stream) getResponse() *http.Response {
return stream.streamConnection.response
}
// Optional, set stream connection properties
// wait - time in milliseconds of waiting between reconnections. Will grow exponentially.
// retries - number of reconnections retries before dropping the stream.
func (gitter *Gitter) newStreamConnection(wait time.Duration, retries int) *streamConnection {
return &streamConnection{
closed: true,
wait: wait,
retries: retries,
}
}

View File

@ -1,30 +0,0 @@
package gitter
import (
"net/http"
"net/http/httptest"
"net/url"
)
var (
mux *http.ServeMux
gitter *Gitter
server *httptest.Server
)
func setup() {
mux = http.NewServeMux()
server = httptest.NewServer(mux)
gitter = New("abc")
// Fake the API and Stream base URLs by using the test
// server URL instead.
url, _ := url.Parse(server.URL)
gitter.config.apiBaseURL = url.String() + "/"
gitter.config.streamBaseURL = url.String() + "/"
}
func teardown() {
server.Close()
}

View File

@ -1,9 +1,11 @@
---
run:
timeout: 5m
linters:
disable-all: true
enable:
- bodyclose
- deadcode
- errcheck
- gochecknoglobals
- goconst
@ -19,13 +21,11 @@ linters:
- nakedret
- prealloc
- staticcheck
- structcheck
- stylecheck
- typecheck
- unconvert
- unparam
- unused
- varcheck
- whitespace
- wsl
- godot
@ -40,7 +40,6 @@ linters:
- makezero
- thelper
- predeclared
- ifshort
- revive
- durationcheck
- gomoddirectives
@ -54,7 +53,34 @@ linters:
- nilnil
- tenv
- nestif
- grouper
- decorder
- containedctx
- nosprintfhostport
- usestdlibvars
- interfacebloat
- reassign
- testableexamples
- gocheckcompilerdirectives
- asasalint
- musttag
- gosmopolitan
- mirror
- tagalign
- gochecksumtype
- inamedparam
- perfsprint
- sloglint
- testifylint
# - musttag # TODO: need update golangci-lint
# - wrapcheck # TODO: v3 Fix
# - testpackage # TODO: Fix testpackage
# - noctx # TODO: Fix noctx
@ -82,11 +108,29 @@ linters:
# - tagliatelle
# - errname
# - varnamelen
# - errchkjson
# - maintidx
# - nonamedreturns
# - nosnakecase
# - execinquery
# - logrlint
# - dupword
# - ginkgolinter
# - zerologlint
# - protogetter
# - spancheck
# depricated
# - maligned
# - interfacer
# - golint
# - ifshort
# - deadcode
# - structcheck
# - varcheck
issues:
exclude-rules:
@ -106,4 +150,14 @@ issues:
- stylecheck
text: "ST1003:.*(Ts|ts).*TS"
- linters:
- gosec
text: "G307:"
exclude-use-default: false
linters-settings:
testifylint:
enable-all: true
disable:
- require-error # f func false positive

View File

@ -6,7 +6,7 @@
Требования:
- [Go 1.16+](https://golang.org/doc/install)
- [Go 1.21+](https://golang.org/doc/install)
- [golangci-lint](https://github.com/golangci/golangci-lint)
- [global .gitignore](https://help.github.com/en/articles/ignoring-files#create-a-global-gitignore)
@ -54,17 +54,17 @@ go test ./...
```json
{
"go.testEnvVars": {
"SERVICE_TOKEN": "",
"WIDGET_TOKEN": "",
"MARUSIA_TOKEN": "",
"GROUP_TOKEN": "",
"CLIENT_SECRET": "",
"USER_TOKEN": "",
"CLIENT_ID": "123456",
"GROUP_ID": "123456",
"ACCOUNT_ID": "123456"
}
"go.testEnvVars": {
"SERVICE_TOKEN": "",
"WIDGET_TOKEN": "",
"MARUSIA_TOKEN": "",
"GROUP_TOKEN": "",
"CLIENT_SECRET": "",
"USER_TOKEN": "",
"CLIENT_ID": "123456",
"GROUP_ID": "123456",
"ACCOUNT_ID": "123456"
}
}
```
@ -88,7 +88,4 @@ git push origin <name_of_your_new_branch>
```
Затем откройте [pull request](https://github.com/SevereCloud/vksdk/pulls)
с веткой:
- `master` если это багфикс
- `dev-v1.2.3` если это новая фича
с веткой master

View File

@ -1,7 +1,7 @@
# VK SDK for Golang
[![PkgGoDev](https://pkg.go.dev/badge/github.com/SevereCloud/vksdk/v2/v2)](https://pkg.go.dev/github.com/SevereCloud/vksdk/v2?tab=subdirectories)
[![VK Developers](https://img.shields.io/badge/developers-%234a76a8.svg?logo=VK&logoColor=white)](https://vk.com/dev/)
[![VK Developers](https://img.shields.io/badge/developers-%234a76a8.svg?logo=VK&logoColor=white)](https://dev.vk.com/)
[![codecov](https://codecov.io/gh/SevereCloud/vksdk/branch/master/graph/badge.svg)](https://codecov.io/gh/SevereCloud/vksdk)
[![VK chat](https://img.shields.io/badge/VK%20chat-%234a76a8.svg?logo=VK&logoColor=white)](https://vk.me/join/AJQ1d6Or8Q00Y_CSOESfbqGt)
[![release](https://img.shields.io/github/v/tag/SevereCloud/vksdk?label=release)](https://github.com/SevereCloud/vksdk/releases)

View File

@ -1,13 +1,13 @@
# API
[![PkgGoDev](https://pkg.go.dev/badge/github.com/SevereCloud/vksdk/v2/api)](https://pkg.go.dev/github.com/SevereCloud/vksdk/v2/api)
[![VK](https://img.shields.io/badge/developers-%234a76a8.svg?logo=VK&logoColor=white)](https://vk.com/dev/first_guide)
[![VK](https://img.shields.io/badge/developers-%234a76a8.svg?logo=VK&logoColor=white)](https://dev.vk.com/ru/api/getting-started)
Данная библиотека поддерживает версию API **5.131**.
## Запросы
В начале необходимо инициализировать api с помощью [ключа доступа](https://vk.com/dev/access_token):
В начале необходимо инициализировать api с помощью [ключа доступа](https://dev.vk.com/ru/api/access-token/getting-started):
```go
vk := api.NewVK("<TOKEN>")
@ -21,7 +21,7 @@ vk := api.NewVK("<TOKEN>")
Список всех методов можно найти на
[данной странице](https://pkg.go.dev/github.com/SevereCloud/vksdk/v2/api?tab=doc#VK).
Пример запроса [`users.get`](https://vk.com/dev/users.get)
Пример запроса [`users.get`](https://dev.vk.com/method/users.get)
```go
users, err := vk.UsersGet(api.Params{
@ -52,7 +52,7 @@ res, err = api.MessageSend(b.Params)
### Обработка ошибок
[![VK](https://img.shields.io/badge/developers-%234a76a8.svg?logo=VK&logoColor=white)](https://vk.com/dev/errors)
[![VK](https://img.shields.io/badge/developers-%234a76a8.svg?logo=VK&logoColor=white)](https://dev.vk.com/ru/reference/errors)
Обработка ошибок полностью поддерживает методы
[go 1.13](https://blog.golang.org/go1.13-errors)
@ -130,7 +130,7 @@ log.Println("msgpack:", len(r)) // msgpack: 650775
### Запрос любого метода
Пример запроса [users.get](https://vk.com/dev/users.get)
Пример запроса [users.get](https://dev.vk.com/method/users.get)
```go
// Определяем структуру, которую вернет API
@ -153,7 +153,7 @@ log.Print(response)
### Execute
[![PkgGoDev](https://pkg.go.dev/badge/github.com/SevereCloud/vksdk/v2/errors)](https://pkg.go.dev/github.com/SevereCloud/vksdk/v2/api#VK.Execute)
[![VK](https://img.shields.io/badge/developers-%234a76a8.svg?logo=VK&logoColor=white)](https://vk.com/dev/execute)
[![VK](https://img.shields.io/badge/developers-%234a76a8.svg?logo=VK&logoColor=white)](https://dev.vk.com/ru/method/execute)
Универсальный метод, который позволяет запускать последовательность других
методов, сохраняя и фильтруя промежуточные результаты.
@ -199,7 +199,7 @@ vk.Handler = func(method string, params ...api.Params) (api.Response, error) {
позволяет совершить до 25 обращений к разным методам в рамках одного запроса.
Для методов секции ads действуют собственные ограничения, ознакомиться с ними
Вы можете на [этой странице](https://vk.com/dev/ads_limits).
Вы можете на [этой странице](https://dev.vk.com/ru/method/ads).
Максимальное число обращений к методам секции secure зависит от числа
пользователей, установивших приложение. Если приложение установило меньше 10
@ -236,7 +236,7 @@ vk.Client = client
### Ошибка с Captcha
[![VK](https://img.shields.io/badge/developers-%234a76a8.svg?logo=VK&logoColor=white)](https://vk.com/dev/captcha_error)
[![VK](https://img.shields.io/badge/developers-%234a76a8.svg?logo=VK&logoColor=white)](https://dev.vk.com/ru/api/captcha-error)
Если какое-либо действие (например, отправка сообщения) выполняется
пользователем слишком часто, то запрос к API может возвращать ошибку
@ -262,7 +262,7 @@ vk.Client = client
## Загрузка файлов
[![VK](https://img.shields.io/badge/developers-%234a76a8.svg?logo=VK&logoColor=white)](https://vk.com/dev/upload_files)
[![VK](https://img.shields.io/badge/developers-%234a76a8.svg?logo=VK&logoColor=white)](https://dev.vk.com/ru/api/upload/overview)
### 1. Загрузка фотографий в альбом
@ -364,7 +364,7 @@ messageInfo, err = vk.UploadChatPhotoCrop(peerID, cropX, cropY, cropWidth, file)
не более 14000px, файл объемом не более 50 МБ, соотношение сторон не менее 1:20.
Если Вы хотите загрузить основную фотографию товара, необходимо передать
параметр `mainPhoto = true`. Если фотография не основная, она не будет обрезаться.
параметр `mainPhoto = true`. Если фотография не основная, она не будет обрезаться.
Без обрезки:
@ -392,7 +392,7 @@ photosPhoto, err = vk.UploadMarketAlbumPhoto(groupID, file)
Допустимые форматы: AVI, MP4, 3GP, MPEG, MOV, MP3, FLV, WMV.
[Параметры](https://vk.com/dev/video.save)
[Параметры](https://dev.vk.com/method/video.save)
```go
videoUploadResponse, err = vk.UploadVideo(params, file)
@ -476,13 +476,13 @@ docsDoc, err = vk.UploadMessagesDoc(peerID, "audio_message", title, tags, file)
не более 10МБ. Формат видео: h264 video, aac audio,
максимальное разрешение 720х1280, 30fps.
Загрузить историю с фотографией. [Параметры](https://vk.com/dev/stories.getPhotoUploadServer)
Загрузить историю с фотографией. [Параметры](https://dev.vk.com/method/stories.getPhotoUploadServer)
```go
uploadInfo, err = vk.UploadStoriesPhoto(params, file)
```
Загрузить историю с видео. [Параметры](https://vk.com/dev/stories.getVideoUploadServer)
Загрузить историю с видео. [Параметры](https://dev.vk.com/method/stories.getVideoUploadServer)
```go
uploadInfo, err = vk.UploadStoriesVideo(params, file)
@ -529,14 +529,14 @@ photo, err = vk.UploadLeadFormsPhoto(file)
```
Полученные данные можно использовать в методах
[leadForms.create](https://vk.com/dev/leadForms.create)
[leadForms.create](https://dev.vk.com/method/leadForms.create)
и
[leadForms.edit](https://vk.com/dev/leadForms.edit).
[leadForms.edit](https://dev.vk.com/method/leadForms.edit).
Полученные данные можно использовать в методах
[prettyCards.create](https://vk.com/dev/prettyCards.create)
[prettyCards.create](https://dev.vk.com/method/prettyCards.create)
и
[prettyCards.edit](https://vk.com/dev/prettyCards.edit).
[prettyCards.edit](https://dev.vk.com/method/prettyCards.edit).
### Загрузки фотографии в коллекцию приложения для виджетов приложений сообществ

View File

@ -6,7 +6,7 @@ import (
// AccountBan account.ban.
//
// https://vk.com/dev/account.ban
// https://dev.vk.com/method/account.ban
func (vk *VK) AccountBan(params Params) (response int, err error) {
err = vk.RequestUnmarshal("account.ban", &response, params)
return
@ -19,7 +19,7 @@ type AccountChangePasswordResponse struct {
// AccountChangePassword changes a user password after access is successfully restored with the auth.restore method.
//
// https://vk.com/dev/account.changePassword
// https://dev.vk.com/method/account.changePassword
func (vk *VK) AccountChangePassword(params Params) (response AccountChangePasswordResponse, err error) {
err = vk.RequestUnmarshal("account.changePassword", &response, params)
return
@ -35,7 +35,7 @@ type AccountGetActiveOffersResponse struct {
// If the user fulfill their conditions, he will be able to get
// the appropriate number of votes to his balance.
//
// https://vk.com/dev/account.getActiveOffers
// https://dev.vk.com/method/account.getActiveOffers
func (vk *VK) AccountGetActiveOffers(params Params) (response AccountGetActiveOffersResponse, err error) {
err = vk.RequestUnmarshal("account.getActiveOffers", &response, params)
return
@ -43,7 +43,7 @@ func (vk *VK) AccountGetActiveOffers(params Params) (response AccountGetActiveOf
// AccountGetAppPermissions gets settings of the user in this application.
//
// https://vk.com/dev/account.getAppPermissions
// https://dev.vk.com/method/account.getAppPermissions
func (vk *VK) AccountGetAppPermissions(params Params) (response int, err error) {
err = vk.RequestUnmarshal("account.getAppPermissions", &response, params)
return
@ -58,7 +58,7 @@ type AccountGetBannedResponse struct {
// AccountGetBanned returns a user's blacklist.
//
// https://vk.com/dev/account.getBanned
// https://dev.vk.com/method/account.getBanned
func (vk *VK) AccountGetBanned(params Params) (response AccountGetBannedResponse, err error) {
err = vk.RequestUnmarshal("account.getBanned", &response, params)
return
@ -69,7 +69,7 @@ type AccountGetCountersResponse object.AccountAccountCounters
// AccountGetCounters returns non-null values of user counters.
//
// https://vk.com/dev/account.getCounters
// https://dev.vk.com/method/account.getCounters
func (vk *VK) AccountGetCounters(params Params) (response AccountGetCountersResponse, err error) {
err = vk.RequestUnmarshal("account.getCounters", &response, params)
return
@ -80,7 +80,7 @@ type AccountGetInfoResponse object.AccountInfo
// AccountGetInfo returns current account info.
//
// https://vk.com/dev/account.getInfo
// https://dev.vk.com/method/account.getInfo
func (vk *VK) AccountGetInfo(params Params) (response AccountGetInfoResponse, err error) {
err = vk.RequestUnmarshal("account.getInfo", &response, params)
return
@ -91,7 +91,7 @@ type AccountGetProfileInfoResponse object.AccountUserSettings
// AccountGetProfileInfo returns the current account info.
//
// https://vk.com/dev/account.getProfileInfo
// https://dev.vk.com/method/account.getProfileInfo
func (vk *VK) AccountGetProfileInfo(params Params) (response AccountGetProfileInfoResponse, err error) {
err = vk.RequestUnmarshal("account.getProfileInfo", &response, params)
return
@ -102,7 +102,7 @@ type AccountGetPushSettingsResponse object.AccountPushSettings
// AccountGetPushSettings account.getPushSettings Gets settings of push notifications.
//
// https://vk.com/dev/account.getPushSettings
// https://dev.vk.com/method/account.getPushSettings
func (vk *VK) AccountGetPushSettings(params Params) (response AccountGetPushSettingsResponse, err error) {
err = vk.RequestUnmarshal("account.getPushSettings", &response, params)
return
@ -110,7 +110,7 @@ func (vk *VK) AccountGetPushSettings(params Params) (response AccountGetPushSett
// AccountRegisterDevice subscribes an iOS/Android/Windows/Mac based device to receive push notifications.
//
// https://vk.com/dev/account.registerDevice
// https://dev.vk.com/method/account.registerDevice
func (vk *VK) AccountRegisterDevice(params Params) (response int, err error) {
err = vk.RequestUnmarshal("account.registerDevice", &response, params)
return
@ -124,7 +124,7 @@ type AccountSaveProfileInfoResponse struct {
// AccountSaveProfileInfo edits current profile info.
//
// https://vk.com/dev/account.saveProfileInfo
// https://dev.vk.com/method/account.saveProfileInfo
func (vk *VK) AccountSaveProfileInfo(params Params) (response AccountSaveProfileInfoResponse, err error) {
err = vk.RequestUnmarshal("account.saveProfileInfo", &response, params)
return
@ -132,7 +132,7 @@ func (vk *VK) AccountSaveProfileInfo(params Params) (response AccountSaveProfile
// AccountSetInfo allows to edit the current account info.
//
// https://vk.com/dev/account.setInfo
// https://dev.vk.com/method/account.setInfo
func (vk *VK) AccountSetInfo(params Params) (response int, err error) {
err = vk.RequestUnmarshal("account.setInfo", &response, params)
return
@ -141,7 +141,9 @@ func (vk *VK) AccountSetInfo(params Params) (response int, err error) {
// AccountSetNameInMenu sets an application screen name
// (up to 17 characters), that is shown to the user in the left menu.
//
// https://vk.com/dev/account.setNameInMenu
// Deprecated: This method is deprecated and may be disabled soon, please avoid
//
// https://dev.vk.com/method/account.setNameInMenu
func (vk *VK) AccountSetNameInMenu(params Params) (response int, err error) {
err = vk.RequestUnmarshal("account.setNameInMenu", &response, params)
return
@ -149,7 +151,7 @@ func (vk *VK) AccountSetNameInMenu(params Params) (response int, err error) {
// AccountSetOffline marks a current user as offline.
//
// https://vk.com/dev/account.setOffline
// https://dev.vk.com/method/account.setOffline
func (vk *VK) AccountSetOffline(params Params) (response int, err error) {
err = vk.RequestUnmarshal("account.setOffline", &response, params)
return
@ -157,7 +159,7 @@ func (vk *VK) AccountSetOffline(params Params) (response int, err error) {
// AccountSetOnline marks the current user as online for 5 minutes.
//
// https://vk.com/dev/account.setOnline
// https://dev.vk.com/method/account.setOnline
func (vk *VK) AccountSetOnline(params Params) (response int, err error) {
err = vk.RequestUnmarshal("account.setOnline", &response, params)
return
@ -165,7 +167,7 @@ func (vk *VK) AccountSetOnline(params Params) (response int, err error) {
// AccountSetPushSettings change push settings.
//
// https://vk.com/dev/account.setPushSettings
// https://dev.vk.com/method/account.setPushSettings
func (vk *VK) AccountSetPushSettings(params Params) (response int, err error) {
err = vk.RequestUnmarshal("account.setPushSettings", &response, params)
return
@ -173,7 +175,7 @@ func (vk *VK) AccountSetPushSettings(params Params) (response int, err error) {
// AccountSetSilenceMode mutes push notifications for the set period of time.
//
// https://vk.com/dev/account.setSilenceMode
// https://dev.vk.com/method/account.setSilenceMode
func (vk *VK) AccountSetSilenceMode(params Params) (response int, err error) {
err = vk.RequestUnmarshal("account.setSilenceMode", &response, params)
return
@ -181,7 +183,7 @@ func (vk *VK) AccountSetSilenceMode(params Params) (response int, err error) {
// AccountUnban account.unban.
//
// https://vk.com/dev/account.unban
// https://dev.vk.com/method/account.unban
func (vk *VK) AccountUnban(params Params) (response int, err error) {
err = vk.RequestUnmarshal("account.unban", &response, params)
return
@ -189,7 +191,7 @@ func (vk *VK) AccountUnban(params Params) (response int, err error) {
// AccountUnregisterDevice unsubscribes a device from push notifications.
//
// https://vk.com/dev/account.unregisterDevice
// https://dev.vk.com/method/account.unregisterDevice
func (vk *VK) AccountUnregisterDevice(params Params) (response int, err error) {
err = vk.RequestUnmarshal("account.unregisterDevice", &response, params)
return

View File

@ -45,7 +45,7 @@ type AdsAddOfficeUsersResponse []AdsAddOfficeUsersItem
// AdsAddOfficeUsers adds managers and/or supervisors to advertising account.
//
// https://vk.com/dev/ads.addOfficeUsers
// https://dev.vk.com/method/ads.addOfficeUsers
func (vk *VK) AdsAddOfficeUsers(params Params) (response AdsAddOfficeUsersResponse, err error) {
err = vk.RequestUnmarshal("ads.addOfficeUsers", &response, params)
return
@ -65,7 +65,7 @@ type AdsCheckLinkResponse struct {
// AdsCheckLink allows to check the ad link.
//
// https://vk.com/dev/ads.checkLink
// https://dev.vk.com/method/ads.checkLink
func (vk *VK) AdsCheckLink(params Params) (response AdsCheckLinkResponse, err error) {
err = vk.RequestUnmarshal("ads.checkLink", &response, params)
return
@ -82,7 +82,7 @@ type AdsCreateAdsResponse []struct {
// Please note! Maximum allowed number of ads created in one request is 5.
// Minimum size of ad audience is 50 people.
//
// https://vk.com/dev/ads.createAds
// https://dev.vk.com/method/ads.createAds
func (vk *VK) AdsCreateAds(params Params) (response AdsCreateAdsResponse, err error) {
err = vk.RequestUnmarshal("ads.createAds", &response, params)
return
@ -98,7 +98,7 @@ type AdsCreateCampaignsResponse []struct {
//
// Please note! Allowed number of campaigns created in one request is 50.
//
// https://vk.com/dev/ads.createCampaigns
// https://dev.vk.com/method/ads.createCampaigns
func (vk *VK) AdsCreateCampaigns(params Params) (response AdsCreateCampaignsResponse, err error) {
err = vk.RequestUnmarshal("ads.createCampaigns", &response, params)
return
@ -116,7 +116,7 @@ type AdsCreateClientsResponse []struct {
//
// Please note! Allowed number of clients created in one request is 50.
//
// https://vk.com/dev/ads.createClients
// https://dev.vk.com/method/ads.createClients
func (vk *VK) AdsCreateClients(params Params) (response AdsCreateClientsResponse, err error) {
err = vk.RequestUnmarshal("ads.createClients", &response, params)
return
@ -129,7 +129,7 @@ type AdsCreateLookalikeRequestResponse struct {
// AdsCreateLookalikeRequest creates a request to find a similar audience.
//
// https://vk.com/dev/ads.createLookalikeRequest
// https://dev.vk.com/method/ads.createLookalikeRequest
func (vk *VK) AdsCreateLookalikeRequest(params Params) (response AdsCreateLookalikeRequestResponse, err error) {
err = vk.RequestUnmarshal("ads.createLookalikeRequest", &response, params)
return
@ -154,7 +154,7 @@ type AdsCreateTargetGroupResponse struct {
// Please note! Maximum allowed number of groups for one advertising
// account is 100.
//
// https://vk.com/dev/ads.createTargetGroup
// https://dev.vk.com/method/ads.createTargetGroup
func (vk *VK) AdsCreateTargetGroup(params Params) (response AdsCreateTargetGroupResponse, err error) {
err = vk.RequestUnmarshal("ads.createTargetGroup", &response, params)
return
@ -175,7 +175,7 @@ type AdsCreateTargetPixelResponse struct {
//
// Maximum pixels number per advertising account is 25.
//
// https://vk.com/dev/ads.createTargetPixel
// https://dev.vk.com/method/ads.createTargetPixel
func (vk *VK) AdsCreateTargetPixel(params Params) (response AdsCreateTargetPixelResponse, err error) {
err = vk.RequestUnmarshal("ads.createTargetPixel", &response, params)
return
@ -190,7 +190,7 @@ type AdsDeleteAdsResponse []ErrorType
//
// Warning! Maximum allowed number of ads archived in one request is 100.
//
// https://vk.com/dev/ads.deleteAds
// https://dev.vk.com/method/ads.deleteAds
func (vk *VK) AdsDeleteAds(params Params) (response AdsDeleteAdsResponse, err error) {
err = vk.RequestUnmarshal("ads.deleteAds", &response, params)
return
@ -203,10 +203,9 @@ type AdsDeleteCampaignsResponse []ErrorType
// AdsDeleteCampaigns archives advertising campaigns.
//
//
// Warning! Maximum allowed number of campaigns archived in one request is 100.
//
// https://vk.com/dev/ads.deleteCampaigns
// https://dev.vk.com/method/ads.deleteCampaigns
func (vk *VK) AdsDeleteCampaigns(params Params) (response AdsDeleteCampaignsResponse, err error) {
err = vk.RequestUnmarshal("ads.deleteCampaigns", &response, params)
return
@ -223,7 +222,7 @@ type AdsDeleteClientsResponse []ErrorType
//
// Please note! Maximum allowed number of clients edited in one request is 10.
//
// https://vk.com/dev/ads.deleteClients
// https://dev.vk.com/method/ads.deleteClients
func (vk *VK) AdsDeleteClients(params Params) (response AdsDeleteClientsResponse, err error) {
err = vk.RequestUnmarshal("ads.deleteClients", &response, params)
return
@ -231,7 +230,7 @@ func (vk *VK) AdsDeleteClients(params Params) (response AdsDeleteClientsResponse
// AdsDeleteTargetGroup deletes target group.
//
// https://vk.com/dev/ads.deleteTargetGroup
// https://dev.vk.com/method/ads.deleteTargetGroup
func (vk *VK) AdsDeleteTargetGroup(params Params) (response int, err error) {
err = vk.RequestUnmarshal("ads.deleteTargetGroup", &response, params)
return
@ -239,7 +238,7 @@ func (vk *VK) AdsDeleteTargetGroup(params Params) (response int, err error) {
// AdsDeleteTargetPixel deletes target pixel.
//
// https://vk.com/dev/ads.deleteTargetPixel
// https://dev.vk.com/method/ads.deleteTargetPixel
func (vk *VK) AdsDeleteTargetPixel(params Params) (response int, err error) {
err = vk.RequestUnmarshal("ads.deleteTargetPixel", &response, params)
return
@ -250,7 +249,7 @@ type AdsGetAccountsResponse []object.AdsAccount
// AdsGetAccounts returns a list of advertising accounts.
//
// https://vk.com/dev/ads.getAccounts
// https://dev.vk.com/method/ads.getAccounts
func (vk *VK) AdsGetAccounts(params Params) (response AdsGetAccountsResponse, err error) {
err = vk.RequestUnmarshal("ads.getAccounts", &response, params)
return
@ -261,7 +260,7 @@ type AdsGetAdsResponse []object.AdsAd
// AdsGetAds returns a list of ads.
//
// https://vk.com/dev/ads.getAds
// https://dev.vk.com/method/ads.getAds
func (vk *VK) AdsGetAds(params Params) (response AdsGetAdsResponse, err error) {
err = vk.RequestUnmarshal("ads.getAds", &response, params)
return
@ -272,7 +271,7 @@ type AdsGetAdsLayoutResponse []object.AdsAdLayout
// AdsGetAdsLayout returns descriptions of ad layouts.
//
// https://vk.com/dev/ads.getAdsLayout
// https://dev.vk.com/method/ads.getAdsLayout
func (vk *VK) AdsGetAdsLayout(params Params) (response AdsGetAdsLayoutResponse, err error) {
err = vk.RequestUnmarshal("ads.getAdsLayout", &response, params)
return
@ -283,7 +282,7 @@ func (vk *VK) AdsGetAdsLayout(params Params) (response AdsGetAdsLayoutResponse,
// TODO: AdsGetAdsTargeting ...
//
// https://vk.com/dev/ads.getAdsTargeting
// https://dev.vk.com/method/ads.getAdsTargeting
// func (vk *VK) AdsGetAdsTargeting(params Params) (response AdsGetAdsTargetingResponse, err error) {
// err = vk.RequestUnmarshal("ads.getAdsTargeting", &response, params)
// return
@ -294,7 +293,7 @@ func (vk *VK) AdsGetAdsLayout(params Params) (response AdsGetAdsLayoutResponse,
// TODO: AdsGetBudget ...
//
// https://vk.com/dev/ads.getBudget
// https://dev.vk.com/method/ads.getBudget
// func (vk *VK) AdsGetBudget(params Params) (response AdsGetBudgetResponse, err error) {
// err = vk.RequestUnmarshal("ads.getBudget", &response, params)
// return
@ -305,7 +304,7 @@ func (vk *VK) AdsGetAdsLayout(params Params) (response AdsGetAdsLayoutResponse,
// TODO: AdsGetCampaigns ...
//
// https://vk.com/dev/ads.getCampaigns
// https://dev.vk.com/method/ads.getCampaigns
// func (vk *VK) AdsGetCampaigns(params Params) (response AdsGetCampaignsResponse, err error) {
// err = vk.RequestUnmarshal("ads.getCampaigns", &response, params)
// return
@ -316,7 +315,7 @@ func (vk *VK) AdsGetAdsLayout(params Params) (response AdsGetAdsLayoutResponse,
// TODO: AdsGetCategories ...
//
// https://vk.com/dev/ads.getCategories
// https://dev.vk.com/method/ads.getCategories
// func (vk *VK) AdsGetCategories(params Params) (response AdsGetCategoriesResponse, err error) {
// err = vk.RequestUnmarshal("ads.getCategories", &response, params)
// return
@ -327,7 +326,7 @@ func (vk *VK) AdsGetAdsLayout(params Params) (response AdsGetAdsLayoutResponse,
// TODO: AdsGetClients ...
//
// https://vk.com/dev/ads.getClients
// https://dev.vk.com/method/ads.getClients
// func (vk *VK) AdsGetClients(params Params) (response AdsGetClientsResponse, err error) {
// err = vk.RequestUnmarshal("ads.getClients", &response, params)
// return
@ -338,7 +337,7 @@ func (vk *VK) AdsGetAdsLayout(params Params) (response AdsGetAdsLayoutResponse,
// TODO: AdsGetDemographics ...
//
// https://vk.com/dev/ads.getDemographics
// https://dev.vk.com/method/ads.getDemographics
// func (vk *VK) AdsGetDemographics(params Params) (response AdsGetDemographicsResponse, err error) {
// err = vk.RequestUnmarshal("ads.getDemographics", &response, params)
// return
@ -349,7 +348,7 @@ func (vk *VK) AdsGetAdsLayout(params Params) (response AdsGetAdsLayoutResponse,
// TODO: AdsGetFloodStats ...
//
// https://vk.com/dev/ads.getFloodStats
// https://dev.vk.com/method/ads.getFloodStats
// func (vk *VK) AdsGetFloodStats(params Params) (response AdsGetFloodStatsResponse, err error) {
// err = vk.RequestUnmarshal("ads.getFloodStats", &response, params)
// return
@ -360,7 +359,7 @@ func (vk *VK) AdsGetAdsLayout(params Params) (response AdsGetAdsLayoutResponse,
// TODO: AdsGetLookalikeRequests ...
//
// https://vk.com/dev/ads.getLookalikeRequests
// https://dev.vk.com/method/ads.getLookalikeRequests
// func (vk *VK) AdsGetLookalikeRequests(params Params) (response AdsGetLookalikeRequestsResponse, err error) {
// err = vk.RequestUnmarshal("ads.getLookalikeRequests", &response, params)
// return
@ -373,7 +372,7 @@ type AdsGetMusiciansResponse struct {
// AdsGetMusicians returns a list of musicians.
//
// https://vk.com/dev/ads.getMusicians
// https://dev.vk.com/method/ads.getMusicians
func (vk *VK) AdsGetMusicians(params Params) (response AdsGetMusiciansResponse, err error) {
err = vk.RequestUnmarshal("ads.getMusicians", &response, params)
return
@ -384,7 +383,7 @@ func (vk *VK) AdsGetMusicians(params Params) (response AdsGetMusiciansResponse,
// TODO: AdsGetOfficeUsers ...
//
// https://vk.com/dev/ads.getOfficeUsers
// https://dev.vk.com/method/ads.getOfficeUsers
// func (vk *VK) AdsGetOfficeUsers(params Params) (response AdsGetOfficeUsersResponse, err error) {
// err = vk.RequestUnmarshal("ads.getOfficeUsers", &response, params)
// return
@ -395,7 +394,7 @@ func (vk *VK) AdsGetMusicians(params Params) (response AdsGetMusiciansResponse,
// TODO: AdsGetPostsReach ...
//
// https://vk.com/dev/ads.getPostsReach
// https://dev.vk.com/method/ads.getPostsReach
// func (vk *VK) AdsGetPostsReach(params Params) (response AdsGetPostsReachResponse, err error) {
// err = vk.RequestUnmarshal("ads.getPostsReach", &response, params)
// return
@ -406,7 +405,7 @@ func (vk *VK) AdsGetMusicians(params Params) (response AdsGetMusiciansResponse,
// TODO: AdsGetRejectionReason ...
//
// https://vk.com/dev/ads.getRejectionReason
// https://dev.vk.com/method/ads.getRejectionReason
// func (vk *VK) AdsGetRejectionReason(params Params) (response AdsGetRejectionReasonResponse, err error) {
// err = vk.RequestUnmarshal("ads.getRejectionReason", &response, params)
// return
@ -417,7 +416,7 @@ func (vk *VK) AdsGetMusicians(params Params) (response AdsGetMusiciansResponse,
// TODO: AdsGetStatistics ...
//
// https://vk.com/dev/ads.getStatistics
// https://dev.vk.com/method/ads.getStatistics
// func (vk *VK) AdsGetStatistics(params Params) (response AdsGetStatisticsResponse, err error) {
// err = vk.RequestUnmarshal("ads.getStatistics", &response, params)
// return
@ -428,7 +427,7 @@ func (vk *VK) AdsGetMusicians(params Params) (response AdsGetMusiciansResponse,
// TODO: AdsGetSuggestions ...
//
// https://vk.com/dev/ads.getSuggestions
// https://dev.vk.com/method/ads.getSuggestions
// func (vk *VK) AdsGetSuggestions(params Params) (response AdsGetSuggestionsResponse, err error) {
// err = vk.RequestUnmarshal("ads.getSuggestions", &response, params)
// return
@ -439,7 +438,7 @@ type AdsGetTargetGroupsResponse []object.AdsTargetGroup
// AdsGetTargetGroups returns a list of target groups.
//
// https://vk.com/dev/ads.getTargetGroups
// https://dev.vk.com/method/ads.getTargetGroups
func (vk *VK) AdsGetTargetGroups(params Params) (response AdsGetTargetGroupsResponse, err error) {
err = vk.RequestUnmarshal("ads.getTargetGroups", &response, params)
return
@ -450,7 +449,7 @@ func (vk *VK) AdsGetTargetGroups(params Params) (response AdsGetTargetGroupsResp
// TODO: AdsGetTargetPixels ...
//
// https://vk.com/dev/ads.getTargetPixels
// https://dev.vk.com/method/ads.getTargetPixels
// func (vk *VK) AdsGetTargetPixels(params Params) (response AdsGetTargetPixelsResponse, err error) {
// err = vk.RequestUnmarshal("ads.getTargetPixels", &response, params)
// return
@ -461,7 +460,7 @@ func (vk *VK) AdsGetTargetGroups(params Params) (response AdsGetTargetGroupsResp
// TODO: AdsGetTargetingStats ...
//
// https://vk.com/dev/ads.getTargetingStats
// https://dev.vk.com/method/ads.getTargetingStats
// func (vk *VK) AdsGetTargetingStats(params Params) (response AdsGetTargetingStatsResponse, err error) {
// err = vk.RequestUnmarshal("ads.getTargetingStats", &response, params)
// return
@ -472,7 +471,7 @@ func (vk *VK) AdsGetTargetGroups(params Params) (response AdsGetTargetGroupsResp
// TODO: AdsGetUploadURL ...
//
// https://vk.com/dev/ads.getUploadURL
// https://dev.vk.com/method/ads.getUploadURL
// func (vk *VK) AdsGetUploadURL(params Params) (response AdsGetUploadURLResponse, err error) {
// err = vk.RequestUnmarshal("ads.getUploadURL", &response, params)
// return
@ -483,7 +482,7 @@ func (vk *VK) AdsGetTargetGroups(params Params) (response AdsGetTargetGroupsResp
// TODO: AdsGetVideoUploadURL ...
//
// https://vk.com/dev/ads.getVideoUploadURL
// https://dev.vk.com/method/ads.getVideoUploadURL
// func (vk *VK) AdsGetVideoUploadURL(params Params) (response AdsGetVideoUploadURLResponse, err error) {
// err = vk.RequestUnmarshal("ads.getVideoUploadURL", &response, params)
// return
@ -494,7 +493,7 @@ func (vk *VK) AdsGetTargetGroups(params Params) (response AdsGetTargetGroupsResp
// TODO: AdsImportTargetContacts ...
//
// https://vk.com/dev/ads.importTargetContacts
// https://dev.vk.com/method/ads.importTargetContacts
// func (vk *VK) AdsImportTargetContacts(params Params) (response AdsImportTargetContactsResponse, err error) {
// err = vk.RequestUnmarshal("ads.importTargetContacts", &response, params)
// return
@ -505,7 +504,7 @@ func (vk *VK) AdsGetTargetGroups(params Params) (response AdsGetTargetGroupsResp
// TODO: AdsRemoveOfficeUsers ...
//
// https://vk.com/dev/ads.removeOfficeUsers
// https://dev.vk.com/method/ads.removeOfficeUsers
// func (vk *VK) AdsRemoveOfficeUsers(params Params) (response AdsRemoveOfficeUsersResponse, err error) {
// err = vk.RequestUnmarshal("ads.removeOfficeUsers", &response, params)
// return
@ -519,7 +518,7 @@ func (vk *VK) AdsGetTargetGroups(params Params) (response AdsGetTargetGroupsResp
//
// Contacts are excluded within a few hours of the request.
//
// https://vk.com/dev/ads.removeTargetContacts
// https://dev.vk.com/method/ads.removeTargetContacts
func (vk *VK) AdsRemoveTargetContacts(params Params) (response int, err error) {
err = vk.RequestUnmarshal("ads.removeTargetContacts", &response, params)
return
@ -530,7 +529,7 @@ func (vk *VK) AdsRemoveTargetContacts(params Params) (response int, err error) {
// TODO: AdsSaveLookalikeRequestResult ...
//
// https://vk.com/dev/ads.saveLookalikeRequestResult
// https://dev.vk.com/method/ads.saveLookalikeRequestResult
// func (vk *VK) AdsSaveLookalikeRequestResult(params Params) (
// response AdsSaveLookalikeRequestResultResponse,
// err error,
@ -544,7 +543,7 @@ func (vk *VK) AdsRemoveTargetContacts(params Params) (response int, err error) {
// TODO: AdsShareTargetGroup ...
//
// https://vk.com/dev/ads.shareTargetGroup
// https://dev.vk.com/method/ads.shareTargetGroup
// func (vk *VK) AdsShareTargetGroup(params Params) (response AdsShareTargetGroupResponse, err error) {
// err = vk.RequestUnmarshal("ads.shareTargetGroup", &response, params)
// return
@ -555,7 +554,7 @@ func (vk *VK) AdsRemoveTargetContacts(params Params) (response int, err error) {
// TODO: AdsUpdateAds ...
//
// https://vk.com/dev/ads.updateAds
// https://dev.vk.com/method/ads.updateAds
// func (vk *VK) AdsUpdateAds(params Params) (response AdsUpdateAdsResponse, err error) {
// err = vk.RequestUnmarshal("ads.updateAds", &response, params)
// return
@ -566,7 +565,7 @@ func (vk *VK) AdsRemoveTargetContacts(params Params) (response int, err error) {
// TODO: AdsUpdateCampaigns ...
//
// https://vk.com/dev/ads.updateCampaigns
// https://dev.vk.com/method/ads.updateCampaigns
// func (vk *VK) AdsUpdateCampaigns(params Params) (response AdsUpdateCampaignsResponse, err error) {
// err = vk.RequestUnmarshal("ads.updateCampaigns", &response, params)
// return
@ -577,7 +576,7 @@ func (vk *VK) AdsRemoveTargetContacts(params Params) (response int, err error) {
// TODO: AdsUpdateClients ...
//
// https://vk.com/dev/ads.updateClients
// https://dev.vk.com/method/ads.updateClients
// func (vk *VK) AdsUpdateClients(params Params) (response AdsUpdateClientsResponse, err error) {
// err = vk.RequestUnmarshal("ads.updateClients", &response, params)
// return
@ -585,7 +584,7 @@ func (vk *VK) AdsRemoveTargetContacts(params Params) (response int, err error) {
// AdsUpdateTargetGroup edits target group.
//
// https://vk.com/dev/ads.updateTargetGroup
// https://dev.vk.com/method/ads.updateTargetGroup
func (vk *VK) AdsUpdateTargetGroup(params Params) (response int, err error) {
err = vk.RequestUnmarshal("ads.updateTargetGroup", &response, params)
return
@ -593,7 +592,7 @@ func (vk *VK) AdsUpdateTargetGroup(params Params) (response int, err error) {
// AdsUpdateTargetPixel edits target pixel.
//
// https://vk.com/dev/ads.updateTargetPixel
// https://dev.vk.com/method/ads.updateTargetPixel
func (vk *VK) AdsUpdateTargetPixel(params Params) (response int, err error) {
err = vk.RequestUnmarshal("ads. updateTargetPixel", &response, params)
return

View File

@ -1,7 +1,7 @@
/*
Package api implements VK API.
See more https://vk.com/dev/api_requests
See more https://dev.vk.com/ru/api/api-requests
*/
package api // import "github.com/SevereCloud/vksdk/v2/api"
@ -46,7 +46,7 @@ const (
// 1 000 000+ 35 requests.
//
// The ads section methods are subject to their own limitations,
// you can read them on this page - https://vk.com/dev/ads_limits
// you can read them on this page - https://dev.vk.com/ru/method/ads
//
// If one of this limits is exceeded, the server will return following error:
// "Too many requests per second". (errors.TooMany).
@ -58,7 +58,7 @@ const (
// quantitative restrictions on calling the same type of methods.
//
// After exceeding the quantitative limit, access to a particular method may
// require entering a captcha (see https://vk.com/dev/captcha_error),
// require entering a captcha (see https://dev.vk.com/ru/api/captcha-error),
// and may also be temporarily restricted (in this case, the server does
// not return a response to the call of a particular method, but handles
// any other requests without problems).
@ -78,7 +78,7 @@ const (
//
// captcha_key - text entered by the user.
//
// More info: https://vk.com/dev/api_requests
// More info: https://dev.vk.com/ru/api/api-requests
const (
LimitUserToken = 3
LimitGroupToken = 20
@ -152,7 +152,7 @@ type Params map[string]interface{}
// cyrillic symbols will be transliterated automatically.
// Numeric format from account.getInfo is supported as well.
//
// p.Lang(object.LangRU)
// p.Lang(object.LangRU)
//
// See all language code in module object.
func (p Params) Lang(v int) Params {
@ -169,7 +169,7 @@ func (p Params) TestMode(v bool) Params {
// CaptchaSID received ID.
//
// See https://vk.com/dev/captcha_error
// See https://dev.vk.com/ru/api/captcha-error
func (p Params) CaptchaSID(v string) Params {
p["captcha_sid"] = v
return p
@ -177,7 +177,7 @@ func (p Params) CaptchaSID(v string) Params {
// CaptchaKey text input.
//
// See https://vk.com/dev/captcha_error
// See https://dev.vk.com/ru/api/captcha-error
func (p Params) CaptchaKey(v string) Params {
p["captcha_key"] = v
return p
@ -185,7 +185,7 @@ func (p Params) CaptchaKey(v string) Params {
// Confirm parameter.
//
// See https://vk.com/dev/need_confirmation
// See https://dev.vk.com/ru/api/confirmation-required-error
func (p Params) Confirm(v bool) Params {
p["confirm"] = v
return p
@ -203,10 +203,13 @@ func buildQuery(sliceParams ...Params) (context.Context, url.Values) {
for _, params := range sliceParams {
for key, value := range params {
if key != ":context" {
query.Set(key, FmtValue(value, 0))
} else {
switch key {
case "access_token":
continue
case ":context":
ctx = value.(context.Context)
default:
query.Set(key, FmtValue(value, 0))
}
}
}
@ -238,6 +241,7 @@ func (vk *VK) DefaultHandler(method string, sliceParams ...Params) (Response, er
vk.lastTime = time.Now()
vk.rps = 0
}
vk.rps++
vk.mux.Unlock()
@ -245,7 +249,7 @@ func (vk *VK) DefaultHandler(method string, sliceParams ...Params) (Response, er
rawBody := bytes.NewBufferString(query.Encode())
req, err := http.NewRequestWithContext(ctx, "POST", u, rawBody)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, u, rawBody)
if err != nil {
return response, err
}
@ -255,6 +259,9 @@ func (vk *VK) DefaultHandler(method string, sliceParams ...Params) (Response, er
acceptEncoding = "zstd"
}
token := sliceParams[len(sliceParams)-1]["access_token"].(string)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("User-Agent", vk.UserAgent)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
@ -269,9 +276,15 @@ func (vk *VK) DefaultHandler(method string, sliceParams ...Params) (Response, er
switch resp.Header.Get("Content-Encoding") {
case "zstd":
reader, _ = zstd.NewReader(resp.Body)
zstdReader, _ := zstd.NewReader(resp.Body)
defer zstdReader.Close()
reader = zstdReader
case "gzip":
reader, _ = gzip.NewReader(resp.Body)
gzipReader, _ := gzip.NewReader(resp.Body)
defer gzipReader.Close()
reader = gzipReader
default:
reader = resp.Body
}

View File

@ -4,9 +4,17 @@ import (
"github.com/SevereCloud/vksdk/v2/object"
)
// AppsAddUsersToTestingGroup method.
//
// https://dev.vk.com/method/apps.addUsersToTestingGroup
func (vk *VK) AppsAddUsersToTestingGroup(params Params) (response int, err error) {
err = vk.RequestUnmarshal("apps.addUsersToTestingGroup", &response, params)
return
}
// AppsDeleteAppRequests deletes all request notifications from the current app.
//
// https://vk.com/dev/apps.deleteAppRequests
// https://dev.vk.com/method/apps.deleteAppRequests
func (vk *VK) AppsDeleteAppRequests(params Params) (response int, err error) {
err = vk.RequestUnmarshal("apps.deleteAppRequests", &response, params)
return
@ -21,7 +29,7 @@ type AppsGetResponse struct {
// AppsGet returns applications data.
//
// https://vk.com/dev/apps.get
// https://dev.vk.com/method/apps.get
func (vk *VK) AppsGet(params Params) (response AppsGetResponse, err error) {
err = vk.RequestUnmarshal("apps.get", &response, params)
return
@ -36,7 +44,7 @@ type AppsGetCatalogResponse struct {
// AppsGetCatalog returns a list of applications (apps) available to users in the App Catalog.
//
// https://vk.com/dev/apps.getCatalog
// https://dev.vk.com/method/apps.getCatalog
func (vk *VK) AppsGetCatalog(params Params) (response AppsGetCatalogResponse, err error) {
err = vk.RequestUnmarshal("apps.getCatalog", &response, params)
return
@ -50,9 +58,9 @@ type AppsGetFriendsListResponse struct {
// AppsGetFriendsList creates friends list for requests and invites in current app.
//
// extended=0
// extended=0
//
// https://vk.com/dev/apps.getFriendsList
// https://dev.vk.com/method/apps.getFriendsList
func (vk *VK) AppsGetFriendsList(params Params) (response AppsGetFriendsListResponse, err error) {
err = vk.RequestUnmarshal("apps.getFriendsList", &response, params, Params{"extended": false})
@ -67,9 +75,9 @@ type AppsGetFriendsListExtendedResponse struct {
// AppsGetFriendsListExtended creates friends list for requests and invites in current app.
//
// extended=1
// extended=1
//
// https://vk.com/dev/apps.getFriendsList
// https://dev.vk.com/method/apps.getFriendsList
func (vk *VK) AppsGetFriendsListExtended(params Params) (response AppsGetFriendsListExtendedResponse, err error) {
err = vk.RequestUnmarshal("apps.getFriendsList", &response, params, Params{"extended": true})
@ -84,9 +92,9 @@ type AppsGetLeaderboardResponse struct {
// AppsGetLeaderboard returns players rating in the game.
//
// extended=0
// extended=0
//
// https://vk.com/dev/apps.getLeaderboard
// https://dev.vk.com/method/apps.getLeaderboard
func (vk *VK) AppsGetLeaderboard(params Params) (response AppsGetLeaderboardResponse, err error) {
err = vk.RequestUnmarshal("apps.getLeaderboard", &response, params, Params{"extended": false})
@ -105,9 +113,9 @@ type AppsGetLeaderboardExtendedResponse struct {
// AppsGetLeaderboardExtended returns players rating in the game.
//
// extended=1
// extended=1
//
// https://vk.com/dev/apps.getLeaderboard
// https://dev.vk.com/method/apps.getLeaderboard
func (vk *VK) AppsGetLeaderboardExtended(params Params) (response AppsGetLeaderboardExtendedResponse, err error) {
err = vk.RequestUnmarshal("apps.getLeaderboard", &response, params, Params{"extended": true})
@ -124,7 +132,7 @@ type AppsGetScopesResponse struct {
//
// TODO: write docs.
//
// https://vk.com/dev/apps.getScopes
// https://dev.vk.com/method/apps.getScopes
func (vk *VK) AppsGetScopes(params Params) (response AppsGetScopesResponse, err error) {
err = vk.RequestUnmarshal("apps.getScopes", &response, params)
return
@ -134,16 +142,56 @@ func (vk *VK) AppsGetScopes(params Params) (response AppsGetScopesResponse, err
//
// NOTE: vk wtf!?
//
// https://vk.com/dev/apps.getScore
// https://dev.vk.com/method/apps.getScore
func (vk *VK) AppsGetScore(params Params) (response string, err error) {
err = vk.RequestUnmarshal("apps.getScore", &response, params)
return
}
// AppsGetTestingGroupsResponse struct.
type AppsGetTestingGroupsResponse []object.AppsTestingGroup
// AppsGetTestingGroups method.
//
// https://dev.vk.com/method/apps.getTestingGroups
func (vk *VK) AppsGetTestingGroups(params Params) (response AppsGetTestingGroupsResponse, err error) {
err = vk.RequestUnmarshal("apps.getTestingGroups", &response, params)
return
}
// AppsRemoveTestingGroup method.
//
// https://dev.vk.com/method/apps.removeTestingGroup
func (vk *VK) AppsRemoveTestingGroup(params Params) (response int, err error) {
err = vk.RequestUnmarshal("apps.removeTestingGroup", &response, params)
return
}
// AppsRemoveUsersFromTestingGroups method.
//
// https://dev.vk.com/method/apps.removeUsersFromTestingGroups
func (vk *VK) AppsRemoveUsersFromTestingGroups(params Params) (response int, err error) {
err = vk.RequestUnmarshal("apps.removeUsersFromTestingGroups", &response, params)
return
}
// AppsSendRequest sends a request to another user in an app that uses VK authorization.
//
// https://vk.com/dev/apps.sendRequest
// https://dev.vk.com/method/apps.sendRequest
func (vk *VK) AppsSendRequest(params Params) (response int, err error) {
err = vk.RequestUnmarshal("apps.sendRequest", &response, params)
return
}
// AppsUpdateMetaForTestingGroupResponse struct.
type AppsUpdateMetaForTestingGroupResponse struct {
GroupID int `json:"group_id"`
}
// AppsUpdateMetaForTestingGroup method.
//
// https://dev.vk.com/method/apps.updateMetaForTestingGroup
func (vk *VK) AppsUpdateMetaForTestingGroup(params Params) (response AppsUpdateMetaForTestingGroupResponse, err error) {
err = vk.RequestUnmarshal("apps.updateMetaForTestingGroup", &response, params)
return
}

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