1
0
forked from lug/matterbridge

Compare commits

..

57 Commits

Author SHA1 Message Date
Wim
65c7ac80b5 Release v1.24.0 (#1732) 2022-02-07 23:10:56 +01:00
dependabot[bot]
dd3fb32ec7 Bump github.com/SevereCloud/vksdk/v2 from 2.13.0 to 2.13.1 (#1730)
Bumps [github.com/SevereCloud/vksdk/v2](https://github.com/SevereCloud/vksdk) from 2.13.0 to 2.13.1.
- [Release notes](https://github.com/SevereCloud/vksdk/releases)
- [Commits](https://github.com/SevereCloud/vksdk/compare/v2.13.0...v2.13.1)

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

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

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

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

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

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

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

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

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

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

* Add debuglevel option

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

* Gofumpt

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

* Handle replies from matrix

* Include protocol in canonical ID return

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

* Updated: schedule interval

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

This is a change to handle comments in telegram messages!

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

This should fix the issue in #1649

* [fix] discord: send comments in extras

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

* support multi-line code blocks.

* remove import.

* handle code blocks in middle of normal text.

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

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

* Remove not neccessary line

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

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

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

* version: Move version information into a separate package.

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

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

Co-authored-by: Wim <wim@42.be>
2021-10-23 23:46:27 +02:00
Wim
b7d73077e5 Remove forbidigo linter 2021-10-23 23:25:15 +02:00
Wim
77f61ee20a Fix gozulipbot vendor 2021-10-23 23:20:34 +02:00
Wim
8967f02fc9 Update gozulipbot dependency (#1618) 2021-10-23 23:13:07 +02:00
Wim
831ff6d0a9 Update matterclient dep. Fixes #1617 2021-10-21 15:57:34 +02:00
Wim
2199174def Bump version 2021-10-18 23:40:20 +02:00
610 changed files with 111079 additions and 21231 deletions

22
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
# 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

@@ -35,9 +35,9 @@ jobs:
run: | run: |
mkdir -p output/{win,lin,arm,mac} mkdir -p output/{win,lin,arm,mac}
VERSION=$(git describe --tags) VERSION=$(git describe --tags)
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o output/lin/matterbridge-$VERSION-linux-amd64 CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/lin/matterbridge-$VERSION-linux-amd64
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o output/win/matterbridge-$VERSION-windows-amd64.exe CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/win/matterbridge-$VERSION-windows-amd64.exe
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -X main.githash=$(git log --pretty=format:'%h' -n 1)" -o output/mac/matterbridge-$VERSION-darwin-amd64 CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/mac/matterbridge-$VERSION-darwin-amd64
- name: Upload linux 64-bit - name: Upload linux 64-bit
if: startsWith(matrix.go-version,'1.17') if: startsWith(matrix.go-version,'1.17')
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2

3
.gitignore vendored
View File

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

View File

@@ -186,7 +186,24 @@ linters:
- errorlint - errorlint
- nlreturn - nlreturn
- exhaustivestruct - exhaustivestruct
- forbidigo
- wrapcheck
- varnamelen
- ireturn
- errorlint
- tparallel
- wrapcheck
- paralleltest
- makezero
- thelper
- cyclop
- revive
- importas
- gomoddirectives
- promlinter
- tagliatelle
- errname
- typecheck
# rules to deal with reported isues # rules to deal with reported isues
issues: issues:
# List of regexps of issue texts to exclude, empty list by default. # List of regexps of issue texts to exclude, empty list by default.

View File

@@ -22,7 +22,7 @@ builds:
- 6 - 6
- 7 - 7
ldflags: ldflags:
- -s -w -X main.githash={{.ShortCommit}} - -s -w -X github.com/42wim/matterbridge/version.GitHash={{.ShortCommit}}
archives: archives:
- -

View File

@@ -1,9 +1,9 @@
FROM alpine:edge AS builder FROM alpine AS builder
COPY . /go/src/matterbridge COPY . /go/src/matterbridge
RUN apk --no-cache add go git \ RUN apk --no-cache add go git \
&& cd /go/src/matterbridge \ && cd /go/src/matterbridge \
&& CGO_ENABLED=0 go build -mod vendor -ldflags "-X main.githash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge && CGO_ENABLED=0 go build -mod vendor -ldflags "-X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge
FROM alpine FROM alpine
RUN apk --no-cache add ca-certificates mailcap RUN apk --no-cache add ca-certificates mailcap

View File

@@ -164,7 +164,7 @@ See <https://github.com/42wim/matterbridge/wiki>
### Binaries ### Binaries
- Latest stable release [v1.23.0](https://github.com/42wim/matterbridge/releases/latest) - Latest stable release [v1.24.0](https://github.com/42wim/matterbridge/releases/latest)
- Development releases (follows master) can be downloaded [here](https://github.com/42wim/matterbridge/actions) selecting the latest green build and then artifacts. - Development releases (follows master) can be downloaded [here](https://github.com/42wim/matterbridge/actions) selecting the latest green build and then artifacts.
To install or upgrade just download the latest [binary](https://github.com/42wim/matterbridge/releases/latest). 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.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.
@@ -336,6 +336,7 @@ See [FAQ](https://github.com/42wim/matterbridge/wiki/FAQ)
- <https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/> - <https://daniele.tech/2019/02/how-to-use-matterbridge-to-connect-2-different-slack-workspaces/>
- <https://userlinux.net/mattermost-and-matterbridge.html> - <https://userlinux.net/mattermost-and-matterbridge.html>
- <https://nextcloud.com/blog/bridging-chat-services-in-talk/> - <https://nextcloud.com/blog/bridging-chat-services-in-talk/>
- <https://minecraftchest1.wordpress.com/2021/06/05/how-to-install-and-setup-matterbridge/>
- Youtube: [whatsapp - telegram bridging](https://www.youtube.com/watch?v=W-VXISoKtNc) - Youtube: [whatsapp - telegram bridging](https://www.youtube.com/watch?v=W-VXISoKtNc)
## Thanks ## Thanks

View File

@@ -23,6 +23,7 @@ const (
EventRejoinChannels = "rejoin_channels" EventRejoinChannels = "rejoin_channels"
EventUserAction = "user_action" EventUserAction = "user_action"
EventMsgDelete = "msg_delete" EventMsgDelete = "msg_delete"
EventFileDelete = "file_delete"
EventAPIConnected = "api_connected" EventAPIConnected = "api_connected"
EventUserTyping = "user_typing" EventUserTyping = "user_typing"
EventGetChannelMembers = "get_channel_members" EventGetChannelMembers = "get_channel_members"
@@ -63,6 +64,7 @@ type FileInfo struct {
Size int64 Size int64
Avatar bool Avatar bool
SHA string SHA string
NativeID string
} }
type ChannelInfo struct { type ChannelInfo struct {
@@ -168,7 +170,7 @@ type Protocol struct {
UseTLS bool // IRC UseTLS bool // IRC
UseDiscriminator bool // discord UseDiscriminator bool // discord
UseFirstName bool // telegram UseFirstName bool // telegram
UseUserName bool // discord, matrix UseUserName bool // discord, matrix, mattermost
UseInsecureURL bool // telegram UseInsecureURL bool // telegram
UserName string // IRC UserName string // IRC
VerboseJoinPart bool // IRC VerboseJoinPart bool // IRC

View File

@@ -10,10 +10,14 @@ import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/discord/transmitter" "github.com/42wim/matterbridge/bridge/discord/transmitter"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
lru "github.com/hashicorp/golang-lru"
"github.com/matterbridge/discordgo" "github.com/matterbridge/discordgo"
) )
const MessageLength = 1950 const (
MessageLength = 1950
cFileUpload = "file_upload"
)
type Bdiscord struct { type Bdiscord struct {
*bridge.Config *bridge.Config
@@ -35,10 +39,20 @@ type Bdiscord struct {
// Webhook specific logic // Webhook specific logic
useAutoWebhooks bool useAutoWebhooks bool
transmitter *transmitter.Transmitter transmitter *transmitter.Transmitter
cache *lru.Cache
} }
func New(cfg *bridge.Config) bridge.Bridger { func New(cfg *bridge.Config) bridge.Bridger {
b := &Bdiscord{Config: cfg} newCache, err := lru.New(5000)
if err != nil {
cfg.Log.Fatalf("Could not create LRU cache: %v", err)
}
b := &Bdiscord{
Config: cfg,
cache: newCache,
}
b.userMemberMap = make(map[string]*discordgo.Member) b.userMemberMap = make(map[string]*discordgo.Member)
b.nickMemberMap = make(map[string]*discordgo.Member) b.nickMemberMap = make(map[string]*discordgo.Member)
b.channelInfoMap = make(map[string]*config.ChannelInfo) b.channelInfoMap = make(map[string]*config.ChannelInfo)
@@ -75,6 +89,9 @@ func (b *Bdiscord) Connect() error {
b.c.AddHandler(b.messageDeleteBulk) b.c.AddHandler(b.messageDeleteBulk)
b.c.AddHandler(b.memberAdd) b.c.AddHandler(b.memberAdd)
b.c.AddHandler(b.memberRemove) 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 // Add privileged intent for guild member tracking. This is needed to track nicks
// for display names and @mention translation // for display names and @mention translation
b.c.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged | b.c.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged |
@@ -153,7 +170,7 @@ func (b *Bdiscord) Connect() error {
return fmt.Errorf("use of removed WebhookURL setting") return fmt.Errorf("use of removed WebhookURL setting")
} }
if b.GetInt("debuglevel") > 0 { if b.GetInt("debuglevel") == 2 {
b.Log.Debug("enabling even more discord debug") b.Log.Debug("enabling even more discord debug")
b.c.Debug = true b.c.Debug = true
} }
@@ -280,6 +297,21 @@ func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (st
return "", err return "", err
} }
// Delete a file
if msg.Event == config.EventFileDelete {
if msg.ID == "" {
return "", nil
}
if fi, ok := b.cache.Get(cFileUpload + msg.ID); ok {
err := b.c.ChannelMessageDelete(channelID, fi.(string)) // nolint:forcetypeassert
b.cache.Remove(cFileUpload + msg.ID)
return "", err
}
return "", fmt.Errorf("file %s not found", msg.ID)
}
// Upload a file if it exists // Upload a file if it exists
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(msg, b.General) { for _, rmsg := range helper.HandleExtra(msg, b.General) {
@@ -327,7 +359,6 @@ func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (st
// handleUploadFile handles native upload of files // handleUploadFile handles native upload of files
func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (string, error) { func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (string, error) {
var err error
for _, f := range msg.Extra["file"] { for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo) fi := f.(config.FileInfo)
file := discordgo.File{ file := discordgo.File{
@@ -340,10 +371,15 @@ func (b *Bdiscord) handleUploadFile(msg *config.Message, channelID string) (stri
Files: []*discordgo.File{&file}, Files: []*discordgo.File{&file},
AllowedMentions: b.getAllowedMentions(), AllowedMentions: b.getAllowedMentions(),
} }
_, err = b.c.ChannelMessageSendComplex(channelID, &m) res, err := b.c.ChannelMessageSendComplex(channelID, &m)
if err != nil { if err != nil {
return "", fmt.Errorf("file upload failed: %s", err) return "", fmt.Errorf("file upload failed: %s", err)
} }
// link file_upload_nativeID (file ID from the original bridge) to our upload id
// so that we can remove this later when it eg needs to be deleted
b.cache.Add(cFileUpload+fi.NativeID, res.ID)
} }
return "", nil return "", nil
} }

View File

@@ -2,6 +2,7 @@ package bdiscord
import ( import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/davecgh/go-spew/spew"
"github.com/matterbridge/discordgo" "github.com/matterbridge/discordgo"
) )
@@ -31,6 +32,10 @@ func (b *Bdiscord) messageDeleteBulk(s *discordgo.Session, m *discordgo.MessageD
} }
} }
func (b *Bdiscord) messageEvent(s *discordgo.Session, m *discordgo.Event) {
b.Log.Debug(spew.Sdump(m.Struct))
}
func (b *Bdiscord) messageTyping(s *discordgo.Session, m *discordgo.TypingStart) { func (b *Bdiscord) messageTyping(s *discordgo.Session, m *discordgo.TypingStart) {
if !b.GetBool("ShowUserTyping") { if !b.GetBool("ShowUserTyping") {
return return
@@ -82,8 +87,9 @@ func (b *Bdiscord) messageCreate(s *discordgo.Session, m *discordgo.MessageCreat
rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg", UserID: m.Author.ID, ID: m.ID} rmsg := config.Message{Account: b.Account, Avatar: "https://cdn.discordapp.com/avatars/" + m.Author.ID + "/" + m.Author.Avatar + ".jpg", UserID: m.Author.ID, ID: m.ID}
if m.Content != "" {
b.Log.Debugf("== Receiving event %#v", m.Message) b.Log.Debugf("== Receiving event %#v", m.Message)
if m.Content != "" {
m.Message.Content = b.replaceChannelMentions(m.Message.Content) m.Message.Content = b.replaceChannelMentions(m.Message.Content)
rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c) rmsg.Text, err = m.ContentWithMoreMentionsReplaced(b.c)
if err != nil { if err != nil {

View File

@@ -82,10 +82,8 @@ func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordg
ContentType: "", ContentType: "",
Reader: bytes.NewReader(*fi.Data), Reader: bytes.NewReader(*fi.Data),
} }
content := "" content := fi.Comment
if msg.Text == "" {
content = fi.Comment
}
_, e2 := b.transmitter.Send( _, e2 := b.transmitter.Send(
channelID, channelID,
&discordgo.WebhookParams{ &discordgo.WebhookParams{

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

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

View File

@@ -168,6 +168,11 @@ func HandleDownloadSize(logger *logrus.Entry, msg *config.Message, name string,
// HandleDownloadData adds the data for a remote file into a Matterbridge gateway message. // HandleDownloadData adds the data for a remote file into a Matterbridge gateway message.
func HandleDownloadData(logger *logrus.Entry, msg *config.Message, name, comment, url string, data *[]byte, general *config.Protocol) { func HandleDownloadData(logger *logrus.Entry, msg *config.Message, name, comment, url string, data *[]byte, general *config.Protocol) {
HandleDownloadData2(logger, msg, name, "", comment, url, data, general)
}
// HandleDownloadData adds the data for a remote file into a Matterbridge gateway message.
func HandleDownloadData2(logger *logrus.Entry, msg *config.Message, name, id, comment, url string, data *[]byte, general *config.Protocol) {
var avatar bool var avatar bool
logger.Debugf("Download OK %#v %#v", name, len(*data)) logger.Debugf("Download OK %#v %#v", name, len(*data))
if msg.Event == config.EventAvatarDownload { if msg.Event == config.EventAvatarDownload {
@@ -179,6 +184,7 @@ func HandleDownloadData(logger *logrus.Entry, msg *config.Message, name, comment
URL: url, URL: url,
Comment: comment, Comment: comment,
Avatar: avatar, Avatar: avatar,
NativeID: id,
}) })
} }

View File

@@ -2,6 +2,7 @@ package birc
import ( import (
"crypto/tls" "crypto/tls"
"errors"
"fmt" "fmt"
"hash/crc32" "hash/crc32"
"io/ioutil" "io/ioutil"
@@ -72,6 +73,10 @@ func (b *Birc) Command(msg *config.Message) string {
} }
func (b *Birc) Connect() error { func (b *Birc) Connect() error {
if b.GetBool("UseSASL") && b.GetString("TLSClientCertificate") != "" {
return errors.New("you can't enable SASL and TLSClientCertificate at the same time")
}
b.Local = make(chan config.Message, b.MessageQueue+10) b.Local = make(chan config.Message, b.MessageQueue+10)
b.Log.Infof("Connecting %s", b.GetString("Server")) b.Log.Infof("Connecting %s", b.GetString("Server"))
@@ -300,6 +305,11 @@ func (b *Birc) getClient() (*girc.Client, error) {
b.Log.Debugf("setting pingdelay to %s", pingDelay) b.Log.Debugf("setting pingdelay to %s", pingDelay)
tlsConfig, err := b.getTLSConfig()
if err != nil {
return nil, err
}
i := girc.New(girc.Config{ i := girc.New(girc.Config{
Server: server, Server: server,
ServerPass: b.GetString("Password"), ServerPass: b.GetString("Password"),
@@ -308,7 +318,8 @@ func (b *Birc) getClient() (*girc.Client, error) {
User: user, User: user,
Name: realName, Name: realName,
SSL: b.GetBool("UseTLS"), SSL: b.GetBool("UseTLS"),
TLSConfig: &tls.Config{InsecureSkipVerify: b.GetBool("SkipTLSVerify"), ServerName: server}, //nolint:gosec Bind: b.GetString("Bind"),
TLSConfig: tlsConfig,
PingDelay: pingDelay, PingDelay: pingDelay,
// skip gIRC internal rate limiting, since we have our own throttling // skip gIRC internal rate limiting, since we have our own throttling
AllowFlood: true, AllowFlood: true,
@@ -380,3 +391,23 @@ func (b *Birc) storeNames(client *girc.Client, event girc.Event) {
func (b *Birc) formatnicks(nicks []string) string { func (b *Birc) formatnicks(nicks []string) string {
return strings.Join(nicks, ", ") + " currently on IRC" return strings.Join(nicks, ", ") + " currently on IRC"
} }
func (b *Birc) getTLSConfig() (*tls.Config, error) {
server, _, _ := net.SplitHostPort(b.GetString("server"))
tlsConfig := &tls.Config{
InsecureSkipVerify: b.GetBool("skiptlsverify"), //nolint:gosec
ServerName: server,
}
if filename := b.GetString("TLSClientCertificate"); filename != "" {
cert, err := tls.LoadX509KeyPair(filename, filename)
if err != nil {
return nil, err
}
tlsConfig.Certificates = []tls.Certificate{cert}
}
return tlsConfig, nil
}

View File

@@ -50,6 +50,8 @@ type matrixUsername struct {
type SubTextMessage struct { type SubTextMessage struct {
MsgType string `json:"msgtype"` MsgType string `json:"msgtype"`
Body string `json:"body"` Body string `json:"body"`
FormattedBody string `json:"formatted_body,omitempty"`
Format string `json:"format,omitempty"`
} }
// MessageRelation explains how the current message relates to a previous message. // MessageRelation explains how the current message relates to a previous message.
@@ -65,6 +67,19 @@ type EditedMessage struct {
matrix.TextMessage matrix.TextMessage
} }
type InReplyToRelationContent struct {
EventID string `json:"event_id"`
}
type InReplyToRelation struct {
InReplyTo InReplyToRelationContent `json:"m.in_reply_to"`
}
type ReplyMessage struct {
RelatedTo InReplyToRelation `json:"m.relates_to"`
matrix.TextMessage
}
func New(cfg *bridge.Config) bridge.Bridger { func New(cfg *bridge.Config) bridge.Bridger {
b := &Bmatrix{Config: cfg} b := &Bmatrix{Config: cfg}
b.RoomMap = make(map[string]string) b.RoomMap = make(map[string]string)
@@ -138,7 +153,13 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
m := matrix.TextMessage{ m := matrix.TextMessage{
MsgType: "m.emote", MsgType: "m.emote",
Body: username.plain + msg.Text, Body: username.plain + msg.Text,
FormattedBody: username.formatted + msg.Text, FormattedBody: username.formatted + helper.ParseMarkdown(msg.Text),
Format: "org.matrix.custom.html",
}
if b.GetBool("HTMLDisable") {
m.Format = ""
m.FormattedBody = ""
} }
msgID := "" msgID := ""
@@ -201,20 +222,29 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
// Edit message if we have an ID // Edit message if we have an ID
if msg.ID != "" { if msg.ID != "" {
rmsg := EditedMessage{TextMessage: matrix.TextMessage{ rmsg := EditedMessage{
TextMessage: matrix.TextMessage{
Body: username.plain + msg.Text, Body: username.plain + msg.Text,
MsgType: "m.text", MsgType: "m.text",
}} Format: "org.matrix.custom.html",
if b.GetBool("HTMLDisable") { FormattedBody: username.formatted + helper.ParseMarkdown(msg.Text),
rmsg.TextMessage.FormattedBody = username.formatted + "* " + msg.Text },
} else {
rmsg.Format = "org.matrix.custom.html"
rmsg.TextMessage.FormattedBody = username.formatted + "* " + helper.ParseMarkdown(msg.Text)
} }
rmsg.NewContent = SubTextMessage{ rmsg.NewContent = SubTextMessage{
Body: rmsg.TextMessage.Body, Body: rmsg.TextMessage.Body,
FormattedBody: rmsg.TextMessage.FormattedBody,
Format: rmsg.TextMessage.Format,
MsgType: "m.text", MsgType: "m.text",
} }
if b.GetBool("HTMLDisable") {
rmsg.TextMessage.Format = ""
rmsg.TextMessage.FormattedBody = ""
rmsg.NewContent.Format = ""
rmsg.NewContent.FormattedBody = ""
}
rmsg.RelatedTo = MessageRelation{ rmsg.RelatedTo = MessageRelation{
EventID: msg.ID, EventID: msg.ID,
Type: "m.replace", Type: "m.replace",
@@ -238,6 +268,50 @@ func (b *Bmatrix) Send(msg config.Message) (string, error) {
MsgType: "m.notice", MsgType: "m.notice",
Body: username.plain + msg.Text, Body: username.plain + msg.Text,
FormattedBody: username.formatted + msg.Text, FormattedBody: username.formatted + msg.Text,
Format: "org.matrix.custom.html",
}
if b.GetBool("HTMLDisable") {
m.Format = ""
m.FormattedBody = ""
}
var (
resp *matrix.RespSendEvent
err error
)
err = b.retry(func() error {
resp, err = b.mc.SendMessageEvent(channel, "m.room.message", m)
return err
})
if err != nil {
return "", err
}
return resp.EventID, err
}
if msg.ParentValid() {
m := ReplyMessage{
TextMessage: matrix.TextMessage{
MsgType: "m.text",
Body: username.plain + msg.Text,
FormattedBody: username.formatted + helper.ParseMarkdown(msg.Text),
Format: "org.matrix.custom.html",
},
}
if b.GetBool("HTMLDisable") {
m.TextMessage.Format = ""
m.TextMessage.FormattedBody = ""
}
m.RelatedTo = InReplyToRelation{
InReplyTo: InReplyToRelationContent{
EventID: msg.ParentID,
},
} }
var ( var (
@@ -301,6 +375,9 @@ func (b *Bmatrix) handlematrix() {
syncer.OnEventType("m.room.member", b.handleMemberChange) syncer.OnEventType("m.room.member", b.handleMemberChange)
go func() { go func() {
for { for {
if b == nil {
return
}
if err := b.mc.Sync(); err != nil { if err := b.mc.Sync(); err != nil {
b.Log.Println("Sync() returned ", err) b.Log.Println("Sync() returned ", err)
} }
@@ -338,6 +415,35 @@ func (b *Bmatrix) handleEdit(ev *matrix.Event, rmsg config.Message) bool {
return true return true
} }
func (b *Bmatrix) handleReply(ev *matrix.Event, rmsg config.Message) bool {
relationInterface, present := ev.Content["m.relates_to"]
if !present {
return false
}
var relation InReplyToRelation
if err := interface2Struct(relationInterface, &relation); err != nil {
// probably fine
return false
}
body := rmsg.Text
for strings.HasPrefix(body, "> ") {
lineIdx := strings.IndexRune(body, '\n')
if lineIdx == -1 {
body = ""
} else {
body = body[(lineIdx + 1):]
}
}
rmsg.Text = body
rmsg.ParentID = relation.InReplyTo.EventID
b.Remote <- rmsg
return true
}
func (b *Bmatrix) handleMemberChange(ev *matrix.Event) { func (b *Bmatrix) handleMemberChange(ev *matrix.Event) {
// Update the displayname on join messages, according to https://matrix.org/docs/spec/client_server/r0.6.1#events-on-change-of-profile-information // Update the displayname on join messages, according to https://matrix.org/docs/spec/client_server/r0.6.1#events-on-change-of-profile-information
if ev.Content["membership"] == "join" { if ev.Content["membership"] == "join" {
@@ -400,6 +506,11 @@ func (b *Bmatrix) handleEvent(ev *matrix.Event) {
return return
} }
// Is it a reply?
if b.handleReply(ev, rmsg) {
return
}
// Do we have attachments // Do we have attachments
if b.containsAttachment(ev.Content) { if b.containsAttachment(ev.Content) {
err := b.handleDownloadFile(&rmsg, ev.Content) err := b.handleDownloadFile(&rmsg, ev.Content)

View File

@@ -140,9 +140,14 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
continue continue
} }
channelName := b.getChannelName(message.Post.ChannelId)
if channelName == "" {
channelName = message.Channel
}
// only download avatars if we have a place to upload them (configured mediaserver) // only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" { if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" {
b.handleDownloadAvatar(message.UserID, message.Channel) b.handleDownloadAvatar(message.UserID, channelName)
} }
b.Log.Debugf("== Receiving event %#v", message) b.Log.Debugf("== Receiving event %#v", message)
@@ -150,7 +155,7 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
rmsg := &config.Message{ rmsg := &config.Message{
Username: message.Username, Username: message.Username,
UserID: message.UserID, UserID: message.UserID,
Channel: message.Channel, Channel: channelName,
Text: message.Text, Text: message.Text,
ID: message.Post.Id, ID: message.Post.Id,
ParentID: message.Post.RootId, // ParentID is obsolete with mattermost ParentID: message.Post.RootId, // ParentID is obsolete with mattermost
@@ -177,9 +182,11 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
} }
// Use nickname instead of username if defined // Use nickname instead of username if defined
if !b.GetBool("useusername") {
if nick := b.mc.GetNickName(rmsg.UserID); nick != "" { if nick := b.mc.GetNickName(rmsg.UserID); nick != "" {
rmsg.Username = nick rmsg.Username = nick
} }
}
messages <- rmsg messages <- rmsg
} }
@@ -188,16 +195,21 @@ func (b *Bmattermost) handleMatterClient(messages chan *config.Message) {
// nolint:cyclop // nolint:cyclop
func (b *Bmattermost) handleMatterClient6(messages chan *config.Message) { func (b *Bmattermost) handleMatterClient6(messages chan *config.Message) {
for message := range b.mc6.MessageChan { for message := range b.mc6.MessageChan {
b.Log.Debugf("%#v", message.Raw.GetData()) b.Log.Debugf("%#v %#v", message.Raw.GetData(), message.Raw.EventType())
if b.skipMessage6(message) { if b.skipMessage6(message) {
b.Log.Debugf("Skipped message: %#v", message) b.Log.Debugf("Skipped message: %#v", message)
continue continue
} }
channelName := b.getChannelName(message.Post.ChannelId)
if channelName == "" {
channelName = message.Channel
}
// only download avatars if we have a place to upload them (configured mediaserver) // only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" { if b.General.MediaServerUpload != "" || b.General.MediaDownloadPath != "" {
b.handleDownloadAvatar(message.UserID, message.Channel) b.handleDownloadAvatar(message.UserID, channelName)
} }
b.Log.Debugf("== Receiving event %#v", message) b.Log.Debugf("== Receiving event %#v", message)
@@ -205,7 +217,7 @@ func (b *Bmattermost) handleMatterClient6(messages chan *config.Message) {
rmsg := &config.Message{ rmsg := &config.Message{
Username: message.Username, Username: message.Username,
UserID: message.UserID, UserID: message.UserID,
Channel: message.Channel, Channel: channelName,
Text: message.Text, Text: message.Text,
ID: message.Post.Id, ID: message.Post.Id,
ParentID: message.Post.RootId, // ParentID is obsolete with mattermost ParentID: message.Post.RootId, // ParentID is obsolete with mattermost
@@ -232,9 +244,11 @@ func (b *Bmattermost) handleMatterClient6(messages chan *config.Message) {
} }
// Use nickname instead of username if defined // Use nickname instead of username if defined
if !b.GetBool("useusername") {
if nick := b.mc6.GetNickName(rmsg.UserID); nick != "" { if nick := b.mc6.GetNickName(rmsg.UserID); nick != "" {
rmsg.Username = nick rmsg.Username = nick
} }
}
messages <- rmsg messages <- rmsg
} }
@@ -244,6 +258,7 @@ func (b *Bmattermost) handleMatterHook(messages chan *config.Message) {
for { for {
message := b.mh.Receive() message := b.mh.Receive()
b.Log.Debugf("Receiving from matterhook %#v", message) b.Log.Debugf("Receiving from matterhook %#v", message)
messages <- &config.Message{ messages <- &config.Message{
UserID: message.UserID, UserID: message.UserID,
Username: message.UserName, Username: message.UserName,
@@ -261,7 +276,7 @@ func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
var err error var err error
var res, id string var res, id string
channelID := b.mc.GetChannelId(msg.Channel, b.TeamID) channelID := b.getChannelID(msg.Channel)
for _, f := range msg.Extra["file"] { for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo) fi := f.(config.FileInfo)
id, err = b.mc.UploadFile(*fi.Data, channelID, fi.Name) id, err = b.mc.UploadFile(*fi.Data, channelID, fi.Name)
@@ -281,7 +296,7 @@ func (b *Bmattermost) handleUploadFile(msg *config.Message) (string, error) {
func (b *Bmattermost) handleUploadFile6(msg *config.Message) (string, error) { func (b *Bmattermost) handleUploadFile6(msg *config.Message) (string, error) {
var err error var err error
var res, id string var res, id string
channelID := b.mc6.GetChannelID(msg.Channel, b.TeamID) channelID := b.getChannelID(msg.Channel)
for _, f := range msg.Extra["file"] { for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo) fi := f.(config.FileInfo)
id, err = b.mc6.UploadFile(*fi.Data, channelID, fi.Name) id, err = b.mc6.UploadFile(*fi.Data, channelID, fi.Name)

View File

@@ -241,11 +241,17 @@ func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
if b.GetBool("nosendjoinpart") { if b.GetBool("nosendjoinpart") {
return true return true
} }
channelName := b.getChannelName(message.Post.ChannelId)
if channelName == "" {
channelName = message.Channel
}
b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account) b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
b.Remote <- config.Message{ b.Remote <- config.Message{
Username: "system", Username: "system",
Text: message.Text, Text: message.Text,
Channel: message.Channel, Channel: channelName,
Account: b.Account, Account: b.Account,
Event: config.EventJoinLeave, Event: config.EventJoinLeave,
} }
@@ -304,11 +310,17 @@ func (b *Bmattermost) skipMessage6(message *matterclient6.Message) bool {
if b.GetBool("nosendjoinpart") { if b.GetBool("nosendjoinpart") {
return true return true
} }
channelName := b.getChannelName(message.Post.ChannelId)
if channelName == "" {
channelName = message.Channel
}
b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account) b.Log.Debugf("Sending JOIN_LEAVE event from %s to gateway", b.Account)
b.Remote <- config.Message{ b.Remote <- config.Message{
Username: "system", Username: "system",
Text: message.Text, Text: message.Text,
Channel: message.Channel, Channel: channelName,
Account: b.Account, Account: b.Account,
Event: config.EventJoinLeave, Event: config.EventJoinLeave,
} }
@@ -329,13 +341,14 @@ func (b *Bmattermost) skipMessage6(message *matterclient6.Message) bool {
// Ignore messages sent from matterbridge // Ignore messages sent from matterbridge
if message.Post.Props != nil { if message.Post.Props != nil {
if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok { if _, ok := message.Post.Props["matterbridge_"+b.uuid].(bool); ok {
b.Log.Debugf("sent by matterbridge, ignoring") b.Log.Debug("sent by matterbridge, ignoring")
return true return true
} }
} }
// Ignore messages sent from a user logged in as the bot // Ignore messages sent from a user logged in as the bot
if b.mc6.User.Username == message.Username { if b.mc6.User.Username == message.Username {
b.Log.Debug("message from same user as bot, ignoring")
return true return true
} }
@@ -346,6 +359,7 @@ func (b *Bmattermost) skipMessage6(message *matterclient6.Message) bool {
// ignore messages from other teams than ours // ignore messages from other teams than ours
if message.Raw.GetData()["team_id"].(string) != b.TeamID { if message.Raw.GetData()["team_id"].(string) != b.TeamID {
b.Log.Debug("message from other team, ignoring")
return true return true
} }
@@ -374,3 +388,30 @@ func (b *Bmattermost) getVersion() string {
return resp.Header.Get("X-Version-Id") return resp.Header.Get("X-Version-Id")
} }
func (b *Bmattermost) getChannelID(name string) string {
idcheck := strings.Split(name, "ID:")
if len(idcheck) > 1 {
return idcheck[1]
}
if b.mc6 != nil {
return b.mc6.GetChannelID(name, b.TeamID)
}
return b.mc.GetChannelId(name, b.TeamID)
}
func (b *Bmattermost) getChannelName(id string) string {
b.channelsMutex.RLock()
defer b.channelsMutex.RUnlock()
for _, c := range b.channelInfoMap {
if c.Name == "ID:"+id {
// if we have ID: specified in our gateway configuration return this
return c.Name
}
}
return ""
}

View File

@@ -4,6 +4,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"sync"
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
@@ -23,12 +24,18 @@ type Bmattermost struct {
TeamID string TeamID string
*bridge.Config *bridge.Config
avatarMap map[string]string avatarMap map[string]string
channelsMutex sync.RWMutex
channelInfoMap map[string]*config.ChannelInfo
} }
const mattermostPlugin = "mattermost.plugin" const mattermostPlugin = "mattermost.plugin"
func New(cfg *bridge.Config) bridge.Bridger { func New(cfg *bridge.Config) bridge.Bridger {
b := &Bmattermost{Config: cfg, avatarMap: make(map[string]string)} b := &Bmattermost{
Config: cfg,
avatarMap: make(map[string]string),
channelInfoMap: make(map[string]*config.ChannelInfo),
}
b.v6 = b.GetBool("v6") b.v6 = b.GetBool("v6")
b.uuid = xid.New().String() b.uuid = xid.New().String()
@@ -113,14 +120,14 @@ func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error {
if b.Account == mattermostPlugin { if b.Account == mattermostPlugin {
return nil return nil
} }
b.channelsMutex.Lock()
b.channelInfoMap[channel.ID] = &channel
b.channelsMutex.Unlock()
// we can only join channels using the API // we can only join channels using the API
if b.GetString("WebhookURL") == "" && b.GetString("WebhookBindAddress") == "" { if b.GetString("WebhookURL") == "" && b.GetString("WebhookBindAddress") == "" {
var id string id := b.getChannelID(channel.Name)
if b.mc6 != nil {
id = b.mc6.GetChannelID(channel.Name, b.TeamID)
} else {
id = b.mc.GetChannelId(channel.Name, b.TeamID)
}
if id == "" { if id == "" {
return fmt.Errorf("Could not find channel ID for channel %s", channel.Name) return fmt.Errorf("Could not find channel ID for channel %s", channel.Name)
} }
@@ -131,6 +138,7 @@ func (b *Bmattermost) JoinChannel(channel config.ChannelInfo) error {
return b.mc.JoinChannel(id) return b.mc.JoinChannel(id)
} }
return nil return nil
} }
@@ -180,25 +188,29 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
if err != nil { if err != nil {
b.Log.Errorf("getting post %s failed: %s", msg.ParentID, err) b.Log.Errorf("getting post %s failed: %s", msg.ParentID, err)
} }
if post.RootId != "" {
msg.ParentID = post.RootId msg.ParentID = post.RootId
}
} else { } else {
post, res := b.mc.Client.GetPost(msg.ParentID, "") post, res := b.mc.Client.GetPost(msg.ParentID, "")
if res.Error != nil { if res.Error != nil {
b.Log.Errorf("getting post %s failed: %s", msg.ParentID, res.Error.DetailedError) b.Log.Errorf("getting post %s failed: %s", msg.ParentID, res.Error.DetailedError)
} }
if post.RootId != "" {
msg.ParentID = post.RootId msg.ParentID = post.RootId
} }
} }
}
// Upload a file if it exists // Upload a file if it exists
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) { for _, rmsg := range helper.HandleExtra(&msg, b.General) {
if b.mc6 != nil { if b.mc6 != nil {
if _, err := b.mc6.PostMessage(b.mc.GetChannelId(rmsg.Channel, b.TeamID), rmsg.Username+rmsg.Text, msg.ParentID); err != 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) b.Log.Errorf("PostMessage failed: %s", err)
} }
} else { } else {
if _, err := b.mc.PostMessage(b.mc.GetChannelId(rmsg.Channel, b.TeamID), rmsg.Username+rmsg.Text, msg.ParentID); err != nil { if _, err := b.mc.PostMessage(b.getChannelID(rmsg.Channel), rmsg.Username+rmsg.Text, msg.ParentID); err != nil {
b.Log.Errorf("PostMessage failed: %s", err) b.Log.Errorf("PostMessage failed: %s", err)
} }
} }
@@ -224,8 +236,8 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
// Post normal message // Post normal message
if b.mc6 != nil { if b.mc6 != nil {
return b.mc6.PostMessage(b.mc6.GetChannelID(msg.Channel, b.TeamID), msg.Text, msg.ParentID) // nolint:wrapcheck return b.mc6.PostMessage(b.getChannelID(msg.Channel), msg.Text, msg.ParentID) // nolint:wrapcheck
} }
return b.mc.PostMessage(b.mc.GetChannelId(msg.Channel, b.TeamID), msg.Text, msg.ParentID) return b.mc.PostMessage(b.getChannelID(msg.Channel), msg.Text, msg.ParentID)
} }

View File

@@ -19,8 +19,10 @@ import (
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
var defaultScopes = []string{"openid", "profile", "offline_access", "Group.Read.All", "Group.ReadWrite.All"} var (
var attachRE = regexp.MustCompile(`<attachment id=.*?attachment>`) defaultScopes = []string{"openid", "profile", "offline_access", "Group.Read.All", "Group.ReadWrite.All"}
attachRE = regexp.MustCompile(`<attachment id=.*?attachment>`)
)
type Bmsteams struct { type Bmsteams struct {
gc *msgraph.GraphServiceRequestBuilder gc *msgraph.GraphServiceRequestBuilder
@@ -50,7 +52,7 @@ func (b *Bmsteams) Connect() error {
b.Log.Errorf("Couldn't save sessionfile in %s: %s", tokenCachePath, err) b.Log.Errorf("Couldn't save sessionfile in %s: %s", tokenCachePath, err)
} }
// make file readable only for matterbridge user // make file readable only for matterbridge user
err = os.Chmod(tokenCachePath, 0600) err = os.Chmod(tokenCachePath, 0o600)
if err != nil { if err != nil {
b.Log.Errorf("Couldn't change permissions for %s: %s", tokenCachePath, err) b.Log.Errorf("Couldn't change permissions for %s: %s", tokenCachePath, err)
} }
@@ -168,7 +170,7 @@ func (b *Bmsteams) poll(channelName string) error {
} }
// skip non-user message for now. // skip non-user message for now.
if msg.From.User == nil { if msg.From == nil || msg.From.User == nil {
continue continue
} }

View File

@@ -27,7 +27,8 @@ func (b *Bslack) handleSlack() {
b.Log.Debug("Start listening for Slack messages") b.Log.Debug("Start listening for Slack messages")
for message := range messages { for message := range messages {
// don't do any action on deleted/typing messages // don't do any action on deleted/typing messages
if message.Event != config.EventUserTyping && message.Event != config.EventMsgDelete { if message.Event != config.EventUserTyping && message.Event != config.EventMsgDelete &&
message.Event != config.EventFileDelete {
b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account) b.Log.Debugf("<= Sending message from %s on %s to gateway", message.Username, b.Account)
// cleanup the message // cleanup the message
message.Text = b.replaceMention(message.Text) message.Text = b.replaceMention(message.Text)
@@ -76,6 +77,13 @@ func (b *Bslack) handleSlackClient(messages chan *config.Message) {
continue continue
} }
messages <- rmsg messages <- rmsg
case *slack.FileDeletedEvent:
rmsg, err := b.handleFileDeletedEvent(ev)
if err != nil {
b.Log.Errorf("%#v", err)
continue
}
messages <- rmsg
case *slack.OutgoingErrorEvent: case *slack.OutgoingErrorEvent:
b.Log.Debugf("%#v", ev.Error()) b.Log.Debugf("%#v", ev.Error())
case *slack.ChannelJoinedEvent: case *slack.ChannelJoinedEvent:
@@ -222,6 +230,26 @@ func (b *Bslack) handleMessageEvent(ev *slack.MessageEvent) (*config.Message, er
return rmsg, nil return rmsg, nil
} }
func (b *Bslack) handleFileDeletedEvent(ev *slack.FileDeletedEvent) (*config.Message, error) {
if rawChannel, ok := b.cache.Get(cfileDownloadChannel + ev.FileID); ok {
channel, err := b.channels.getChannelByID(rawChannel.(string))
if err != nil {
return nil, err
}
return &config.Message{
Event: config.EventFileDelete,
Text: config.EventFileDelete,
Channel: channel.Name,
Account: b.Account,
ID: ev.FileID,
Protocol: b.Protocol,
}, nil
}
return nil, fmt.Errorf("channel ID for file ID %s not found", ev.FileID)
}
func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) bool { func (b *Bslack) handleStatusEvent(ev *slack.MessageEvent, rmsg *config.Message) bool {
switch ev.SubType { switch ev.SubType {
case sChannelJoined, sMemberJoined: case sChannelJoined, sMemberJoined:
@@ -281,6 +309,8 @@ func (b *Bslack) handleAttachments(ev *slack.MessageEvent, rmsg *config.Message)
// If we have files attached, download them (in memory) and put a pointer to it in msg.Extra. // If we have files attached, download them (in memory) and put a pointer to it in msg.Extra.
for i := range ev.Files { for i := range ev.Files {
// keep reference in cache on which channel we added this file
b.cache.Add(cfileDownloadChannel+ev.Files[i].ID, ev.Channel)
if err := b.handleDownloadFile(rmsg, &ev.Files[i], false); err != nil { if err := b.handleDownloadFile(rmsg, &ev.Files[i], false); err != nil {
b.Log.Errorf("Could not download incoming file: %#v", err) b.Log.Errorf("Could not download incoming file: %#v", err)
} }
@@ -330,7 +360,7 @@ func (b *Bslack) handleDownloadFile(rmsg *config.Message, file *slack.File, retr
// that the comment is not duplicated. // that the comment is not duplicated.
comment := rmsg.Text comment := rmsg.Text
rmsg.Text = "" rmsg.Text = ""
helper.HandleDownloadData(b.Log, rmsg, file.Name, comment, file.URLPrivateDownload, data, b.General) helper.HandleDownloadData2(b.Log, rmsg, file.Name, file.ID, comment, file.URLPrivateDownload, data, b.General)
return nil return nil
} }

View File

@@ -54,6 +54,7 @@ const (
sLatencyReport = "latency_report" sLatencyReport = "latency_report"
sSystemUser = "system" sSystemUser = "system"
sSlackBotUser = "slackbot" sSlackBotUser = "slackbot"
cfileDownloadChannel = "file_download_channel"
tokenConfig = "Token" tokenConfig = "Token"
incomingWebhookConfig = "WebhookBindAddress" incomingWebhookConfig = "WebhookBindAddress"

View File

@@ -291,6 +291,7 @@ func (b *channels) populateChannels(wait bool) {
queryParams := &slack.GetConversationsParameters{ queryParams := &slack.GetConversationsParameters{
ExcludeArchived: true, ExcludeArchived: true,
Types: []string{"public_channel,private_channel"}, Types: []string{"public_channel,private_channel"},
Limit: 1000,
} }
for { for {
channels, nextCursor, err := b.sc.GetConversations(queryParams) channels, nextCursor, err := b.sc.GetConversations(queryParams)

View File

@@ -1,6 +1,7 @@
package btelegram package btelegram
import ( import (
"fmt"
"html" "html"
"path/filepath" "path/filepath"
"strconv" "strconv"
@@ -9,15 +10,28 @@ import (
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" "github.com/davecgh/go-spew/spew"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message { func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message {
// handle channels // handle channels
if posted != nil { if posted != nil {
if posted.Text == "/chatId" {
chatID := strconv.FormatInt(posted.Chat.ID, 10)
_, err := b.Send(config.Message{
Channel: chatID,
Text: fmt.Sprintf("ID of this chat: %s", chatID),
})
if err != nil {
b.Log.Warnf("Unable to send chatID to %s", chatID)
}
} else {
message = posted message = posted
rmsg.Text = message.Text rmsg.Text = message.Text
} }
}
// edited channel message // edited channel message
if edited != nil && !b.GetBool("EditDisable") { if edited != nil && !b.GetBool("EditDisable") {
@@ -94,7 +108,7 @@ func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Messag
// handleUsername handles the correct setting of the username // handleUsername handles the correct setting of the username
func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) { func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Message) {
if message.From != nil { if message.From != nil {
rmsg.UserID = strconv.Itoa(message.From.ID) rmsg.UserID = strconv.FormatInt(message.From.ID, 10)
if b.GetBool("UseFirstName") { if b.GetBool("UseFirstName") {
rmsg.Username = message.From.FirstName rmsg.Username = message.From.FirstName
} }
@@ -110,6 +124,25 @@ func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Messa
} }
} }
if message.SenderChat != nil { //nolint:nestif
rmsg.UserID = strconv.FormatInt(message.SenderChat.ID, 10)
if b.GetBool("UseFirstName") {
rmsg.Username = message.SenderChat.FirstName
}
if rmsg.Username == "" || rmsg.Username == "Channel_Bot" {
rmsg.Username = message.SenderChat.UserName
if rmsg.Username == "" || rmsg.Username == "Channel_Bot" {
rmsg.Username = message.SenderChat.FirstName
}
}
// only download avatars if we have a place to upload them (configured mediaserver)
if b.General.MediaServerUpload != "" || (b.General.MediaServerDownload != "" && b.General.MediaDownloadPath != "") {
b.handleDownloadAvatar(message.SenderChat.ID, rmsg.Channel)
}
}
// if we really didn't find a username, set it to unknown // if we really didn't find a username, set it to unknown
if rmsg.Username == "" { if rmsg.Username == "" {
rmsg.Username = unknownUser rmsg.Username = unknownUser
@@ -126,6 +159,10 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
continue continue
} }
if b.GetInt("debuglevel") == 1 {
spew.Dump(update.Message)
}
var message *tgbotapi.Message var message *tgbotapi.Message
rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})} rmsg := config.Message{Account: b.Account, Extra: make(map[string][]interface{})}
@@ -167,7 +204,7 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text) rmsg.Text = helper.RemoveEmptyNewLines(rmsg.Text)
// channels don't have (always?) user information. see #410 // channels don't have (always?) user information. see #410
if message.From != nil { if message.From != nil {
rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.Itoa(message.From.ID), b.General) rmsg.Avatar = helper.GetAvatar(b.avatarMap, strconv.FormatInt(message.From.ID, 10), b.General)
} }
b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account) b.Log.Debugf("<= Sending message from %s on %s to gateway", rmsg.Username, b.Account)
@@ -180,18 +217,21 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
// handleDownloadAvatar downloads the avatar of userid from channel // handleDownloadAvatar downloads the avatar of userid from channel
// sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful. // sends a EVENT_AVATAR_DOWNLOAD message to the gateway if successful.
// logs an error message if it fails // logs an error message if it fails
func (b *Btelegram) handleDownloadAvatar(userid int, channel string) { func (b *Btelegram) handleDownloadAvatar(userid int64, channel string) {
rmsg := config.Message{ rmsg := config.Message{
Username: "system", Username: "system",
Text: "avatar", Text: "avatar",
Channel: channel, Channel: channel,
Account: b.Account, Account: b.Account,
UserID: strconv.Itoa(userid), UserID: strconv.FormatInt(userid, 10),
Event: config.EventAvatarDownload, Event: config.EventAvatarDownload,
Extra: make(map[string][]interface{}), Extra: make(map[string][]interface{}),
} }
if _, ok := b.avatarMap[strconv.Itoa(userid)]; !ok { if _, ok := b.avatarMap[strconv.FormatInt(userid, 10)]; ok {
return
}
photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1}) photos, err := b.c.GetUserProfilePhotos(tgbotapi.UserProfilePhotosConfig{UserID: userid, Limit: 1})
if err != nil { if err != nil {
b.Log.Errorf("Userprofile download failed for %#v %s", userid, err) b.Log.Errorf("Userprofile download failed for %#v %s", userid, err)
@@ -200,7 +240,7 @@ func (b *Btelegram) handleDownloadAvatar(userid int, channel string) {
if len(photos.Photos) > 0 { if len(photos.Photos) > 0 {
photo := photos.Photos[0][0] photo := photos.Photos[0][0]
url := b.getFileDirectURL(photo.FileID) url := b.getFileDirectURL(photo.FileID)
name := strconv.Itoa(userid) + ".png" name := strconv.FormatInt(userid, 10) + ".png"
b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize) b.Log.Debugf("trying to download %#v fileid %#v with size %#v", name, photo.FileID, photo.FileSize)
err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General) err := helper.HandleDownloadSize(b.Log, &rmsg, name, int64(photo.FileSize), b.General)
@@ -217,7 +257,6 @@ func (b *Btelegram) handleDownloadAvatar(userid int, channel string) {
b.Remote <- rmsg b.Remote <- rmsg
} }
} }
}
func (b *Btelegram) maybeConvertTgs(name *string, data *[]byte) { func (b *Btelegram) maybeConvertTgs(name *string, data *[]byte) {
format := b.GetString("MediaConvertTgs") format := b.GetString("MediaConvertTgs")
@@ -272,7 +311,7 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa
name = message.Document.FileName name = message.Document.FileName
text = " " + message.Document.FileName + " : " + url text = " " + message.Document.FileName + " : " + url
case message.Photo != nil: case message.Photo != nil:
photos := *message.Photo photos := message.Photo
size = photos[len(photos)-1].FileSize size = photos[len(photos)-1].FileSize
text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true) text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true)
} }
@@ -331,11 +370,15 @@ func (b *Btelegram) handleDelete(msg *config.Message, chatid int64) (string, err
if msg.ID == "" { if msg.ID == "" {
return "", nil return "", nil
} }
msgid, err := strconv.Atoi(msg.ID) msgid, err := strconv.Atoi(msg.ID)
if err != nil { if err != nil {
return "", err return "", err
} }
_, err = b.c.DeleteMessage(tgbotapi.DeleteMessageConfig{ChatID: chatid, MessageID: msgid})
cfg := tgbotapi.NewDeleteMessage(chatid, msgid)
_, err = b.c.Send(cfg)
return "", err return "", err
} }
@@ -383,23 +426,23 @@ func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64) string {
} }
switch filepath.Ext(fi.Name) { switch filepath.Ext(fi.Name) {
case ".jpg", ".jpe", ".png": case ".jpg", ".jpe", ".png":
pc := tgbotapi.NewPhotoUpload(chatid, file) pc := tgbotapi.NewPhoto(chatid, file)
pc.Caption, pc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment) pc.Caption, pc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
c = pc c = pc
case ".mp4", ".m4v": case ".mp4", ".m4v":
vc := tgbotapi.NewVideoUpload(chatid, file) vc := tgbotapi.NewVideo(chatid, file)
vc.Caption, vc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment) vc.Caption, vc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
c = vc c = vc
case ".mp3", ".oga": case ".mp3", ".oga":
ac := tgbotapi.NewAudioUpload(chatid, file) ac := tgbotapi.NewAudio(chatid, file)
ac.Caption, ac.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment) ac.Caption, ac.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
c = ac c = ac
case ".ogg": case ".ogg":
voc := tgbotapi.NewVoiceUpload(chatid, file) voc := tgbotapi.NewVoice(chatid, file)
voc.Caption, voc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment) voc.Caption, voc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
c = voc c = voc
default: default:
dc := tgbotapi.NewDocumentUpload(chatid, file) dc := tgbotapi.NewDocument(chatid, file)
dc.Caption, dc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment) dc.Caption, dc.ParseMode = TGGetParseMode(b, msg.Username, fi.Comment)
c = dc c = dc
} }
@@ -435,8 +478,11 @@ func (b *Btelegram) handleEntities(rmsg *config.Message, message *tgbotapi.Messa
if message.Entities == nil { if message.Entities == nil {
return return
} }
indexMovedBy := 0
// for now only do URL replacements // for now only do URL replacements
for _, e := range *message.Entities { for _, e := range message.Entities {
if e.Type == "text_link" { if e.Type == "text_link" {
url, err := e.ParseURL() url, err := e.ParseURL()
if err != nil { if err != nil {
@@ -451,5 +497,17 @@ func (b *Btelegram) handleEntities(rmsg *config.Message, message *tgbotapi.Messa
link := utf16.Decode(utfEncodedString[e.Offset : e.Offset+e.Length]) link := utf16.Decode(utfEncodedString[e.Offset : e.Offset+e.Length])
rmsg.Text = strings.Replace(rmsg.Text, string(link), url.String(), 1) rmsg.Text = strings.Replace(rmsg.Text, string(link), url.String(), 1)
} }
if e.Type == "code" {
offset := e.Offset + indexMovedBy
rmsg.Text = rmsg.Text[:offset] + "`" + rmsg.Text[offset:offset+e.Length] + "`" + rmsg.Text[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:]
indexMovedBy += 8
}
} }
} }

View File

@@ -9,7 +9,7 @@ import (
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
const ( const (
@@ -49,11 +49,7 @@ func (b *Btelegram) Connect() error {
} }
u := tgbotapi.NewUpdate(0) u := tgbotapi.NewUpdate(0)
u.Timeout = 60 u.Timeout = 60
updates, err := b.c.GetUpdatesChan(u) updates := b.c.GetUpdatesChan(u)
if err != nil {
b.Log.Debugf("%#v", err)
return err
}
b.Log.Info("Connection succeeded") b.Log.Info("Connection succeeded")
go b.handleRecv(updates) go b.handleRecv(updates)
return nil return nil

View File

@@ -34,6 +34,7 @@ type user struct {
type Bvk struct { type Bvk struct {
c *api.VK c *api.VK
lp *longpoll.LongPoll
usernamesMap map[int]user // cache of user names and avatar URLs usernamesMap map[int]user // cache of user names and avatar URLs
*bridge.Config *bridge.Config
} }
@@ -45,21 +46,23 @@ func New(cfg *bridge.Config) bridge.Bridger {
func (b *Bvk) Connect() error { func (b *Bvk) Connect() error {
b.Log.Info("Connecting") b.Log.Info("Connecting")
b.c = api.NewVK(b.GetString("Token")) b.c = api.NewVK(b.GetString("Token"))
lp, err := longpoll.NewLongPoll(b.c, b.GetInt("GroupID"))
var err error
b.lp, err = longpoll.NewLongPollCommunity(b.c)
if err != nil { if err != nil {
b.Log.Debugf("%#v", err) b.Log.Debugf("%#v", err)
return err return err
} }
lp.MessageNew(func(ctx context.Context, obj events.MessageNewObject) { b.lp.MessageNew(func(ctx context.Context, obj events.MessageNewObject) {
b.handleMessage(obj.Message, false) b.handleMessage(obj.Message, false)
}) })
b.Log.Info("Connection succeeded") b.Log.Info("Connection succeeded")
go func() { go func() {
err := lp.Run() err := b.lp.Run()
if err != nil { if err != nil {
b.Log.Fatal("Enable longpoll in group management") b.Log.Fatal("Enable longpoll in group management")
} }
@@ -69,6 +72,8 @@ func (b *Bvk) Connect() error {
} }
func (b *Bvk) Disconnect() error { func (b *Bvk) Disconnect() error {
b.lp.Shutdown()
return nil return nil
} }

View File

@@ -293,8 +293,12 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
if msg.ID != "" { if msg.ID != "" {
b.Log.Debugf("updating message with id %s", msg.ID) b.Log.Debugf("updating message with id %s", msg.ID)
if b.GetString("editsuffix") != "" {
msg.Text += b.GetString("EditSuffix")
} else {
msg.Text += " (edited)" msg.Text += " (edited)"
} }
}
// Handle Upload a file // Handle Upload a file
if msg.Extra["file"] != nil { if msg.Extra["file"] != nil {

View File

@@ -128,7 +128,6 @@ func (b *Bxmpp) Send(msg config.Message) (string, error) {
var msgReplaceID string var msgReplaceID string
msgID := xid.New().String() msgID := xid.New().String()
if msg.ID != "" { if msg.ID != "" {
msgID = msg.ID
msgReplaceID = msg.ID msgReplaceID = msg.ID
} }
b.Log.Debugf("=> Sending message %#v", msg) b.Log.Debugf("=> Sending message %#v", msg)
@@ -284,8 +283,14 @@ func (b *Bxmpp) handleXMPP() error {
for { for {
m, err := b.xc.Recv() m, err := b.xc.Recv()
if err != nil { if err != nil {
// An error together with AvatarData is non-fatal
switch m.(type) {
case xmpp.AvatarData:
continue
default:
return err return err
} }
}
switch v := m.(type) { switch v := m.(type) {
case xmpp.Chat: case xmpp.Chat:

View File

@@ -2,6 +2,7 @@ package bzulip
import ( import (
"encoding/json" "encoding/json"
"fmt"
"io/ioutil" "io/ioutil"
"strconv" "strconv"
"strings" "strings"
@@ -11,6 +12,7 @@ import (
"github.com/42wim/matterbridge/bridge" "github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/version"
gzb "github.com/matterbridge/gozulipbot" gzb "github.com/matterbridge/gozulipbot"
) )
@@ -27,7 +29,7 @@ func New(cfg *bridge.Config) bridge.Bridger {
} }
func (b *Bzulip) Connect() error { func (b *Bzulip) Connect() error {
bot := gzb.Bot{APIKey: b.GetString("token"), APIURL: b.GetString("server") + "/api/v1/", Email: b.GetString("login")} bot := gzb.Bot{APIKey: b.GetString("token"), APIURL: b.GetString("server") + "/api/v1/", Email: b.GetString("login"), UserAgent: fmt.Sprintf("matterbridge/%s", version.Release)}
bot.Init() bot.Init()
q, err := bot.RegisterAll() q, err := bot.RegisterAll()
b.q = q b.q = q
@@ -125,6 +127,7 @@ func (b *Bzulip) handleQueue() error {
b.Log.Debug("heartbeat received.") b.Log.Debug("heartbeat received.")
default: default:
b.Log.Debugf("receiving error: %#v", err) b.Log.Debugf("receiving error: %#v", err)
time.Sleep(time.Second * 10)
} }
if err != nil { if err != nil {
continue continue

View File

@@ -1,3 +1,57 @@
# 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)
# v1.23.2
If you're running whatsapp you should update.
## Bugfix
- whatsapp: Update go-whatsapp version (#1630)
# v1.23.1
If you're running mattermost 6 you should update.
## Bugfix
- mattermost: Do not check cache on deleted messages (mattermost). Fixes #1555 (#1624)
- mattermost: Fix crash on users updating info. Update matterclient dep. Fixes #1617
- matrix: Keep the logger on a disabled bridge. Fixes #1616 (#1621)
- msteams: Fix panic in msteams. Fixes #1588 (#1622)
- xmpp: Do not fail on no avatar data (xmpp) #1529 (#1627)
- xmpp: Use a new msgID when replacing messages (xmpp). Fixes #1584 (#1623)
- zulip: Add better error handling on Zulip (#1589)
# v1.23.0 # v1.23.0
## New features ## New features
@@ -24,8 +78,6 @@
This release couldn't exist without the following contributors: This release couldn't exist without the following contributors:
@powerjungle, @gary-kim, @KingPin, @Benau, @keenan-v1, @tytan652, @KidA001,@minecraftchest1, @irydacea @powerjungle, @gary-kim, @KingPin, @Benau, @keenan-v1, @tytan652, @KidA001,@minecraftchest1, @irydacea
##
# v1.22.3 # v1.22.3
## Bugfixes ## Bugfixes

View File

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

View File

@@ -66,7 +66,7 @@ func New(rootLogger *logrus.Logger, cfg *config.Gateway, r *Router) *Gateway {
func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string { func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string {
ID := protocol + " " + mID ID := protocol + " " + mID
if gw.Messages.Contains(ID) { if gw.Messages.Contains(ID) {
return mID return ID
} }
// If not keyed, iterate through cache for downstream, and infer upstream. // If not keyed, iterate through cache for downstream, and infer upstream.
@@ -75,7 +75,7 @@ func (gw *Gateway) FindCanonicalMsgID(protocol string, mID string) string {
ids := v.([]*BrMsgID) ids := v.([]*BrMsgID)
for _, downstreamMsgObj := range ids { for _, downstreamMsgObj := range ids {
if ID == downstreamMsgObj.ID { if ID == downstreamMsgObj.ID {
return strings.Replace(mid.(string), protocol+" ", "", 1) return mid.(string)
} }
} }
} }
@@ -447,16 +447,19 @@ func (gw *Gateway) SendMessage(
msg.Avatar = gw.modifyAvatar(rmsg, dest) msg.Avatar = gw.modifyAvatar(rmsg, dest)
msg.Username = gw.modifyUsername(rmsg, dest) msg.Username = gw.modifyUsername(rmsg, dest)
// exclude file delete event as the msg ID here is the native file ID that needs to be deleted
if msg.Event != config.EventFileDelete {
msg.ID = gw.getDestMsgID(rmsg.Protocol+" "+rmsg.ID, dest, channel) msg.ID = gw.getDestMsgID(rmsg.Protocol+" "+rmsg.ID, dest, channel)
}
// for api we need originchannel as channel // for api we need originchannel as channel
if dest.Protocol == apiProtocol { if dest.Protocol == apiProtocol {
msg.Channel = rmsg.Channel msg.Channel = rmsg.Channel
} }
msg.ParentID = gw.getDestMsgID(rmsg.Protocol+" "+canonicalParentMsgID, dest, channel) msg.ParentID = gw.getDestMsgID(canonicalParentMsgID, dest, channel)
if msg.ParentID == "" { if msg.ParentID == "" {
msg.ParentID = canonicalParentMsgID msg.ParentID = strings.Replace(canonicalParentMsgID, dest.Protocol+" ", "", 1)
} }
// if the parentID is still empty and we have a parentID set in the original message // if the parentID is still empty and we have a parentID set in the original message

View File

@@ -110,7 +110,9 @@ func (r *Router) disableBridge(br *bridge.Bridge, err error) bool {
if r.BridgeValues().General.IgnoreFailureOnStart { if r.BridgeValues().General.IgnoreFailureOnStart {
r.logger.Error(err) r.logger.Error(err)
// setting this bridge empty // setting this bridge empty
*br = bridge.Bridge{} *br = bridge.Bridge{
Log: br.Log,
}
return true return true
} }
return false return false

75
go.mod
View File

@@ -5,31 +5,32 @@ require (
github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f github.com/Baozisoftware/qrcode-terminal-go v0.0.0-20170407111555-c0650d8dff0f
github.com/Benau/tgsconverter v0.0.0-20210809170556-99f4a4f6337f github.com/Benau/tgsconverter v0.0.0-20210809170556-99f4a4f6337f
github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560 github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560
github.com/Rhymen/go-whatsapp v0.1.2-0.20210615184944-2b8a3e9b8aa2 github.com/Rhymen/go-whatsapp v0.1.2-0.20211102134409-31a2e740845c
github.com/SevereCloud/vksdk/v2 v2.10.0 github.com/SevereCloud/vksdk/v2 v2.13.1
github.com/d5/tengo/v2 v2.8.0 github.com/d5/tengo/v2 v2.10.0
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.1
github.com/fsnotify/fsnotify v1.5.1 github.com/fsnotify/fsnotify v1.5.1
github.com/go-telegram-bot-api/telegram-bot-api v1.0.1-0.20200524105306-7434b0456e81 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/gomarkdown/markdown v0.0.0-20210918233619-6c1113f12c4a github.com/gomarkdown/markdown v0.0.0-20211207152620-5d6539fd8bfc
github.com/google/gops v0.3.21 github.com/google/gops v0.3.22
github.com/gorilla/schema v1.2.0 github.com/gorilla/schema v1.2.0
github.com/gorilla/websocket v1.4.2 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/hashicorp/golang-lru v0.5.4
github.com/jpillora/backoff v1.0.0 github.com/jpillora/backoff v1.0.0
github.com/keybase/go-keybase-chat-bot v0.0.0-20211004153716-fd2ee4d6be11 github.com/keybase/go-keybase-chat-bot v0.0.0-20211201215354-ee4b23828b55
github.com/kyokomi/emoji/v2 v2.2.8 github.com/kyokomi/emoji/v2 v2.2.8
github.com/labstack/echo/v4 v4.6.1 github.com/labstack/echo/v4 v4.6.3
github.com/lrstanley/girc v0.0.0-20210611213246-771323f1624b github.com/lrstanley/girc v0.0.0-20211023233735-147f0ff77566
github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16 github.com/matrix-org/gomatrix v0.0.0-20210324163249-be2af5ef2e16
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20211016222428-79310a412696 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/discordgo v0.21.2-0.20210201201054-fb39a175b4f7
github.com/matterbridge/go-xmpp v0.0.0-20210731150933-5702291c239f github.com/matterbridge/go-xmpp v0.0.0-20211030125215-791a06c5f1be
github.com/matterbridge/gozulipbot v0.0.0-20200820220548-be5824faa913 github.com/matterbridge/gozulipbot v0.0.0-20211023205727-a19d6c1f3b75
github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba
github.com/matterbridge/matterclient v0.0.0-20211016195328-346acac403d8 github.com/matterbridge/matterclient v0.0.0-20211107234719-faca3cd42315
github.com/mattermost/mattermost-server/v5 v5.39.0 github.com/mattermost/mattermost-server/v5 v5.39.3
github.com/mattermost/mattermost-server/v6 v6.0.0 github.com/mattermost/mattermost-server/v6 v6.3.0
github.com/mattn/godown v0.0.1 github.com/mattn/godown v0.0.1
github.com/missdeer/golib v1.0.4 github.com/missdeer/golib v1.0.4
github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9 github.com/nelsonken/gomf v0.0.0-20180504123937-a9dd2f9deae9
@@ -39,15 +40,15 @@ require (
github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca github.com/saintfish/chardet v0.0.0-20120816061221-3af4cd4741ca
github.com/shazow/ssh-chat v1.10.1 github.com/shazow/ssh-chat v1.10.1
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/slack-go/slack v0.9.5 github.com/slack-go/slack v0.10.0
github.com/spf13/viper v1.9.0 github.com/spf13/viper v1.10.1
github.com/stretchr/testify v1.7.0 github.com/stretchr/testify v1.7.0
github.com/vincent-petithory/dataurl v0.0.0-20191104211930-d1553a71de50 github.com/vincent-petithory/dataurl v1.0.0
github.com/writeas/go-strip-markdown v2.0.1+incompatible github.com/writeas/go-strip-markdown v2.0.1+incompatible
github.com/yaegashi/msgraph.go v0.1.4 github.com/yaegashi/msgraph.go v0.1.4
github.com/zfjagann/golang-ring v0.0.0-20210116075443-7c86fdb43134 github.com/zfjagann/golang-ring v0.0.0-20210116075443-7c86fdb43134
golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d golang.org/x/image v0.0.0-20211028202545-6944b10bf410
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1 golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8
gomod.garykim.dev/nc-talk v0.3.0 gomod.garykim.dev/nc-talk v0.3.0
gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376 gopkg.in/olahol/melody.v1 v1.0.0-20170518105555-d52139073376
layeh.com/gumble v0.0.0-20200818122324-146f9205029b layeh.com/gumble v0.0.0-20200818122324-146f9205029b
@@ -65,31 +66,32 @@ require (
github.com/go-asn1-ber/asn1-ber v1.5.3 // indirect github.com/go-asn1-ber/asn1-ber v1.5.3 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.2 // indirect
github.com/google/uuid v1.2.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/gopackage/ddp v0.0.3 // indirect github.com/gopackage/ddp v0.0.3 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/json-iterator/go v1.1.11 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/kettek/apng v0.0.0-20191108220231-414630eed80f // indirect github.com/kettek/apng v0.0.0-20191108220231-414630eed80f // indirect
github.com/klauspost/cpuid/v2 v2.0.6 // indirect github.com/klauspost/compress v1.14.2 // indirect
github.com/labstack/gommon v0.3.0 // 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/magiconair/properties v1.8.5 // indirect
github.com/mattermost/go-i18n v1.11.0 // 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/ldap v0.0.0-20201202150706-ee0e6284187d // indirect
github.com/mattermost/logr v1.0.13 // indirect github.com/mattermost/logr v1.0.13 // indirect
github.com/mattermost/logr/v2 v2.0.10 // indirect github.com/mattermost/logr/v2 v2.0.15 // indirect
github.com/mattn/go-colorable v0.1.8 // indirect github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.0.11 // indirect github.com/minio/minio-go/v7 v7.0.16 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.4.2 // indirect github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monaco-io/request v1.0.5 // indirect github.com/monaco-io/request v1.0.5 // indirect
github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d // indirect github.com/mreiferson/go-httpclient v0.0.0-20201222173833-5e475fde3a4d // indirect
github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect github.com/mrexodia/wray v0.0.0-20160318003008-78a2c1f284ff // indirect
@@ -109,28 +111,31 @@ require (
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.2.0 // indirect github.com/subosito/gotenv v1.2.0 // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/tinylib/msgp v1.1.6 // indirect github.com/tinylib/msgp v1.1.6 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.1 // indirect github.com/valyala/fasttemplate v1.2.1 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wiggin77/cfg v1.0.2 // indirect github.com/wiggin77/cfg v1.0.2 // indirect
github.com/wiggin77/merror v1.0.3 // indirect github.com/wiggin77/merror v1.0.3 // indirect
github.com/wiggin77/srslog v1.0.1 // indirect github.com/wiggin77/srslog v1.0.1 // indirect
go.uber.org/atomic v1.8.0 // indirect go.uber.org/atomic v1.9.0 // indirect
go.uber.org/multierr v1.7.0 // indirect go.uber.org/multierr v1.7.0 // indirect
go.uber.org/zap v1.17.0 // indirect go.uber.org/zap v1.17.0 // indirect
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 // indirect golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871 // indirect
golang.org/x/net v0.0.0-20210913180222-943fd674d43e // indirect golang.org/x/net v0.0.0-20211123203042-d83791d6bcd9 // indirect
golang.org/x/sys v0.0.0-20210910150752-751e447fb3d0 // indirect golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect golang.org/x/time v0.0.0-20201208040808-7e3f01d25324 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.27.1 // indirect google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/ini.v1 v1.63.2 // indirect gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // 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.0-20210107192922-496545a6307b // indirect
) )
replace github.com/matrix-org/gomatrix => github.com/matterbridge/gomatrix v0.0.0-20220205235239-607eb9ee6419
go 1.17 go 1.17

942
go.sum

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -24,6 +24,13 @@ Password=""
#OPTIONAL (default false) #OPTIONAL (default false)
UseTLS=false UseTLS=false
#Use client certificate - see CertFP https://libera.chat/guides/certfp.html
#Specify filename which contains private key and cert
#OPTIONAL (default "")
#
#TLSClientCertificate="cert.pem"
TLSClientCertificate=""
#Enable SASL (PLAIN) authentication. (libera requires this from eg AWS hosts) #Enable SASL (PLAIN) authentication. (libera requires this from eg AWS hosts)
#It uses NickServNick and NickServPassword as login and password #It uses NickServNick and NickServPassword as login and password
#OPTIONAL (default false) #OPTIONAL (default false)
@@ -34,6 +41,11 @@ UseSASL=false
#OPTIONAL (default false) #OPTIONAL (default false)
SkipTLSVerify=true SkipTLSVerify=true
#Local address to use for server connection
#Note that Server and Bind must resolve to addresses of the same family.
#OPTIONAL (default "")
Bind=""
#If you know your charset, you can specify it manually. #If you know your charset, you can specify it manually.
#Otherwise it tries to detect this automatically. Select one below #Otherwise it tries to detect this automatically. Select one below
# "iso-8859-2:1987", "iso-8859-9:1989", "866", "latin9", "iso-8859-10:1992", "iso-ir-109", "hebrew", # "iso-8859-2:1987", "iso-8859-9:1989", "866", "latin9", "iso-8859-10:1992", "iso-ir-109", "hebrew",
@@ -396,6 +408,10 @@ SkipTLSVerify=true
## RELOADABLE SETTINGS ## RELOADABLE SETTINGS
## Settings below can be reloaded by editing the file ## Settings below can be reloaded by editing the file
# UseUserName shows the username instead of the server nickname
# OPTIONAL (default false)
UseUserName=false
#how to format the list of IRC nicks when displayed in mattermost. #how to format the list of IRC nicks when displayed in mattermost.
#Possible options are "table" and "plain" #Possible options are "table" and "plain"
#OPTIONAL (default plain) #OPTIONAL (default plain)
@@ -1534,10 +1550,6 @@ MessageClipped="<clipped message>"
#See https://vk.com/dev/bots_docs #See https://vk.com/dev/bots_docs
Token="Yourtokenhere" Token="Yourtokenhere"
#Group ID
#For example in URL https://vk.com/public168963511 group ID is 168963511
GroupID=123456789
################################################################### ###################################################################
# WhatsApp # WhatsApp
################################################################### ###################################################################
@@ -1655,6 +1667,18 @@ StripNick=false
#OPTIONAL (default false) #OPTIONAL (default false)
ShowTopicChange=false ShowTopicChange=false
###################################################################
# Harmony
###################################################################
[harmony.chat_harmonyapp_io]
Homeserver = "https://chat.harmonyapp.io:2289"
Token = "your token goes here"
UserID = "user id of the bot account"
Community = "community id that channels will be located in"
UseUserName = true
RemoteNickFormat = "{NICK}"
################################################################### ###################################################################
#API #API
################################################################### ###################################################################
@@ -1700,6 +1724,7 @@ RemoteNickFormat="{NICK}"
#RemoteNickFormat defines how remote users appear on this bridge #RemoteNickFormat defines how remote users appear on this bridge
#The string "{NICK}" (case sensitive) will be replaced by the actual nick. #The string "{NICK}" (case sensitive) will be replaced by the actual nick.
#The string "{NOPINGNICK}" (case sensitive) will be replaced by the actual nick / username, but with a ZWSP inside the nick, so the irc user with the same nick won't get pinged.
#The string "{USERID}" (case sensitive) will be replaced by the user ID. #The string "{USERID}" (case sensitive) will be replaced by the user ID.
#The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge #The string "{BRIDGE}" (case sensitive) will be replaced by the sending bridge
#The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge #The string "{LABEL}" (case sensitive) will be replaced by label= field of the sending bridge
@@ -1872,7 +1897,8 @@ enable=true
# ------------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------------------------
# irc | channel | #general | The # symbol is required and should be lowercase! # irc | channel | #general | The # symbol is required and should be lowercase!
# ------------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------------------------
# mattermost | channel | general | This is the channel name as seen in the URL, not the display name # | channel | general | This is the channel name as seen in the URL, not the display name
# mattermost | channel id | ID:oc4wifyuojgw5f3nsuweesmz8w | This is the channel ID (only use if you know what you're doing)
# ------------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------------------------
# matrix | #channel:server | #yourchannel:matrix.org | Encrypted rooms are not supported in matrix # matrix | #channel:server | #yourchannel:matrix.org | Encrypted rooms are not supported in matrix
# ------------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------------------------
@@ -1898,7 +1924,7 @@ enable=true
# ------------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------------------------
# xmpp | channel | general | The room name # xmpp | channel | general | The room name
# ------------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------------------------
# zulip | stream/topic:topic | general/off-topic:food | Do not use the # when specifying a topic # zulip | stream/topic:topic | general/topic:food | Do not use the # when specifying a topic
# ------------------------------------------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------------------------------------------
# #
@@ -1947,6 +1973,10 @@ enable=true
account="zulip.streamchat" account="zulip.streamchat"
channel="general/topic:mytopic" channel="general/topic:mytopic"
[[gateway.inout]]
account="harmony.chat_harmonyapp_io"
channel="channel id goes here"
#API example #API example
#[[gateway.inout]] #[[gateway.inout]]
#account="api.local" #account="api.local"

View File

@@ -9,7 +9,7 @@ import (
func (m *MMClient) parseActionPost(rmsg *Message) { func (m *MMClient) parseActionPost(rmsg *Message) {
// add post to cache, if it already exists don't relay this again. // add post to cache, if it already exists don't relay this again.
// this should fix reposts // this should fix reposts
if ok, _ := m.lruCache.ContainsOrAdd(digestString(rmsg.Raw.Data["post"].(string)), true); ok { 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)) m.logger.Debugf("message %#v in cache, not processing again", rmsg.Raw.Data["post"].(string))
rmsg.Text = "" rmsg.Text = ""
return return

View File

@@ -5,7 +5,7 @@ RUN apk add \
go \ go \
git \ git \
&& cd /go/src/matterbridge \ && cd /go/src/matterbridge \
&& go build -mod vendor -ldflags "-X main.githash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge && CGO_ENABLED=0 go build -mod vendor -ldflags "-X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o /bin/matterbridge
FROM alpine FROM alpine
RUN apk --no-cache add \ RUN apk --no-cache add \

View File

@@ -70,7 +70,7 @@ func (myHandler) HandleContactMessage(message whatsapp.ContactMessage) {
fmt.Println(message) fmt.Println(message)
} }
func (myHandler) HandleBatteryMessage(msg whatsapp.BatteryMessage) { func (myHandler) HandleBatteryMessage(message whatsapp.BatteryMessage) {
fmt.Println(message) fmt.Println(message)
} }

View File

@@ -18,7 +18,7 @@ import (
) )
//represents the WhatsAppWeb client version //represents the WhatsAppWeb client version
var waVersion = []int{2, 2121, 6} var waVersion = []int{2, 2142, 12}
/* /*
Session contains session individual information. To be able to resume the connection without scanning the qr code Session contains session individual information. To be able to resume the connection without scanning the qr code
@@ -526,5 +526,7 @@ func (wac *Conn) Logout() error {
return fmt.Errorf("error writing logout: %v\n", err) return fmt.Errorf("error writing logout: %v\n", err)
} }
wac.loggedIn = false
return nil return nil
} }

View File

@@ -48,10 +48,18 @@ linters:
- nilerr - nilerr
- revive - revive
- wastedassign - wastedassign
- bidichk
- contextcheck
- ireturn
- nilnil
- tenv
- nestif
- grouper
- decorder
- containedctx
# - wrapcheck # TODO: v3 Fix # - wrapcheck # TODO: v3 Fix
# - testpackage # TODO: Fix testpackage # - testpackage # TODO: Fix testpackage
# - nestif # TODO: Fix nestif
# - noctx # TODO: Fix noctx # - noctx # TODO: Fix noctx
# don't enable: # don't enable:
@@ -75,6 +83,10 @@ linters:
# - cyclop # - cyclop
# - promlinter # - promlinter
# - tagliatelle # - tagliatelle
# - errname
# - varnamelen
# - errchkjson
# - maintidx
# depricated # depricated
# - maligned # - maligned

View File

@@ -1,2 +1,3 @@
--- ---
no-hard-tabs: false no-hard-tabs: false
no-duplicate-heading: false

View File

@@ -1,20 +0,0 @@
---
language: go
cache:
directories:
- $HOME/.cache/go-build
- $HOME/gopath/pkg/mod
go:
- 1.x
before_script:
- git fetch --depth=1 origin +refs/tags/*:refs/tags/*
- git describe --tags $(git rev-list --tags --max-count=1) --always
script:
- go test -v -race -coverprofile=coverage.txt -covermode=atomic -p=1 ./...
after_success:
- bash <(curl -s https://codecov.io/bash)

View File

@@ -6,7 +6,7 @@
Требования: Требования:
- [Go 1.13+](https://golang.org/doc/install) - [Go 1.16+](https://golang.org/doc/install)
- [golangci-lint](https://github.com/golangci/golangci-lint) - [golangci-lint](https://github.com/golangci/golangci-lint)
- [global .gitignore](https://help.github.com/en/articles/ignoring-files#create-a-global-gitignore) - [global .gitignore](https://help.github.com/en/articles/ignoring-files#create-a-global-gitignore)
@@ -39,6 +39,7 @@ golangci-lint run
# CLIENT_SECRET="" # CLIENT_SECRET=""
# USER_TOKEN="" # USER_TOKEN=""
# WIDGET_TOKEN="" # WIDGET_TOKEN=""
# MARUSIA_TOKEN=""
# CLIENT_ID="123456" # CLIENT_ID="123456"
# GROUP_ID="123456" # GROUP_ID="123456"
# ACCOUNT_ID="123456" # ACCOUNT_ID="123456"
@@ -56,6 +57,7 @@ go test ./...
"go.testEnvVars": { "go.testEnvVars": {
"SERVICE_TOKEN": "", "SERVICE_TOKEN": "",
"WIDGET_TOKEN": "", "WIDGET_TOKEN": "",
"MARUSIA_TOKEN": "",
"GROUP_TOKEN": "", "GROUP_TOKEN": "",
"CLIENT_SECRET": "", "CLIENT_SECRET": "",
"USER_TOKEN": "", "USER_TOKEN": "",

View File

@@ -1,6 +1,5 @@
# VK SDK for Golang # VK SDK for Golang
[![Build Status](https://travis-ci.com/SevereCloud/vksdk.svg?branch=master)](https://travis-ci.com/SevereCloud/vksdk)
[![PkgGoDev](https://pkg.go.dev/badge/github.com/SevereCloud/vksdk/v2/v2)](https://pkg.go.dev/github.com/SevereCloud/vksdk/v2?tab=subdirectories) [![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://vk.com/dev/)
[![codecov](https://codecov.io/gh/SevereCloud/vksdk/branch/master/graph/badge.svg)](https://codecov.io/gh/SevereCloud/vksdk) [![codecov](https://codecov.io/gh/SevereCloud/vksdk/branch/master/graph/badge.svg)](https://codecov.io/gh/SevereCloud/vksdk)
@@ -17,11 +16,13 @@
Version API 5.131. Version API 5.131.
- [API](https://pkg.go.dev/github.com/SevereCloud/vksdk/v2/api) - [API](https://pkg.go.dev/github.com/SevereCloud/vksdk/v2/api)
- 400+ methods - 500+ methods
- Ability to change the request handler
- Ability to modify HTTP client - Ability to modify HTTP client
- Request Limiter - Request Limiter
- Support [zstd](https://pkg.go.dev/github.com/SevereCloud/vksdk/v2/api#VK.EnableZstd)
and [MessagePack](https://pkg.go.dev/github.com/SevereCloud/vksdk/v2/api#VK.EnableMessagePack)
- Token pool - Token pool
- [OAuth](https://pkg.go.dev/github.com/SevereCloud/vksdk/v2/api/oauth)
- [Callback API](https://pkg.go.dev/github.com/SevereCloud/vksdk/v2/callback) - [Callback API](https://pkg.go.dev/github.com/SevereCloud/vksdk/v2/callback)
- Tracking tool for users activity in your VK communities - Tracking tool for users activity in your VK communities
- Supports all events - Supports all events
@@ -61,6 +62,7 @@ go get github.com/SevereCloud/vksdk/v2@latest
## Use by ## Use by
- A simple chat bridge: <https://github.com/42wim/matterbridge>
- [Joe](https://github.com/go-joe/joe) adapter: <https://github.com/tdakkota/joe-vk-adapter> - [Joe](https://github.com/go-joe/joe) adapter: <https://github.com/tdakkota/joe-vk-adapter>
- [Logrus](https://github.com/sirupsen/logrus) hook: <https://github.com/SevereCloud/vkrus> - [Logrus](https://github.com/sirupsen/logrus) hook: <https://github.com/SevereCloud/vkrus>

View File

@@ -3,7 +3,7 @@
[![PkgGoDev](https://pkg.go.dev/badge/github.com/SevereCloud/vksdk/v2/api)](https://pkg.go.dev/github.com/SevereCloud/vksdk/v2/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://vk.com/dev/first_guide)
Данная библиотека поддерживает версию API **5.122**. Данная библиотека поддерживает версию API **5.131**.
## Запросы ## Запросы
@@ -80,6 +80,54 @@ if errors.As(err, &e) {
Для Execute существует отдельная ошибка `ExecuteErrors` Для Execute существует отдельная ошибка `ExecuteErrors`
### Поддержка MessagePack и zstd
> Результат перехода с gzip (JSON) на zstd (msgpack):
>
> - в 7 раз быстрее сжатие (1 мкс);
> - на 10% меньше размер данных (8 Кбайт вместо 9 Кбайт);
> - продуктовый эффект не статзначимый :(
>
> [Как мы отказались от JPEG, JSON, TCP и ускорили ВКонтакте в два раза](https://habr.com/ru/company/vk/blog/594633/)
VK API способно возвращать ответ в виде [MessagePack](https://msgpack.org/).
Это эффективный формат двоичной сериализации, похожий на JSON, только быстрее
и меньше по размеру.
ВНИМАНИЕ, C MessagePack НЕКОТОРЫЕ МЕТОДЫ МОГУТ ВОЗВРАЩАТЬ
СЛОМАННУЮ КОДИРОВКУ.
Для сжатия, вместо классического gzip, можно использовать
[zstd](https://github.com/facebook/zstd). Сейчас vksdk поддерживает zstd без
словаря. Если кто знает как получать словарь,
[отпишитесь сюда](https://github.com/SevereCloud/vksdk/issues/180).
```go
vk := api.NewVK(os.Getenv("USER_TOKEN"))
method := "store.getStickersKeywords"
params := api.Params{
"aliases": true,
"all_products": true,
"need_stickers": true,
}
r, err := vk.Request(method, params) // Content-Length: 44758
if err != nil {
log.Fatal(err)
}
log.Println("json:", len(r)) // json: 814231
vk.EnableMessagePack() // Включаем поддержку MessagePack
vk.EnableZstd() // Включаем поддержку zstd
r, err = vk.Request(method, params) // Content-Length: 35755
if err != nil {
log.Fatal(err)
}
log.Println("msgpack:", len(r)) // msgpack: 650775
```
### Запрос любого метода ### Запрос любого метода
Пример запроса [users.get](https://vk.com/dev/users.get) Пример запроса [users.get](https://vk.com/dev/users.get)

View File

@@ -1,9 +1,11 @@
package api // import "github.com/SevereCloud/vksdk/v2/api" package api // import "github.com/SevereCloud/vksdk/v2/api"
import ( import (
"bytes"
"encoding/json" "encoding/json"
"github.com/SevereCloud/vksdk/v2/object" "github.com/SevereCloud/vksdk/v2/object"
"github.com/vmihailenco/msgpack/v5"
) )
// AdsAddOfficeUsersItem struct. // AdsAddOfficeUsersItem struct.
@@ -21,6 +23,23 @@ func (r *AdsAddOfficeUsersItem) UnmarshalJSON(data []byte) (err error) {
return return
} }
// DecodeMsgpack func.
func (r *AdsAddOfficeUsersItem) DecodeMsgpack(dec *msgpack.Decoder) error {
data, err := dec.DecodeRaw()
if err != nil {
return err
}
if msgpack.Unmarshal(data, &r.OK) != nil {
d := msgpack.NewDecoder(bytes.NewReader(data))
d.SetCustomStructTag("json")
return d.Decode(&r.Error)
}
return nil
}
// AdsAddOfficeUsersResponse struct. // AdsAddOfficeUsersResponse struct.
type AdsAddOfficeUsersResponse []AdsAddOfficeUsersItem type AdsAddOfficeUsersResponse []AdsAddOfficeUsersItem
@@ -349,7 +368,7 @@ func (vk *VK) AdsGetAdsLayout(params Params) (response AdsGetAdsLayoutResponse,
// AdsGetMusiciansResponse struct. // AdsGetMusiciansResponse struct.
type AdsGetMusiciansResponse struct { type AdsGetMusiciansResponse struct {
Items []object.BaseObjectWithName Items []object.AdsMusician
} }
// AdsGetMusicians returns a list of musicians. // AdsGetMusicians returns a list of musicians.

View File

@@ -7,9 +7,11 @@ package api // import "github.com/SevereCloud/vksdk/v2/api"
import ( import (
"bytes" "bytes"
"compress/gzip"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"mime" "mime"
"net/http" "net/http"
"net/url" "net/url"
@@ -21,6 +23,8 @@ import (
"github.com/SevereCloud/vksdk/v2" "github.com/SevereCloud/vksdk/v2"
"github.com/SevereCloud/vksdk/v2/internal" "github.com/SevereCloud/vksdk/v2/internal"
"github.com/SevereCloud/vksdk/v2/object" "github.com/SevereCloud/vksdk/v2/object"
"github.com/klauspost/compress/zstd"
"github.com/vmihailenco/msgpack/v5"
) )
// Api constants. // Api constants.
@@ -91,6 +95,9 @@ type VK struct {
UserAgent string UserAgent string
Handler func(method string, params ...Params) (Response, error) Handler func(method string, params ...Params) (Response, error)
msgpack bool
zstd bool
mux sync.Mutex mux sync.Mutex
lastTime time.Time lastTime time.Time
rps int rps int
@@ -98,7 +105,7 @@ type VK struct {
// Response struct. // Response struct.
type Response struct { type Response struct {
Response json.RawMessage `json:"response"` Response object.RawMessage `json:"response"`
Error Error `json:"error"` Error Error `json:"error"`
ExecuteErrors ExecuteErrors `json:"execute_errors"` ExecuteErrors ExecuteErrors `json:"execute_errors"`
} }
@@ -121,7 +128,7 @@ func NewVK(tokens ...string) *VK {
vk.accessTokens = tokens vk.accessTokens = tokens
vk.Version = Version vk.Version = Version
vk.Handler = vk.defaultHandler vk.Handler = vk.DefaultHandler
vk.MethodURL = MethodURL vk.MethodURL = MethodURL
vk.Client = http.DefaultClient vk.Client = http.DefaultClient
@@ -207,8 +214,8 @@ func buildQuery(sliceParams ...Params) (context.Context, url.Values) {
return ctx, query return ctx, query
} }
// defaultHandler provides access to VK API methods. // DefaultHandler provides access to VK API methods.
func (vk *VK) defaultHandler(method string, sliceParams ...Params) (Response, error) { func (vk *VK) DefaultHandler(method string, sliceParams ...Params) (Response, error) {
u := vk.MethodURL + method u := vk.MethodURL + method
ctx, query := buildQuery(sliceParams...) ctx, query := buildQuery(sliceParams...)
attempt := 0 attempt := 0
@@ -243,25 +250,59 @@ func (vk *VK) defaultHandler(method string, sliceParams ...Params) (Response, er
return response, err return response, err
} }
acceptEncoding := "gzip"
if vk.zstd {
acceptEncoding = "zstd"
}
req.Header.Set("User-Agent", vk.UserAgent) req.Header.Set("User-Agent", vk.UserAgent)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept-Encoding", acceptEncoding)
var reader io.Reader
resp, err := vk.Client.Do(req) resp, err := vk.Client.Do(req)
if err != nil { if err != nil {
return response, err return response, err
} }
mediatype, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type")) switch resp.Header.Get("Content-Encoding") {
if mediatype != "application/json" { case "zstd":
_ = resp.Body.Close() zstdReader, _ := zstd.NewReader(resp.Body)
return response, &InvalidContentType{mediatype} defer zstdReader.Close()
reader = zstdReader
case "gzip":
gzipReader, _ := gzip.NewReader(resp.Body)
defer gzipReader.Close()
reader = gzipReader
default:
reader = resp.Body
} }
err = json.NewDecoder(resp.Body).Decode(&response) mediatype, _, _ := mime.ParseMediaType(resp.Header.Get("Content-Type"))
switch mediatype {
case "application/json":
err = json.NewDecoder(reader).Decode(&response)
if err != nil { if err != nil {
_ = resp.Body.Close() _ = resp.Body.Close()
return response, err return response, err
} }
case "application/x-msgpack":
dec := msgpack.NewDecoder(reader)
dec.SetCustomStructTag("json")
err = dec.Decode(&response)
if err != nil {
_ = resp.Body.Close()
return response, err
}
default:
_ = resp.Body.Close()
return response, &InvalidContentType{mediatype}
}
_ = resp.Body.Close() _ = resp.Body.Close()
@@ -291,6 +332,10 @@ func (vk *VK) Request(method string, sliceParams ...Params) ([]byte, error) {
sliceParams = append(sliceParams, reqParams) sliceParams = append(sliceParams, reqParams)
if vk.msgpack {
method += ".msgpack"
}
resp, err := vk.Handler(method, sliceParams...) resp, err := vk.Handler(method, sliceParams...)
return resp.Response, err return resp.Response, err
@@ -303,7 +348,32 @@ func (vk *VK) RequestUnmarshal(method string, obj interface{}, sliceParams ...Pa
return err return err
} }
return json.Unmarshal(rawResponse, &obj) if vk.msgpack {
dec := msgpack.NewDecoder(bytes.NewReader(rawResponse))
dec.SetCustomStructTag("json")
err = dec.Decode(&obj)
} else {
err = json.Unmarshal(rawResponse, &obj)
}
return err
}
// EnableMessagePack enable using MessagePack instead of JSON.
//
// THIS IS EXPERIMENTAL FUNCTION! Broken encoding returned in some methods.
//
// See https://msgpack.org
func (vk *VK) EnableMessagePack() {
vk.msgpack = true
}
// EnableZstd enable using zstd instead of gzip.
//
// This not use dict.
func (vk *VK) EnableZstd() {
vk.zstd = true
} }
func fmtReflectValue(value reflect.Value, depth int) string { func fmtReflectValue(value reflect.Value, depth int) string {

View File

@@ -1,5 +1,9 @@
package api // import "github.com/SevereCloud/vksdk/v2/api" package api // import "github.com/SevereCloud/vksdk/v2/api"
import (
"github.com/SevereCloud/vksdk/v2/object"
)
// AuthCheckPhone checks a user's phone number for correctness. // AuthCheckPhone checks a user's phone number for correctness.
// //
// https://vk.com/dev/auth.checkPhone // https://vk.com/dev/auth.checkPhone
@@ -24,3 +28,58 @@ func (vk *VK) AuthRestore(params Params) (response AuthRestoreResponse, err erro
err = vk.RequestUnmarshal("auth.restore", &response, params) err = vk.RequestUnmarshal("auth.restore", &response, params)
return return
} }
// AuthGetProfileInfoBySilentTokenResponse struct.
type AuthGetProfileInfoBySilentTokenResponse struct {
Success []object.AuthSilentTokenProfile `json:"success"`
Errors []AuthSilentTokenError `json:"errors"`
}
// AuthGetProfileInfoBySilentToken method.
//
// https://platform.vk.com/?p=DocsDashboard&docs=tokens_silent-token
func (vk *VK) AuthGetProfileInfoBySilentToken(params Params) (response AuthGetProfileInfoBySilentTokenResponse, err error) {
err = vk.RequestUnmarshal("auth.getProfileInfoBySilentToken", &response, params)
return
}
// ExchangeSilentTokenSource call conditions exchangeSilentToken.
//
// 0 Unknown
// 1 Silent authentication
// 2 Auth by login and password
// 3 Extended registration
// 4 Auth by exchange token
// 5 Auth by exchange token on reset password
// 6 Auth by exchange token on unblock
// 7 Auth by exchange token on reset session
// 8 Auth by exchange token on change password
// 9 Finish phone validation on authentication
// 10 Auth by code
// 11 Auth by external oauth
// 12 Reactivation
// 15 Auth by SDK temporary access-token
type ExchangeSilentTokenSource int
// AuthExchangeSilentAuthTokenResponse struct.
type AuthExchangeSilentAuthTokenResponse struct {
AccessToken string `json:"access_token"`
AccessTokenID string `json:"access_token_id"`
UserID int `json:"user_id"`
Phone string `json:"phone"`
PhoneValidated interface{} `json:"phone_validated"`
IsPartial bool `json:"is_partial"`
IsService bool `json:"is_service"`
AdditionalSignupRequired bool `json:"additional_signup_required"`
Email string `json:"email"`
Source ExchangeSilentTokenSource `json:"source"`
SourceDescription string `json:"source_description"`
}
// AuthExchangeSilentAuthToken method.
//
// https://platform.vk.com/?p=DocsDashboard&docs=tokens_access-token
func (vk *VK) AuthExchangeSilentAuthToken(params Params) (response AuthExchangeSilentAuthTokenResponse, err error) {
err = vk.RequestUnmarshal("auth.exchangeSilentAuthToken", &response, params)
return
}

View File

@@ -159,6 +159,9 @@ const (
ErrRateLimit ErrorType = 29 ErrRateLimit ErrorType = 29
ErrPrivateProfile ErrorType = 30 // This profile is private ErrPrivateProfile ErrorType = 30 // This profile is private
// Client version deprecated.
ErrClientVersionDeprecated ErrorType = 34
// Method execution was interrupted due to timeout. // Method execution was interrupted due to timeout.
ErrExecutionTimeout ErrorType = 36 ErrExecutionTimeout ErrorType = 36
@@ -177,6 +180,9 @@ const (
// Additional signup required. // Additional signup required.
ErrAdditionalSignupRequired ErrorType = 41 ErrAdditionalSignupRequired ErrorType = 41
// IP is not allowed.
ErrIPNotAllowed ErrorType = 42
// One of the parameters specified was missing or invalid // One of the parameters specified was missing or invalid
// //
// Check the required parameters list and their format on a method // Check the required parameters list and their format on a method
@@ -586,6 +592,18 @@ const (
// Can't send message, reply timed out. // Can't send message, reply timed out.
ErrMessagesReplyTimedOut ErrorType = 950 ErrMessagesReplyTimedOut ErrorType = 950
// You can't access donut chat without subscription.
ErrMessagesAccessDonutChat ErrorType = 962
// This user can't be added to the work chat, as they aren't an employe.
ErrMessagesAccessWorkChat ErrorType = 967
// Message cannot be forwarded.
ErrMessagesCantForwarded ErrorType = 969
// Cannot pin an expiring message.
ErrMessagesPinExpiringMessage ErrorType = 970
// Invalid phone number. // Invalid phone number.
ErrParamPhone ErrorType = 1000 ErrParamPhone ErrorType = 1000
@@ -598,6 +616,12 @@ const (
// Processing.. Try later. // Processing.. Try later.
ErrAuthDelay ErrorType = 1112 ErrAuthDelay ErrorType = 1112
// Anonymous token has expired.
ErrAnonymousTokenExpired ErrorType = 1114
// Anonymous token is invalid.
ErrAnonymousTokenInvalid ErrorType = 1116
// Invalid document id. // Invalid document id.
ErrParamDocID ErrorType = 1150 ErrParamDocID ErrorType = 1150
@@ -724,6 +748,9 @@ const (
// Market was already disabled in this group. // Market was already disabled in this group.
ErrMarketAlreadyDisabled ErrorType = 1432 ErrMarketAlreadyDisabled ErrorType = 1432
// Main album can not be hidden.
ErrMainAlbumCantHidden ErrorType = 1446
// Story has already expired. // Story has already expired.
ErrStoryExpired ErrorType = 1600 ErrStoryExpired ErrorType = 1600
@@ -783,6 +810,33 @@ const (
// Can't set AliExpress tag to this type of object. // Can't set AliExpress tag to this type of object.
ErrAliExpressTag ErrorType = 3800 ErrAliExpressTag ErrorType = 3800
// Invalid upload response.
ErrInvalidUploadResponse ErrorType = 5701
// Invalid upload hash.
ErrInvalidUploadHash ErrorType = 5702
// Invalid upload user.
ErrInvalidUploadUser ErrorType = 5703
// Invalid upload group.
ErrInvalidUploadGroup ErrorType = 5704
// Invalid crop data.
ErrInvalidCropData ErrorType = 5705
// To small avatar.
ErrToSmallAvatar ErrorType = 5706
// Photo not found.
ErrPhotoNotFound ErrorType = 5708
// Invalid Photo.
ErrInvalidPhoto ErrorType = 5709
// Invalid hash.
ErrInvalidHash ErrorType = 5710
) )
// ErrorSubtype is the subtype of an error. // ErrorSubtype is the subtype of an error.
@@ -946,3 +1000,31 @@ func (e AdsError) Is(target error) bool {
return false return false
} }
// AuthSilentTokenError struct.
type AuthSilentTokenError struct {
Token string `json:"token"`
Code ErrorType `json:"code"`
Description string `json:"description"`
}
// Error returns the description of a AuthSilentTokenError.
func (e AuthSilentTokenError) Error() string {
return "api: " + e.Description
}
// Is unwraps its first argument sequentially looking for an error that matches
// the second.
func (e AuthSilentTokenError) Is(target error) bool {
var tError *AuthSilentTokenError
if errors.As(target, &tError) {
return e.Code == tError.Code && e.Description == tError.Description
}
var tErrorType ErrorType
if errors.As(target, &tErrorType) {
return e.Code == tErrorType
}
return false
}

View File

@@ -1,6 +1,11 @@
package api package api
import "encoding/json" import (
"bytes"
"encoding/json"
"github.com/vmihailenco/msgpack/v5"
)
// ExecuteWithArgs a universal method for calling a sequence of other methods // ExecuteWithArgs a universal method for calling a sequence of other methods
// while saving and filtering interim results. // while saving and filtering interim results.
@@ -22,10 +27,23 @@ func (vk *VK) ExecuteWithArgs(code string, params Params, obj interface{}) error
} }
resp, err := vk.Handler("execute", params, reqParams) resp, err := vk.Handler("execute", params, reqParams)
if err != nil {
return err
}
jsonErr := json.Unmarshal(resp.Response, &obj) var decoderErr error
if jsonErr != nil {
return jsonErr if vk.msgpack {
dec := msgpack.NewDecoder(bytes.NewReader(resp.Response))
dec.SetCustomStructTag("json")
decoderErr = dec.Decode(&obj)
} else {
decoderErr = json.Unmarshal(resp.Response, &obj)
}
if decoderErr != nil {
return decoderErr
} }
if resp.ExecuteErrors != nil { if resp.ExecuteErrors != nil {

View File

@@ -20,6 +20,7 @@ func (vk *VK) MarketAdd(params Params) (response MarketAddResponse, err error) {
// MarketAddAlbumResponse struct. // MarketAddAlbumResponse struct.
type MarketAddAlbumResponse struct { type MarketAddAlbumResponse struct {
MarketAlbumID int `json:"market_album_id"` // Album ID MarketAlbumID int `json:"market_album_id"` // Album ID
AlbumsCount int `json:"albums_count"`
} }
// MarketAddAlbum creates new collection of items. // MarketAddAlbum creates new collection of items.
@@ -318,3 +319,19 @@ func (vk *VK) MarketSearch(params Params) (response MarketSearchResponse, err er
err = vk.RequestUnmarshal("market.search", &response, params) err = vk.RequestUnmarshal("market.search", &response, params)
return return
} }
// MarketSearchItemsResponse struct.
type MarketSearchItemsResponse struct {
Count int `json:"count"`
ViewType int `json:"view_type"`
Items []object.MarketMarketItem `json:"items"`
Groups []object.GroupsGroup `json:"groups,omitempty"`
}
// MarketSearchItems method.
//
// https://vk.com/dev/market.searchItems
func (vk *VK) MarketSearchItems(params Params) (response MarketSearchItemsResponse, err error) {
err = vk.RequestUnmarshal("market.searchItems", &response, params)
return
}

103
vendor/github.com/SevereCloud/vksdk/v2/api/marusia.go generated vendored Normal file
View File

@@ -0,0 +1,103 @@
package api // import "github.com/SevereCloud/vksdk/v2/api"
import (
"github.com/SevereCloud/vksdk/v2/object"
)
// MarusiaGetPictureUploadLinkResponse struct.
type MarusiaGetPictureUploadLinkResponse struct {
PictureUploadLink string `json:"picture_upload_link"` // Link
}
// MarusiaGetPictureUploadLink method.
//
// https://vk.com/dev/marusia_skill_docs10
func (vk *VK) MarusiaGetPictureUploadLink(params Params) (response MarusiaGetPictureUploadLinkResponse, err error) {
err = vk.RequestUnmarshal("marusia.getPictureUploadLink", &response, params)
return
}
// MarusiaSavePictureResponse struct.
type MarusiaSavePictureResponse struct {
AppID int `json:"app_id"`
PhotoID int `json:"photo_id"`
}
// MarusiaSavePicture method.
//
// https://vk.com/dev/marusia_skill_docs10
func (vk *VK) MarusiaSavePicture(params Params) (response MarusiaSavePictureResponse, err error) {
err = vk.RequestUnmarshal("marusia.savePicture", &response, params)
return
}
// MarusiaGetPicturesResponse struct.
type MarusiaGetPicturesResponse struct {
Count int `json:"count"`
Items []object.MarusiaPicture `json:"items"`
}
// MarusiaGetPictures method.
//
// https://vk.com/dev/marusia_skill_docs10
func (vk *VK) MarusiaGetPictures(params Params) (response MarusiaGetPicturesResponse, err error) {
err = vk.RequestUnmarshal("marusia.getPictures", &response, params)
return
}
// MarusiaDeletePicture delete picture.
//
// https://vk.com/dev/marusia_skill_docs10
func (vk *VK) MarusiaDeletePicture(params Params) (response int, err error) {
err = vk.RequestUnmarshal("marusia.deletePicture", &response, params)
return
}
// MarusiaGetAudioUploadLinkResponse struct.
type MarusiaGetAudioUploadLinkResponse struct {
AudioUploadLink string `json:"audio_upload_link"` // Link
}
// MarusiaGetAudioUploadLink method.
//
// https://vk.com/dev/marusia_skill_docs10
func (vk *VK) MarusiaGetAudioUploadLink(params Params) (response MarusiaGetAudioUploadLinkResponse, err error) {
err = vk.RequestUnmarshal("marusia.getAudioUploadLink", &response, params)
return
}
// MarusiaCreateAudioResponse struct.
type MarusiaCreateAudioResponse struct {
ID int `json:"id"`
Title string `json:"title"`
}
// MarusiaCreateAudio method.
//
// https://vk.com/dev/marusia_skill_docs10
func (vk *VK) MarusiaCreateAudio(params Params) (response MarusiaCreateAudioResponse, err error) {
err = vk.RequestUnmarshal("marusia.createAudio", &response, params)
return
}
// MarusiaGetAudiosResponse struct.
type MarusiaGetAudiosResponse struct {
Count int `json:"count"`
Audios []object.MarusiaAudio `json:"audios"`
}
// MarusiaGetAudios method.
//
// https://vk.com/dev/marusia_skill_docs10
func (vk *VK) MarusiaGetAudios(params Params) (response MarusiaGetAudiosResponse, err error) {
err = vk.RequestUnmarshal("marusia.getAudios", &response, params)
return
}
// MarusiaDeleteAudio delete audio.
//
// https://vk.com/dev/marusia_skill_docs10
func (vk *VK) MarusiaDeleteAudio(params Params) (response int, err error) {
err = vk.RequestUnmarshal("marusia.deleteAudio", &response, params)
return
}

View File

@@ -1,7 +1,10 @@
package api // import "github.com/SevereCloud/vksdk/v2/api" package api // import "github.com/SevereCloud/vksdk/v2/api"
import ( import (
"strconv"
"github.com/SevereCloud/vksdk/v2/object" "github.com/SevereCloud/vksdk/v2/object"
"github.com/vmihailenco/msgpack/v5"
) )
// MessagesAddChatUser adds a new user to a chat. // MessagesAddChatUser adds a new user to a chat.
@@ -31,11 +34,34 @@ func (vk *VK) MessagesCreateChat(params Params) (response int, err error) {
// MessagesDeleteResponse struct. // MessagesDeleteResponse struct.
type MessagesDeleteResponse map[string]int type MessagesDeleteResponse map[string]int
// DecodeMsgpack funcion.
func (resp *MessagesDeleteResponse) DecodeMsgpack(dec *msgpack.Decoder) error {
data, err := dec.DecodeRaw()
if err != nil {
return err
}
var respMap map[int]int
err = msgpack.Unmarshal(data, &respMap)
if err != nil {
return err
}
*resp = make(MessagesDeleteResponse)
for key, val := range respMap {
(*resp)[strconv.Itoa(key)] = val
}
return nil
}
// MessagesDelete deletes one or more messages. // MessagesDelete deletes one or more messages.
// //
// https://vk.com/dev/messages.delete // https://vk.com/dev/messages.delete
func (vk *VK) MessagesDelete(params Params) (response MessagesDeleteResponse, err error) { func (vk *VK) MessagesDelete(params Params) (response MessagesDeleteResponse, err error) {
err = vk.RequestUnmarshal("messages.delete", &response, params) err = vk.RequestUnmarshal("messages.delete", &response, params)
return return
} }

View File

@@ -572,9 +572,10 @@ func (vk *VK) PhotosSaveOwnerCoverPhoto(params Params) (response PhotosSaveOwner
// PhotosSaveOwnerPhotoResponse struct. // PhotosSaveOwnerPhotoResponse struct.
type PhotosSaveOwnerPhotoResponse struct { type PhotosSaveOwnerPhotoResponse struct {
PhotoHash string `json:"photo_hash"` PhotoHash string `json:"photo_hash"`
PhotoSrc string `json:"photo_src"` // BUG(VK): returns false
PhotoSrcBig string `json:"photo_src_big"` // PhotoSrc string `json:"photo_src"`
PhotoSrcSmall string `json:"photo_src_small"` // PhotoSrcBig string `json:"photo_src_big"`
// PhotoSrcSmall string `json:"photo_src_small"`
Saved int `json:"saved"` Saved int `json:"saved"`
PostID int `json:"post_id"` PostID int `json:"post_id"`
} }

View File

@@ -959,3 +959,57 @@ func (vk *VK) UploadGroupImage(imageType string, file io.Reader) (response objec
return return
} }
// UploadMarusiaPicture uploading picture.
//
// Limits: height not more than 600 px,
// aspect ratio of at least 2:1.
func (vk *VK) UploadMarusiaPicture(file io.Reader) (response MarusiaSavePictureResponse, err error) {
uploadServer, err := vk.MarusiaGetPictureUploadLink(nil)
if err != nil {
return
}
bodyContent, err := vk.UploadFile(uploadServer.PictureUploadLink, file, "photo", "photo.jpg")
if err != nil {
return
}
var handler object.MarusiaPictureUploadResponse
err = json.Unmarshal(bodyContent, &handler)
if err != nil {
return
}
photo, _ := json.Marshal(handler.Photo)
response, err = vk.MarusiaSavePicture(Params{
"server": handler.Server,
"photo": string(photo),
"hash": handler.Hash,
})
return
}
// UploadMarusiaAudio uploading audio.
//
// https://vk.com/dev/marusia_skill_docs10
func (vk *VK) UploadMarusiaAudio(file io.Reader) (response MarusiaCreateAudioResponse, err error) {
uploadServer, err := vk.MarusiaGetAudioUploadLink(nil)
if err != nil {
return
}
bodyContent, err := vk.UploadFile(uploadServer.AudioUploadLink, file, "file", "audio.mp3")
if err != nil {
return
}
response, err = vk.MarusiaCreateAudio(Params{
"audio_meta": string(bodyContent),
})
return
}

View File

@@ -1,9 +1,8 @@
package api // import "github.com/SevereCloud/vksdk/v2/api" package api // import "github.com/SevereCloud/vksdk/v2/api"
import ( import (
"encoding/json"
"github.com/SevereCloud/vksdk/v2/object" "github.com/SevereCloud/vksdk/v2/object"
"github.com/vmihailenco/msgpack/v5"
) )
// UtilsCheckLinkResponse struct. // UtilsCheckLinkResponse struct.
@@ -89,17 +88,34 @@ func (vk *VK) UtilsGetShortLink(params Params) (response UtilsGetShortLinkRespon
// UtilsResolveScreenNameResponse struct. // UtilsResolveScreenNameResponse struct.
type UtilsResolveScreenNameResponse object.UtilsDomainResolved type UtilsResolveScreenNameResponse object.UtilsDomainResolved
// UnmarshalJSON UtilsResolveScreenNameResponse.
//
// BUG(VK): UtilsResolveScreenNameResponse return [].
func (resp *UtilsResolveScreenNameResponse) UnmarshalJSON(data []byte) error {
var p object.UtilsDomainResolved
err := p.UnmarshalJSON(data)
*resp = UtilsResolveScreenNameResponse(p)
return err
}
// DecodeMsgpack UtilsResolveScreenNameResponse.
//
// BUG(VK): UtilsResolveScreenNameResponse return [].
func (resp *UtilsResolveScreenNameResponse) DecodeMsgpack(dec *msgpack.Decoder) error {
var p object.UtilsDomainResolved
err := p.DecodeMsgpack(dec)
*resp = UtilsResolveScreenNameResponse(p)
return err
}
// UtilsResolveScreenName detects a type of object (e.g., user, community, application) and its ID by screen name. // UtilsResolveScreenName detects a type of object (e.g., user, community, application) and its ID by screen name.
// //
// https://vk.com/dev/utils.resolveScreenName // https://vk.com/dev/utils.resolveScreenName
func (vk *VK) UtilsResolveScreenName(params Params) (response UtilsResolveScreenNameResponse, err error) { func (vk *VK) UtilsResolveScreenName(params Params) (response UtilsResolveScreenNameResponse, err error) {
rawResponse, err := vk.Request("utils.resolveScreenName", params) err = vk.RequestUnmarshal("utils.resolveScreenName", &response, params)
// Если короткое имя screen_name не занято, то будет возвращён пустой объект.
if err != nil || string(rawResponse) == "[]" {
return
}
err = json.Unmarshal(rawResponse, &response)
return return
} }

View File

@@ -7,6 +7,6 @@ package vksdk
// Module constants. // Module constants.
const ( const (
Version = "2.10.0" Version = "2.13.1"
API = "5.131" API = "5.131"
) )

View File

@@ -10,7 +10,7 @@ Long Poll настраивается автоматически. Вам не т
### Версия API ### Версия API
Данная библиотека поддерживает версию API **5.122**. Данная библиотека поддерживает версию API **5.131**.
### Инициализация ### Инициализация

View File

@@ -8,8 +8,11 @@ package longpoll // import "github.com/SevereCloud/vksdk/v2/longpoll-bot"
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strconv"
"github.com/SevereCloud/vksdk/v2" "github.com/SevereCloud/vksdk/v2"
"github.com/SevereCloud/vksdk/v2/api" "github.com/SevereCloud/vksdk/v2/api"
@@ -117,7 +120,7 @@ func (lp *LongPoll) check(ctx context.Context) (response Response, err error) {
} }
defer resp.Body.Close() defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(&response) response, err = parseResponse(resp.Body)
if err != nil { if err != nil {
return response, err return response, err
} }
@@ -127,6 +130,59 @@ func (lp *LongPoll) check(ctx context.Context) (response Response, err error) {
return response, err return response, err
} }
func parseResponse(reader io.Reader) (response Response, err error) {
decoder := json.NewDecoder(reader)
for decoder.More() {
token, err := decoder.Token()
if err != nil {
if errors.Is(err, io.EOF) {
break
}
return response, err
}
t, ok := token.(string)
if !ok {
continue
}
switch t {
case "failed":
raw, err := decoder.Token()
if err != nil {
return response, err
}
response.Failed = int(raw.(float64))
case "updates":
var updates []events.GroupEvent
err = decoder.Decode(&updates)
if err != nil {
return response, err
}
response.Updates = updates
case "ts":
// can be a number in the response with "failed" field: {"ts":8,"failed":1}
// or string, e.g. {"ts":"8","updates":[]}
rawTs, err := decoder.Token()
if err != nil {
return response, err
}
if ts, isNumber := rawTs.(float64); isNumber {
response.Ts = strconv.Itoa(int(ts))
} else {
response.Ts = rawTs.(string)
}
}
}
return response, err
}
func (lp *LongPoll) checkResponse(response Response) (err error) { func (lp *LongPoll) checkResponse(response Response) (err error) {
switch response.Failed { switch response.Failed {
case 0: case 0:

View File

@@ -77,6 +77,9 @@ type AccountAccountCounters struct {
MenuClipsBadge int `json:"menu_clips_badge"` // New menu clips badge number MenuClipsBadge int `json:"menu_clips_badge"` // New menu clips badge number
Videos int `json:"videos"` // New video tags number Videos int `json:"videos"` // New video tags number
Faves int `json:"faves"` // New faves number Faves int `json:"faves"` // New faves number
Calls int `json:"calls"` // New calls number
MenuSuperappFriendsBadge int `json:"menu_superapp_friends_badge"`
MenuNewClipsBadge int `json:"menu_new_clips_badge"`
} }
// AccountInfo struct. // AccountInfo struct.
@@ -107,6 +110,7 @@ type AccountInfo struct {
IsLiveStreamingEnabled BaseBoolInt `json:"is_live_streaming_enabled"` IsLiveStreamingEnabled BaseBoolInt `json:"is_live_streaming_enabled"`
IsNewLiveStreamingEnabled BaseBoolInt `json:"is_new_live_streaming_enabled"` IsNewLiveStreamingEnabled BaseBoolInt `json:"is_new_live_streaming_enabled"`
LinkRedirects map[string]string `json:"link_redirects"` LinkRedirects map[string]string `json:"link_redirects"`
VkPayEndpointV2 string `json:"vk_pay_endpoint_v2"`
} }
// AccountPushSettings struct. // AccountPushSettings struct.

View File

@@ -13,6 +13,7 @@ type AdsAccount struct {
AccountName string `json:"account_name"` AccountName string `json:"account_name"`
AccountStatus BaseBoolInt `json:"account_status"` // Information whether account is active AccountStatus BaseBoolInt `json:"account_status"` // Information whether account is active
CanViewBudget BaseBoolInt `json:"can_view_budget"` CanViewBudget BaseBoolInt `json:"can_view_budget"`
AdNetworkAllowedPotentially BaseBoolInt `json:"ad_network_allowed_potentially"`
AccountType string `json:"account_type"` AccountType string `json:"account_type"`
} }
@@ -318,3 +319,10 @@ type AdsPromotedPostReach struct {
VideoViews75p int `json:"video_views_75p"` // Video views for 75 percent VideoViews75p int `json:"video_views_75p"` // Video views for 75 percent
VideoViewsStart int `json:"video_views_start"` // Video starts VideoViewsStart int `json:"video_views_start"` // Video starts
} }
// AdsMusician struct.
type AdsMusician struct {
ID int `json:"id"` // Targeting music artist ID
Name string `json:"name"` // Music artist name
Avatar string `json:"avatar,omitempty"` // Music artist photo.
}

View File

@@ -60,6 +60,7 @@ type AppsApp struct {
IsNew BaseBoolInt `json:"is_new"` IsNew BaseBoolInt `json:"is_new"`
New BaseBoolInt `json:"new"` New BaseBoolInt `json:"new"`
IsInstalled BaseBoolInt `json:"is_installed"` IsInstalled BaseBoolInt `json:"is_installed"`
HasVkConnect BaseBoolInt `json:"has_vk_connect"`
LeaderboardType int `json:"leaderboard_type"` LeaderboardType int `json:"leaderboard_type"`
MembersCount int `json:"members_count"` // Members number MembersCount int `json:"members_count"` // Members number
PlatformID int `json:"platform_id"` // Application ID in store PlatformID int `json:"platform_id"` // Application ID in store
@@ -78,7 +79,7 @@ type AppsApp struct {
// mobile_controls_type = 0 - прозрачный элемент управления поверх области с игрой; // mobile_controls_type = 0 - прозрачный элемент управления поверх области с игрой;
// mobile_controls_type = 1 - чёрная полоска над областью с игрой; // mobile_controls_type = 1 - чёрная полоска над областью с игрой;
// mobile_controls_type = 2 - только для vk apps, без контроллов. // mobile_controls_type = 2 - только для vk apps, без элементов управления'.
MobileControlsType int `json:"mobile_controls_type"` MobileControlsType int `json:"mobile_controls_type"`
// mobile_view_support_type = 0 - игра не использует нижнюю часть экрана на iPhoneX, черная полоса есть. // mobile_view_support_type = 0 - игра не использует нижнюю часть экрана на iPhoneX, черная полоса есть.

17
vendor/github.com/SevereCloud/vksdk/v2/object/auth.go generated vendored Normal file
View File

@@ -0,0 +1,17 @@
package object // import "github.com/SevereCloud/vksdk/v2/object"
// AuthSilentTokenProfile struct.
type AuthSilentTokenProfile struct {
Token string `json:"token"`
Expires int `json:"expires"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Photo50 string `json:"photo_50"`
Photo100 string `json:"photo_100"`
Photo200 string `json:"photo_200"`
Phone string `json:"phone"`
PhoneValidated interface{} `json:"phone_validated"` // int | bool
UserID int `json:"user_id"`
IsPartial BaseBoolInt `json:"is_partial"`
IsService BaseBoolInt `json:"is_service"`
}

View File

@@ -4,9 +4,9 @@ package object // import "github.com/SevereCloud/vksdk/v2/object"
type DatabaseCity struct { type DatabaseCity struct {
ID int `json:"id"` // City ID ID int `json:"id"` // City ID
Title string `json:"title"` // City title Title string `json:"title"` // City title
Area string `json:"area"` Area string `json:"area,omitempty"`
Region string `json:"region"` Region string `json:"region,omitempty"`
Important BaseBoolInt `json:"important"` Important BaseBoolInt `json:"important,omitempty"`
} }
// DatabaseMetroStation struct. // DatabaseMetroStation struct.

View File

@@ -1,9 +1,13 @@
package object // import "github.com/SevereCloud/vksdk/v2/object" package object // import "github.com/SevereCloud/vksdk/v2/object"
import ( import (
"bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"reflect" "reflect"
"github.com/vmihailenco/msgpack/v5"
"github.com/vmihailenco/msgpack/v5/msgpcode"
) )
// GroupsAddress WorkInfoStatus of information about timetable. // GroupsAddress WorkInfoStatus of information about timetable.
@@ -110,112 +114,113 @@ const (
// GroupsGroup struct. // GroupsGroup struct.
type GroupsGroup struct { type GroupsGroup struct {
AdminLevel int `json:"admin_level"`
Deactivated string `json:"deactivated"` // Information whether community is banned
FinishDate int `json:"finish_date"` // Finish date in Unixtime format
ID int `json:"id"` // Community ID ID int `json:"id"` // Community ID
Name string `json:"name"` // Community name Name string `json:"name"` // Community name
Photo100 string `json:"photo_100"` // URL of square photo of the community with 100 pixels in width
Photo200 string `json:"photo_200"` // URL of square photo of the community with 200 pixels in width
Photo50 string `json:"photo_50"` // URL of square photo of the community with 50 pixels in width
ScreenName string `json:"screen_name"` // Domain of the community page ScreenName string `json:"screen_name"` // Domain of the community page
StartDate int `json:"start_date"` // Start date in Unixtime format
Type string `json:"type"` Type string `json:"type"`
Market GroupsMarketInfo `json:"market"`
MemberStatus int `json:"member_status"` // Current user's member status
IsClosed int `json:"is_closed"` IsClosed int `json:"is_closed"`
City BaseObject `json:"city"` AdminLevel int `json:"admin_level,omitempty"`
Country BaseCountry `json:"country"` Deactivated string `json:"deactivated,omitempty"` // Information whether community is banned
FinishDate int `json:"finish_date,omitempty"` // Finish date in Unixtime format
Photo100 string `json:"photo_100,omitempty"` // URL of square photo of the community with 100 pixels in width
Photo200 string `json:"photo_200,omitempty"` // URL of square photo of the community with 200 pixels in width
Photo50 string `json:"photo_50,omitempty"` // URL of square photo of the community with 50 pixels in width
StartDate int `json:"start_date,omitempty"` // Start date in Unixtime format
Market GroupsMarketInfo `json:"market,omitempty"`
MemberStatus int `json:"member_status,omitempty"` // Current user's member status
City BaseObject `json:"city,omitempty"`
Country BaseCountry `json:"country,omitempty"`
// Information whether current user is administrator. // Information whether current user is administrator.
IsAdmin BaseBoolInt `json:"is_admin"` IsAdmin BaseBoolInt `json:"is_admin"`
// Information whether current user is advertiser. // Information whether current user is advertiser.
IsAdvertiser BaseBoolInt `json:"is_advertiser"` IsAdvertiser BaseBoolInt `json:"is_advertiser,omitempty"`
// Information whether current user is member. // Information whether current user is member.
IsMember BaseBoolInt `json:"is_member"` IsMember BaseBoolInt `json:"is_member,omitempty"`
// Information whether community is in faves. // Information whether community is in faves.
IsFavorite BaseBoolInt `json:"is_favorite"` IsFavorite BaseBoolInt `json:"is_favorite,omitempty"`
// Information whether community is adult. // Information whether community is adult.
IsAdult BaseBoolInt `json:"is_adult"` IsAdult BaseBoolInt `json:"is_adult,omitempty"`
// Information whether current user is subscribed. // Information whether current user is subscribed.
IsSubscribed BaseBoolInt `json:"is_subscribed"` IsSubscribed BaseBoolInt `json:"is_subscribed,omitempty"`
// Information whether current user can post on community's wall. // Information whether current user can post on community's wall.
CanPost BaseBoolInt `json:"can_post"` CanPost BaseBoolInt `json:"can_post,omitempty"`
// Information whether current user can see all posts on community's wall. // Information whether current user can see all posts on community's wall.
CanSeeAllPosts BaseBoolInt `json:"can_see_all_posts"` CanSeeAllPosts BaseBoolInt `json:"can_see_all_posts,omitempty"`
// Information whether current user can create topic. // Information whether current user can create topic.
CanCreateTopic BaseBoolInt `json:"can_create_topic"` CanCreateTopic BaseBoolInt `json:"can_create_topic,omitempty"`
// Information whether current user can upload video. // Information whether current user can upload video.
CanUploadVideo BaseBoolInt `json:"can_upload_video"` CanUploadVideo BaseBoolInt `json:"can_upload_video,omitempty"`
// Information whether current user can upload doc. // Information whether current user can upload doc.
CanUploadDoc BaseBoolInt `json:"can_upload_doc"` CanUploadDoc BaseBoolInt `json:"can_upload_doc,omitempty"`
// Information whether community has photo. // Information whether community has photo.
HasPhoto BaseBoolInt `json:"has_photo"` HasPhoto BaseBoolInt `json:"has_photo,omitempty"`
// Information whether current user can send a message to community. // Information whether current user can send a message to community.
CanMessage BaseBoolInt `json:"can_message"` CanMessage BaseBoolInt `json:"can_message,omitempty"`
// Information whether community can send a message to current user. // Information whether community can send a message to current user.
IsMessagesBlocked BaseBoolInt `json:"is_messages_blocked"` IsMessagesBlocked BaseBoolInt `json:"is_messages_blocked,omitempty"`
// Information whether community can send notifications by phone number to current user. // Information whether community can send notifications by phone number to current user.
CanSendNotify BaseBoolInt `json:"can_send_notify"` CanSendNotify BaseBoolInt `json:"can_send_notify,omitempty"`
// Information whether current user is subscribed to podcasts. // Information whether current user is subscribed to podcasts.
IsSubscribedPodcasts BaseBoolInt `json:"is_subscribed_podcasts"` IsSubscribedPodcasts BaseBoolInt `json:"is_subscribed_podcasts,omitempty"`
// Owner in whitelist or not. // Owner in whitelist or not.
CanSubscribePodcasts BaseBoolInt `json:"can_subscribe_podcasts"` CanSubscribePodcasts BaseBoolInt `json:"can_subscribe_podcasts,omitempty"`
// Can subscribe to wall. // Can subscribe to wall.
CanSubscribePosts BaseBoolInt `json:"can_subscribe_posts"` CanSubscribePosts BaseBoolInt `json:"can_subscribe_posts,omitempty"`
// Information whether community has market app. // Information whether community has market app.
HasMarketApp BaseBoolInt `json:"has_market_app"` HasMarketApp BaseBoolInt `json:"has_market_app,omitempty"`
IsHiddenFromFeed BaseBoolInt `json:"is_hidden_from_feed"` IsHiddenFromFeed BaseBoolInt `json:"is_hidden_from_feed,omitempty"`
IsMarketCartEnabled BaseBoolInt `json:"is_market_cart_enabled"` IsMarketCartEnabled BaseBoolInt `json:"is_market_cart_enabled,omitempty"`
Verified BaseBoolInt `json:"verified"` // Information whether community is verified Verified BaseBoolInt `json:"verified,omitempty"` // Information whether community is verified
// Information whether the community has a fire pictogram. // Information whether the community has a fire pictogram.
Trending BaseBoolInt `json:"trending"` Trending BaseBoolInt `json:"trending,omitempty"`
Description string `json:"description"` // Community description Description string `json:"description,omitempty"` // Community description
WikiPage string `json:"wiki_page"` // Community's main wiki page title WikiPage string `json:"wiki_page,omitempty"` // Community's main wiki page title
MembersCount int `json:"members_count"` // Community members number MembersCount int `json:"members_count,omitempty"` // Community members number
Counters GroupsCountersGroup `json:"counters"` Counters GroupsCountersGroup `json:"counters,omitempty"`
Cover GroupsCover `json:"cover"` Cover GroupsCover `json:"cover,omitempty"`
// Type of group, start date of event or category of public page. // Type of group, start date of event or category of public page.
Activity string `json:"activity"` Activity string `json:"activity,omitempty"`
FixedPost int `json:"fixed_post"` // Fixed post ID FixedPost int `json:"fixed_post,omitempty"` // Fixed post ID
Status string `json:"status"` // Community status Status string `json:"status,omitempty"` // Community status
MainAlbumID int `json:"main_album_id"` // Community's main photo album ID MainAlbumID int `json:"main_album_id,omitempty"` // Community's main photo album ID
Links []GroupsLinksItem `json:"links"` Links []GroupsLinksItem `json:"links,omitempty"`
Contacts []GroupsContactsItem `json:"contacts"` Contacts []GroupsContactsItem `json:"contacts,omitempty"`
Site string `json:"site"` // Community's website Site string `json:"site,omitempty"` // Community's website
MainSection int `json:"main_section"` MainSection int `json:"main_section,omitempty"`
OnlineStatus GroupsOnlineStatus `json:"online_status"` // Status of replies in community messages OnlineStatus GroupsOnlineStatus `json:"online_status,omitempty"` // Status of replies in community messages
AgeLimits int `json:"age_limits"` // Information whether age limit AgeLimits int `json:"age_limits,omitempty"` // Information whether age limit
BanInfo GroupsGroupBanInfo `json:"ban_info"` // User ban info BanInfo GroupsGroupBanInfo `json:"ban_info,omitempty"` // User ban info
Addresses GroupsAddressesInfo `json:"addresses"` // Info about addresses in Groups Addresses GroupsAddressesInfo `json:"addresses,omitempty"` // Info about addresses in Groups
LiveCovers GroupsLiveCovers `json:"live_covers"` LiveCovers GroupsLiveCovers `json:"live_covers,omitempty"`
CropPhoto UsersCropPhoto `json:"crop_photo"` CropPhoto UsersCropPhoto `json:"crop_photo,omitempty"`
Wall int `json:"wall"` Wall int `json:"wall,omitempty"`
ActionButton GroupsActionButton `json:"action_button"` ActionButton GroupsActionButton `json:"action_button,omitempty"`
TrackCode string `json:"track_code"` TrackCode string `json:"track_code,omitempty"`
PublicDateLabel string `json:"public_date_label"` PublicDateLabel string `json:"public_date_label,omitempty"`
AuthorID int `json:"author_id"` AuthorID int `json:"author_id,omitempty"`
Phone string `json:"phone"` Phone string `json:"phone,omitempty"`
Like GroupsGroupLike `json:"like"`
} }
// ToMention return mention. // ToMention return mention.
@@ -223,6 +228,18 @@ func (group GroupsGroup) ToMention() string {
return fmt.Sprintf("[club%d|%s]", group.ID, group.Name) return fmt.Sprintf("[club%d|%s]", group.ID, group.Name)
} }
// GroupsGroupLike struct.
type GroupsGroupLike struct {
IsLiked BaseBoolInt `json:"is_liked"`
Friends GroupsGroupLikeFriends `json:"friends"`
}
// GroupsGroupLikeFriends struct.
type GroupsGroupLikeFriends struct {
Count int `json:"count"`
Preview []int `json:"preview"`
}
// GroupsLiveCovers struct. // GroupsLiveCovers struct.
type GroupsLiveCovers struct { type GroupsLiveCovers struct {
IsEnabled BaseBoolInt `json:"is_enabled"` IsEnabled BaseBoolInt `json:"is_enabled"`
@@ -285,6 +302,60 @@ type GroupsCountersGroup struct {
Topics int `json:"topics"` // Topics number Topics int `json:"topics"` // Topics number
Videos int `json:"videos"` // Videos number Videos int `json:"videos"` // Videos number
Narratives int `json:"narratives"` // Narratives number Narratives int `json:"narratives"` // Narratives number
Clips int `json:"clips"` // Clips number
ClipsFollowers int `json:"clips_followers"` // Clips followers number
}
// UnmarshalJSON GroupsCountersGroup.
//
// BUG(VK): GroupsCountersGroup return [].
func (personal *GroupsCountersGroup) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte("[]")) {
return nil
}
type renamedGroupsCountersGroup GroupsCountersGroup
var r renamedGroupsCountersGroup
err := json.Unmarshal(data, &r)
if err != nil {
return err
}
*personal = GroupsCountersGroup(r)
return nil
}
// DecodeMsgpack GroupsCountersGroup.
//
// BUG(VK): GroupsCountersGroup return [].
func (personal *GroupsCountersGroup) DecodeMsgpack(dec *msgpack.Decoder) error {
data, err := dec.DecodeRaw()
if err != nil {
return err
}
if bytes.Equal(data, []byte{msgpcode.FixedArrayLow}) {
return nil
}
type renamedGroupsCountersGroup GroupsCountersGroup
var r renamedGroupsCountersGroup
d := msgpack.NewDecoder(bytes.NewReader(data))
d.SetCustomStructTag("json")
err = d.Decode(&r)
if err != nil {
return err
}
*personal = GroupsCountersGroup(r)
return nil
} }
// GroupsCover struct. // GroupsCover struct.
@@ -479,6 +550,70 @@ type GroupsGroupSettings struct {
SecondarySection int `json:"secondary_section"` SecondarySection int `json:"secondary_section"`
ActionButton GroupsActionButton `json:"action_button"` ActionButton GroupsActionButton `json:"action_button"`
Phone string `json:"phone"` Phone string `json:"phone"`
RecognizePhoto int `json:"recognize_photo"`
MarketServices GroupsMarketServices `json:"market_services"`
Narratives int `json:"narratives"`
Clips int `json:"clips"`
Textlives int `json:"textlives"`
Youla GroupsYoula `json:"youla"`
}
// GroupsMarketServices struct.
type GroupsMarketServices struct {
Enabled BaseBoolInt `json:"enabled"`
CanMessage BaseBoolInt `json:"can_message"`
CommentsEnabled BaseBoolInt `json:"comments_enabled"`
ContactID int `json:"contact_id"`
Currency MarketCurrency `json:"currency"`
ViewType GroupsSelectedItems `json:"view_type"`
BlockName GroupsSelectedItems `json:"block_name"`
ButtonLabel GroupsSelectedItems `json:"button_label"`
}
// GroupsSelectedItems struct.
type GroupsSelectedItems struct {
SelectedItemID int64 `json:"selected_item_id"`
Items []BaseObjectWithName `json:"items"`
}
// GroupsYoula struct.
type GroupsYoula struct {
CategoryTree GroupsYoulaCategory `json:"category_tree"`
GroupSettings GroupsYoulaSettings `json:"group_settings"`
}
// GroupsYoulaCategory struct.
type GroupsYoulaCategory struct {
ID int `json:"id"`
Title string `json:"title"`
Subcategories []GroupsYoulaSubcategory `json:"subcategories"`
}
// GroupsYoulaSubcategory struct.
type GroupsYoulaSubcategory struct {
ID int `json:"id"`
Title string `json:"title"`
ParentID int `json:"parent_id"`
Subcategories []GroupsYoulaSubcategory `json:"subcategories"`
}
// GroupsYoulaSettings struct.
type GroupsYoulaSettings struct {
IsActive BaseBoolInt `json:"is_active"`
IsModerated BaseBoolInt `json:"is_moderated"`
ShowModerationSetting BaseBoolInt `json:"show_moderation_setting"`
ModerationStatus int `json:"moderation_status"`
DeclineReason string `json:"decline_reason"`
GroupMode int `json:"group_mode"`
SelectedCategoryIDS []int `json:"selected_category_ids"`
Lat float64 `json:"lat"`
Long float64 `json:"long"`
Radius float64 `json:"radius"`
RadiusArea string `json:"radius_area"`
Address string `json:"address"`
Radiuses []float64 `json:"radiuses"`
} }
// GroupsSectionsList struct. // GroupsSectionsList struct.
@@ -532,6 +667,53 @@ func (g *GroupsSectionsList) UnmarshalJSON(data []byte) error {
return nil return nil
} }
// DecodeMsgpack need for decode dynamic array (Example: [1, "Фотографии"]) to struct.
func (g *GroupsSectionsList) DecodeMsgpack(dec *msgpack.Decoder) error {
data, err := dec.DecodeRaw()
if err != nil {
return err
}
var alias []interface{}
err = msgpack.Unmarshal(data, &alias)
if err != nil {
return err
}
if len(alias) != 2 {
return &json.UnmarshalTypeError{
Value: string(data),
Type: reflect.TypeOf((*GroupsSectionsList)(nil)),
}
}
id, ok := alias[0].(int8)
if !ok {
return &json.UnmarshalTypeError{
Value: string(data),
Type: reflect.TypeOf((*GroupsSectionsList)(nil)),
Struct: "GroupsSectionsList",
Field: "ID",
}
}
name, ok := alias[1].(string)
if !ok {
return &json.UnmarshalTypeError{
Value: string(data),
Type: reflect.TypeOf((*GroupsSectionsList)(nil)),
Struct: "GroupsSectionsList",
Field: "Name",
}
}
g.ID = int(id)
g.Name = name
return nil
}
// GroupsActionType for action_button in groups. // GroupsActionType for action_button in groups.
type GroupsActionType string type GroupsActionType string
@@ -685,7 +867,10 @@ type GroupsLongPollServer struct {
Ts string `json:"ts"` // Number of the last event Ts string `json:"ts"` // Number of the last event
} }
// TODO: func (g GroupsLongPollServer) GetURL() string { // GetURL return link.
func (lp GroupsLongPollServer) GetURL(wait int) string {
return fmt.Sprintf("%s?act=a_check&key=%s&ts=%s&wait=%d", lp.Server, lp.Key, lp.Ts, wait)
}
// GroupsLongPollSettings struct. // GroupsLongPollSettings struct.
type GroupsLongPollSettings struct { type GroupsLongPollSettings struct {
@@ -714,12 +899,14 @@ type GroupsMarketInfo struct {
Enabled BaseBoolInt `json:"enabled"` // Information whether the market is enabled Enabled BaseBoolInt `json:"enabled"` // Information whether the market is enabled
CommentsEnabled BaseBoolInt `json:"comments_enabled,omitempty"` CommentsEnabled BaseBoolInt `json:"comments_enabled,omitempty"`
CanMessage BaseBoolInt `json:"can_message,omitempty"` CanMessage BaseBoolInt `json:"can_message,omitempty"`
IsHsEnabled BaseBoolInt `json:"is_hs_enabled,omitempty"`
MainAlbumID int `json:"main_album_id,omitempty"` // Main market album ID MainAlbumID int `json:"main_album_id,omitempty"` // Main market album ID
PriceMax string `json:"price_max,omitempty"` // Maximum price PriceMax string `json:"price_max,omitempty"` // Maximum price
PriceMin string `json:"price_min,omitempty"` // Minimum price PriceMin string `json:"price_min,omitempty"` // Minimum price
Wiki PagesWikipageFull `json:"wiki,omitempty"` Wiki PagesWikipageFull `json:"wiki,omitempty"`
CityIDs []int `json:"city_ids"` CityIDs []int `json:"city_ids"`
CountryIDs []int `json:"country_ids,omitempty"` CountryIDs []int `json:"country_ids,omitempty"`
MinOrderPrice MarketPrice `json:"min_order_price,omitempty"`
} }
// GroupsGroupRole Role type. // GroupsGroupRole Role type.

View File

@@ -4,6 +4,9 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/vmihailenco/msgpack/v5"
"github.com/vmihailenco/msgpack/v5/msgpcode"
) )
// Information whether the MarketMarketItem is available. // Information whether the MarketMarketItem is available.
@@ -28,6 +31,8 @@ type MarketMarketAlbum struct {
Photo PhotosPhoto `json:"photo"` Photo PhotosPhoto `json:"photo"`
Title string `json:"title"` // Market album title Title string `json:"title"` // Market album title
UpdatedTime int `json:"updated_time"` // Date when album has been updated last time in Unixtime UpdatedTime int `json:"updated_time"` // Date when album has been updated last time in Unixtime
IsMain BaseBoolInt `json:"is_main"`
IsHidden BaseBoolInt `json:"is_hidden"`
} }
// ToAttachment return attachment format. // ToAttachment return attachment format.
@@ -98,6 +103,36 @@ func (market *MarketMarketItem) UnmarshalJSON(data []byte) error {
return nil return nil
} }
// DecodeMsgpack MarketMarketItem.
//
// BUG(VK): https://github.com/SevereCloud/vksdk/issues/147
func (market *MarketMarketItem) DecodeMsgpack(dec *msgpack.Decoder) error {
data, err := dec.DecodeRaw()
if err != nil {
return err
}
if bytes.Equal(data, []byte{msgpcode.False}) {
return nil
}
type renamedMarketMarketItem MarketMarketItem
var r renamedMarketMarketItem
d := msgpack.NewDecoder(bytes.NewReader(data))
d.SetCustomStructTag("json")
err = d.Decode(&r)
if err != nil {
return err
}
*market = MarketMarketItem(r)
return nil
}
// MarketMarketItemProperty struct. // MarketMarketItemProperty struct.
type MarketMarketItemProperty struct { type MarketMarketItemProperty struct {
VariantID int `json:"variant_id"` VariantID int `json:"variant_id"`
@@ -149,6 +184,36 @@ func (m *MarketPrice) UnmarshalJSON(data []byte) error {
return nil return nil
} }
// DecodeMsgpack MarketPrice.
//
// BUG(VK): unavailable product, in fave.get return [].
func (m *MarketPrice) DecodeMsgpack(dec *msgpack.Decoder) error {
data, err := dec.DecodeRaw()
if err != nil {
return err
}
if bytes.Equal(data, []byte{msgpcode.FixedArrayLow}) {
return nil
}
type renamedMarketPrice MarketPrice
var r renamedMarketPrice
d := msgpack.NewDecoder(bytes.NewReader(data))
d.SetCustomStructTag("json")
err = d.Decode(&r)
if err != nil {
return err
}
*m = MarketPrice(r)
return nil
}
// MarketSection struct. // MarketSection struct.
type MarketSection struct { type MarketSection struct {
ID int `json:"id"` // Section ID ID int `json:"id"` // Section ID

View File

@@ -0,0 +1,52 @@
package object // import "github.com/SevereCloud/vksdk/v2/object"
import (
"encoding/json"
)
// MarusiaPicture struct.
type MarusiaPicture struct {
ID int `json:"id"`
OwnerID int `json:"owner_id"`
}
// MarusiaPictureUploadResponse struct.
type MarusiaPictureUploadResponse struct {
Hash string `json:"hash"` // Uploading hash
Photo json.RawMessage `json:"photo"` // Uploaded photo data
Server int `json:"server"` // Upload server number
AID int `json:"aid"`
MessageCode int `json:"message_code"`
}
// MarusiaAudio struct.
type MarusiaAudio struct {
ID int `json:"id"`
Title string `json:"title"`
OwnerID int `json:"owner_id"`
}
// MarusiaAudioUploadResponse struct.
type MarusiaAudioUploadResponse struct {
Sha string `json:"sha"`
Secret string `json:"secret"`
Meta MarusiaAudioMeta `json:"meta"`
Hash string `json:"hash"`
Server string `json:"server"`
UserID int `json:"user_id"`
RequestID string `json:"request_id"`
}
// MarusiaAudioMeta struct.
type MarusiaAudioMeta struct {
Album string `json:"album"`
Artist string `json:"artist"`
Bitrate string `json:"bitrate"`
Duration string `json:"duration"`
Genre string `json:"genre"`
Kad string `json:"kad"`
Md5 string `json:"md5"`
Md5DataSize string `json:"md5_data_size"`
Samplerate string `json:"samplerate"`
Title string `json:"title"`
}

View File

@@ -79,6 +79,7 @@ type MessagesMessage struct {
UpdateTime int `json:"update_time"` // Date when the message has been updated in Unixtime UpdateTime int `json:"update_time"` // Date when the message has been updated in Unixtime
MembersCount int `json:"members_count"` // Members number MembersCount int `json:"members_count"` // Members number
ExpireTTL int `json:"expire_ttl"` ExpireTTL int `json:"expire_ttl"`
MessageTag string `json:"message_tag"` // for https://notify.mail.ru/
} }
// MessagesBasePayload struct. // MessagesBasePayload struct.
@@ -375,17 +376,18 @@ type MessagesTemplateElement struct {
// MessagesTemplateElementCarousel struct. // MessagesTemplateElementCarousel struct.
type MessagesTemplateElementCarousel struct { type MessagesTemplateElementCarousel struct {
Title string `json:"title"` Title string `json:"title,omitempty"`
Action MessagesTemplateElementCarouselAction `json:"action"` Action MessagesTemplateElementCarouselAction `json:"action,omitempty"`
Description string `json:"description"` Description string `json:"description,omitempty"`
Photo PhotosPhoto `json:"photo"` Photo *PhotosPhoto `json:"photo,omitempty"` // Only read
Buttons []MessagesKeyboardButton `json:"buttons"` PhotoID string `json:"photo_id,omitempty"` // Only for send
Buttons []MessagesKeyboardButton `json:"buttons,omitempty"`
} }
// MessagesTemplateElementCarouselAction struct. // MessagesTemplateElementCarouselAction struct.
type MessagesTemplateElementCarouselAction struct { type MessagesTemplateElementCarouselAction struct {
Type string `json:"type"` Type string `json:"type"`
Link string `json:"link"` Link string `json:"link,omitempty"`
} }
// MessageContentSourceMessage ... // MessageContentSourceMessage ...
@@ -443,6 +445,7 @@ type MessagesChat struct {
AdminID int `json:"admin_id"` // Chat creator ID AdminID int `json:"admin_id"` // Chat creator ID
ID int `json:"id"` // Chat ID ID int `json:"id"` // Chat ID
IsDefaultPhoto BaseBoolInt `json:"is_default_photo"` IsDefaultPhoto BaseBoolInt `json:"is_default_photo"`
IsGroupChannel BaseBoolInt `json:"is_group_channel"`
Photo100 string `json:"photo_100"` // URL of the preview image with 100 px in width Photo100 string `json:"photo_100"` // URL of the preview image with 100 px in width
Photo200 string `json:"photo_200"` // URL of the preview image with 200 px in width Photo200 string `json:"photo_200"` // URL of the preview image with 200 px in width
Photo50 string `json:"photo_50"` // URL of the preview image with 50 px in width Photo50 string `json:"photo_50"` // URL of the preview image with 50 px in width
@@ -477,6 +480,7 @@ type MessagesChatSettingsPhoto struct {
Photo200 string `json:"photo_200"` Photo200 string `json:"photo_200"`
Photo50 string `json:"photo_50"` Photo50 string `json:"photo_50"`
IsDefaultPhoto BaseBoolInt `json:"is_default_photo"` IsDefaultPhoto BaseBoolInt `json:"is_default_photo"`
IsDefaultCallPhoto bool `json:"is_default_call_photo"`
} }
// MessagesConversation struct. // MessagesConversation struct.
@@ -487,6 +491,9 @@ type MessagesConversation struct {
LastMessageID int `json:"last_message_id"` // ID of the last message in conversation LastMessageID int `json:"last_message_id"` // ID of the last message in conversation
Mentions []int `json:"mentions"` // IDs of messages with mentions Mentions []int `json:"mentions"` // IDs of messages with mentions
MessageRequest string `json:"message_request"` MessageRequest string `json:"message_request"`
LastConversationMessageID int `json:"last_conversation_message_id"`
InReadCMID int `json:"in_read_cmid"`
OutReadCMID int `json:"out_read_cmid"`
// Last outcoming message have been read by the opponent. // Last outcoming message have been read by the opponent.
OutRead int `json:"out_read"` OutRead int `json:"out_read"`
@@ -495,6 +502,10 @@ type MessagesConversation struct {
Important BaseBoolInt `json:"important"` Important BaseBoolInt `json:"important"`
Unanswered BaseBoolInt `json:"unanswered"` Unanswered BaseBoolInt `json:"unanswered"`
IsMarkedUnread BaseBoolInt `json:"is_marked_unread"` IsMarkedUnread BaseBoolInt `json:"is_marked_unread"`
CanSendMoney BaseBoolInt `json:"can_send_money"`
CanReceiveMoney BaseBoolInt `json:"can_receive_money"`
IsNew BaseBoolInt `json:"is_new"`
IsArchived BaseBoolInt `json:"is_archived"`
UnreadCount int `json:"unread_count"` // Unread messages number UnreadCount int `json:"unread_count"` // Unread messages number
CurrentKeyboard MessagesKeyboard `json:"current_keyboard"` CurrentKeyboard MessagesKeyboard `json:"current_keyboard"`
SortID struct { SortID struct {
@@ -530,6 +541,7 @@ type MessagesConversationChatSettings struct {
CanCall BaseBoolInt `json:"can_call"` CanCall BaseBoolInt `json:"can_call"`
CanUseMassMentions BaseBoolInt `json:"can_use_mass_mentions"` CanUseMassMentions BaseBoolInt `json:"can_use_mass_mentions"`
CanChangeServiceType BaseBoolInt `json:"can_change_service_type"` CanChangeServiceType BaseBoolInt `json:"can_change_service_type"`
CanChangeStyle BaseBoolInt `json:"can_change_style"`
} `json:"acl"` } `json:"acl"`
IsGroupChannel BaseBoolInt `json:"is_group_channel"` IsGroupChannel BaseBoolInt `json:"is_group_channel"`
IsDisappearing BaseBoolInt `json:"is_disappearing"` IsDisappearing BaseBoolInt `json:"is_disappearing"`
@@ -559,6 +571,7 @@ type MessagesChatPermissions struct {
SeeInviteLink MessagesChatPermission `json:"see_invite_link"` SeeInviteLink MessagesChatPermission `json:"see_invite_link"`
Call MessagesChatPermission `json:"call"` Call MessagesChatPermission `json:"call"`
ChangeAdmins MessagesChatPermission `json:"change_admins"` ChangeAdmins MessagesChatPermission `json:"change_admins"`
ChangeStyle MessagesChatPermission `json:"change_style"`
} }
// MessagesConversationPeer struct. // MessagesConversationPeer struct.
@@ -573,6 +586,8 @@ type MessagesConversationPushSettings struct {
DisabledUntil int `json:"disabled_until"` DisabledUntil int `json:"disabled_until"`
DisabledForever BaseBoolInt `json:"disabled_forever"` DisabledForever BaseBoolInt `json:"disabled_forever"`
NoSound BaseBoolInt `json:"no_sound"` NoSound BaseBoolInt `json:"no_sound"`
DisabledMentions BaseBoolInt `json:"disabled_mentions"`
DisabledMassMentions BaseBoolInt `json:"disabled_mass_mentions"`
} }
// MessagesConversationWithMessage struct. // MessagesConversationWithMessage struct.

View File

@@ -1,7 +1,5 @@
package object // import "github.com/SevereCloud/vksdk/v2/object" package object // import "github.com/SevereCloud/vksdk/v2/object"
import "encoding/json"
// NotificationsFeedback struct. // NotificationsFeedback struct.
type NotificationsFeedback struct { type NotificationsFeedback struct {
Attachments []WallWallpostAttachment `json:"attachments"` Attachments []WallWallpostAttachment `json:"attachments"`
@@ -16,8 +14,8 @@ type NotificationsFeedback struct {
// NotificationsNotification struct. // NotificationsNotification struct.
type NotificationsNotification struct { type NotificationsNotification struct {
Date int `json:"date"` // Date when the event has been occurred Date int `json:"date"` // Date when the event has been occurred
Feedback json.RawMessage `json:"feedback"` Feedback RawMessage `json:"feedback"`
Parent json.RawMessage `json:"parent"` Parent RawMessage `json:"parent"`
Reply NotificationsReply `json:"reply"` Reply NotificationsReply `json:"reply"`
Type string `json:"type"` // Notification type Type string `json:"type"` // Notification type
} }

View File

@@ -9,6 +9,8 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"reflect" "reflect"
"github.com/vmihailenco/msgpack/v5"
) )
// Attachment interface. // Attachment interface.
@@ -42,6 +44,44 @@ func (b *BaseBoolInt) UnmarshalJSON(data []byte) (err error) {
return return
} }
// DecodeMsgpack func.
func (b *BaseBoolInt) DecodeMsgpack(dec *msgpack.Decoder) (err error) {
data, err := dec.DecodeRaw()
if err != nil {
return err
}
var (
valueInt int
valueBool bool
)
switch {
case msgpack.Unmarshal(data, &valueBool) == nil:
*b = BaseBoolInt(valueBool)
case msgpack.Unmarshal(data, &valueInt) == nil:
if valueInt == 1 {
*b = true
break
}
if valueInt == 0 {
*b = false
break
}
fallthrough
default:
// return msgpack error
err = &json.UnmarshalTypeError{
Value: string(data),
Type: reflect.TypeOf((*BaseBoolInt)(nil)),
}
}
return err
}
// BaseCountry struct. // BaseCountry struct.
type BaseCountry struct { type BaseCountry struct {
ID int `json:"id"` ID int `json:"id"`
@@ -151,6 +191,33 @@ func (obj *BaseImage) UnmarshalJSON(data []byte) (err error) {
return err return err
} }
// DecodeMsgpack is required to support images with `src` field.
func (obj *BaseImage) DecodeMsgpack(dec *msgpack.Decoder) (err error) {
type renamedBaseImage struct {
Height float64 `msgpack:"height"`
URL string `msgpack:"url"`
Src string `msgpack:"src"`
Width float64 `msgpack:"width"`
Type string `msgpack:"type"`
}
var renamedObj renamedBaseImage
err = dec.Decode(&renamedObj)
obj.Height = renamedObj.Height
obj.Width = renamedObj.Width
obj.Type = renamedObj.Type
if renamedObj.Src == "" {
obj.URL = renamedObj.URL
} else {
obj.URL = renamedObj.Src
}
return err
}
// BaseLikes struct. // BaseLikes struct.
type BaseLikes struct { type BaseLikes struct {
UserLikes BaseBoolInt `json:"user_likes"` // Information whether current user likes UserLikes BaseBoolInt `json:"user_likes"` // Information whether current user likes
@@ -347,8 +414,10 @@ type Privacy struct {
Category PrivacyCategory `json:"category,omitempty"` Category PrivacyCategory `json:"category,omitempty"`
Lists struct { Lists struct {
Allowed []int `json:"allowed"` Allowed []int `json:"allowed"`
Excluded []int `json:"excluded"`
} `json:"lists,omitempty"` } `json:"lists,omitempty"`
Owners struct { Owners struct {
Allowed []int `json:"allowed"`
Excluded []int `json:"excluded"` Excluded []int `json:"excluded"`
} `json:"owners,omitempty"` } `json:"owners,omitempty"`
} }

View File

@@ -239,6 +239,7 @@ type PhotosPhotoFull struct {
Photo1280 string `json:"photo_1280"` // URL of image with 1280 px width Photo1280 string `json:"photo_1280"` // URL of image with 1280 px width
Photo2560 string `json:"photo_2560"` // URL of image with 2560 px width Photo2560 string `json:"photo_2560"` // URL of image with 2560 px width
Sizes []PhotosPhotoSizes `json:"sizes"` Sizes []PhotosPhotoSizes `json:"sizes"`
OrigPhoto PhotosPhotoSizes `json:"orig_photo"`
} }
// ToAttachment return attachment format. // ToAttachment return attachment format.

39
vendor/github.com/SevereCloud/vksdk/v2/object/raw.go generated vendored Normal file
View File

@@ -0,0 +1,39 @@
package object // import "github.com/SevereCloud/vksdk/v2/object"
import "github.com/vmihailenco/msgpack/v5"
// RawMessage is a raw encoded JSON or MessagePack value.
type RawMessage []byte
// MarshalJSON returns m as the JSON encoding of m.
func (m RawMessage) MarshalJSON() ([]byte, error) {
if m == nil {
return []byte("null"), nil
}
return m, nil
}
// UnmarshalJSON sets *m to a copy of data.
func (m *RawMessage) UnmarshalJSON(data []byte) error {
*m = append((*m)[0:0], data...)
return nil
}
// EncodeMsgpack write m as the MessagePack encoding of m.
func (m RawMessage) EncodeMsgpack(enc *msgpack.Encoder) error {
_, err := enc.Writer().Write(m)
return err
}
// DecodeMsgpack sets *m to a copy of data.
func (m *RawMessage) DecodeMsgpack(dec *msgpack.Decoder) error {
msg, err := dec.DecodeRaw()
if err != nil {
return err
}
*m = RawMessage(msg)
return nil
}

View File

@@ -127,6 +127,7 @@ type StoriesStory struct {
Seen BaseBoolInt `json:"seen"` Seen BaseBoolInt `json:"seen"`
IsOwnerPinned BaseBoolInt `json:"is_owner_pinned"` IsOwnerPinned BaseBoolInt `json:"is_owner_pinned"`
IsOneTime BaseBoolInt `json:"is_one_time"` IsOneTime BaseBoolInt `json:"is_one_time"`
IsAdvice BaseBoolInt `json:"is_advice,omitempty"`
NeedMute BaseBoolInt `json:"need_mute"` NeedMute BaseBoolInt `json:"need_mute"`
MuteReply BaseBoolInt `json:"mute_reply"` MuteReply BaseBoolInt `json:"mute_reply"`
CanLike BaseBoolInt `json:"can_like"` CanLike BaseBoolInt `json:"can_like"`
@@ -152,6 +153,7 @@ type StoriesStory struct {
NarrativesCount int `json:"narratives_count"` NarrativesCount int `json:"narratives_count"`
FirstNarrativeTitle string `json:"first_narrative_title"` FirstNarrativeTitle string `json:"first_narrative_title"`
Questions StoriesQuestions `json:"questions"` Questions StoriesQuestions `json:"questions"`
ReactionSetID string `json:"reaction_set_id"`
} }
// StoriesFeedItemType type. // StoriesFeedItemType type.
@@ -251,8 +253,10 @@ type StoriesClickableSticker struct { // nolint: maligned
StickerID int `json:"sticker_id,omitempty"` StickerID int `json:"sticker_id,omitempty"`
StickerPackID int `json:"sticker_pack_id,omitempty"` StickerPackID int `json:"sticker_pack_id,omitempty"`
// type=place // type=place or geo
PlaceID int `json:"place_id,omitempty"` PlaceID int `json:"place_id,omitempty"`
// Title
CategoryID int `json:"category_id,omitempty"`
// type=question // type=question
Question string `json:"question,omitempty"` Question string `json:"question,omitempty"`
@@ -269,6 +273,12 @@ type StoriesClickableSticker struct { // nolint: maligned
// type=link // type=link
LinkObject BaseLink `json:"link_object,omitempty"` LinkObject BaseLink `json:"link_object,omitempty"`
TooltipText string `json:"tooltip_text,omitempty"` TooltipText string `json:"tooltip_text,omitempty"`
TooltipTextKey string `json:"tooltip_text_key,omitempty"`
// type=time
TimestampMs int64 `json:"timestamp_ms,omitempty"`
Date string `json:"date,omitempty"`
Title string `json:"title,omitempty"`
// type=market_item // type=market_item
Subtype string `json:"subtype,omitempty"` Subtype string `json:"subtype,omitempty"`
@@ -290,10 +300,19 @@ type StoriesClickableSticker struct { // nolint: maligned
AudioStartTime int `json:"audio_start_time,omitempty"` AudioStartTime int `json:"audio_start_time,omitempty"`
// type=app // type=app
App AppsApp `json:"app"` App AppsApp `json:"app,omitempty"`
AppContext string `json:"app_context"` AppContext string `json:"app_context,omitempty"`
HasNewInteractions BaseBoolInt `json:"has_new_interactions"` HasNewInteractions BaseBoolInt `json:"has_new_interactions,omitempty"`
IsBroadcastNotifyAllowed BaseBoolInt `json:"is_broadcast_notify_allowed"` IsBroadcastNotifyAllowed BaseBoolInt `json:"is_broadcast_notify_allowed,omitempty"`
// type=emoji
Emoji string `json:"emoji,omitempty"`
// type=text
Text string `json:"text,omitempty"`
BackgroundStyle string `json:"background_style,omitempty"`
Alignment string `json:"alignment,omitempty"`
SelectionColor string `json:"selection_color,omitempty"`
} }
// TODO: сделать несколько структур для кликабельного стикера // TODO: сделать несколько структур для кликабельного стикера
@@ -313,6 +332,10 @@ const (
ClickableStickerPoll = "poll" ClickableStickerPoll = "poll"
ClickableStickerMusic = "music" ClickableStickerMusic = "music"
ClickableStickerApp = "app" ClickableStickerApp = "app"
ClickableStickerTime = "time"
ClickableStickerEmoji = "emoji"
ClickableStickerGeo = "geo"
ClickableStickerText = "text"
) )
// Subtype of clickable sticker. // Subtype of clickable sticker.

View File

@@ -4,6 +4,9 @@ import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/vmihailenco/msgpack/v5"
"github.com/vmihailenco/msgpack/v5/msgpcode"
) )
// User relationship status. // User relationship status.
@@ -258,6 +261,36 @@ func (personal *UsersPersonal) UnmarshalJSON(data []byte) error {
return nil return nil
} }
// DecodeMsgpack UsersPersonal.
//
// BUG(VK): UsersPersonal return [].
func (personal *UsersPersonal) DecodeMsgpack(dec *msgpack.Decoder) error {
data, err := dec.DecodeRaw()
if err != nil {
return err
}
if bytes.Equal(data, []byte{msgpcode.FixedArrayLow}) {
return nil
}
type renamedUsersPersonal UsersPersonal
var r renamedUsersPersonal
d := msgpack.NewDecoder(bytes.NewReader(data))
d.SetCustomStructTag("json")
err = d.Decode(&r)
if err != nil {
return err
}
*personal = UsersPersonal(r)
return nil
}
// UsersRelative struct. // UsersRelative struct.
type UsersRelative struct { type UsersRelative struct {
BirthDate string `json:"birth_date"` // Date of child birthday (format dd.mm.yyyy) BirthDate string `json:"birth_date"` // Date of child birthday (format dd.mm.yyyy)

View File

@@ -1,5 +1,13 @@
package object // import "github.com/SevereCloud/vksdk/v2/object" package object // import "github.com/SevereCloud/vksdk/v2/object"
import (
"bytes"
"encoding/json"
"github.com/vmihailenco/msgpack/v5"
"github.com/vmihailenco/msgpack/v5/msgpcode"
)
// UtilsDomainResolvedType object type. // UtilsDomainResolvedType object type.
const ( const (
UtilsDomainResolvedTypeUser = "user" UtilsDomainResolvedTypeUser = "user"
@@ -15,6 +23,58 @@ type UtilsDomainResolved struct {
Type string `json:"type"` Type string `json:"type"`
} }
// UnmarshalJSON UtilsDomainResolved.
//
// BUG(VK): UtilsDomainResolved return [].
func (link *UtilsDomainResolved) UnmarshalJSON(data []byte) error {
if bytes.Equal(data, []byte("[]")) {
return nil
}
type renamedUtilsDomainResolved UtilsDomainResolved
var r renamedUtilsDomainResolved
err := json.Unmarshal(data, &r)
if err != nil {
return err
}
*link = UtilsDomainResolved(r)
return nil
}
// DecodeMsgpack UtilsDomainResolved.
//
// BUG(VK): UtilsDomainResolved return [].
func (link *UtilsDomainResolved) DecodeMsgpack(dec *msgpack.Decoder) error {
data, err := dec.DecodeRaw()
if err != nil {
return err
}
if bytes.Equal(data, []byte{msgpcode.FixedArrayLow}) {
return nil
}
type renamedUtilsDomainResolved UtilsDomainResolved
var r renamedUtilsDomainResolved
d := msgpack.NewDecoder(bytes.NewReader(data))
d.SetCustomStructTag("json")
err = d.Decode(&r)
if err != nil {
return err
}
*link = UtilsDomainResolved(r)
return nil
}
// UtilsLastShortenedLink struct. // UtilsLastShortenedLink struct.
type UtilsLastShortenedLink struct { type UtilsLastShortenedLink struct {
AccessKey string `json:"access_key"` // Access key for private stats AccessKey string `json:"access_key"` // Access key for private stats

View File

@@ -12,6 +12,9 @@ type VideoVideo struct {
// Date when the video has been added in Unixtime. // Date when the video has been added in Unixtime.
AddingDate int `json:"adding_date"` AddingDate int `json:"adding_date"`
// Date when the video has been released in Unixtime.
ReleaseDate int `json:"release_date"`
// Information whether current user can add the video. // Information whether current user can add the video.
CanAdd BaseBoolInt `json:"can_add"` CanAdd BaseBoolInt `json:"can_add"`
@@ -27,12 +30,17 @@ type VideoVideo struct {
// Information whether current user can like the video. // Information whether current user can like the video.
CanLike BaseBoolInt `json:"can_like"` CanLike BaseBoolInt `json:"can_like"`
// Information whether current user can download the video.
CanDownload BaseBoolInt `json:"can_download"`
// Information whether current user can repost this video. // Information whether current user can repost this video.
CanRepost BaseBoolInt `json:"can_repost"` CanRepost BaseBoolInt `json:"can_repost"`
CanSubscribe BaseBoolInt `json:"can_subscribe"` CanSubscribe BaseBoolInt `json:"can_subscribe"`
CanAttachLink BaseBoolInt `json:"can_attach_link"` CanAttachLink BaseBoolInt `json:"can_attach_link"`
IsFavorite BaseBoolInt `json:"is_favorite"` IsFavorite BaseBoolInt `json:"is_favorite"`
IsPrivate BaseBoolInt `json:"is_private"` IsPrivate BaseBoolInt `json:"is_private"`
IsExplicit BaseBoolInt `json:"is_explicit"`
IsSubscribed BaseBoolInt `json:"is_subscribed"`
Added BaseBoolInt `json:"added"` Added BaseBoolInt `json:"added"`
Repeat BaseBoolInt `json:"repeat"` // Information whether the video is repeated Repeat BaseBoolInt `json:"repeat"` // Information whether the video is repeated
ContentRestricted int `json:"content_restricted"` ContentRestricted int `json:"content_restricted"`
@@ -43,6 +51,7 @@ type VideoVideo struct {
Description string `json:"description"` // Video description Description string `json:"description"` // Video description
Duration int `json:"duration"` // Video duration in seconds Duration int `json:"duration"` // Video duration in seconds
Files VideoVideoFiles `json:"files"` Files VideoVideoFiles `json:"files"`
Trailer VideoVideoFiles `json:"trailer,omitempty"`
FirstFrame []VideoVideoImage `json:"first_frame"` FirstFrame []VideoVideoImage `json:"first_frame"`
Image []VideoVideoImage `json:"image"` Image []VideoVideoImage `json:"image"`
Height int `json:"height"` // Video height Height int `json:"height"` // Video height
@@ -59,6 +68,7 @@ type VideoVideo struct {
Player string `json:"player"` Player string `json:"player"`
Processing int `json:"processing"` // Returns if the video is processing Processing int `json:"processing"` // Returns if the video is processing
Title string `json:"title"` // Video title Title string `json:"title"` // Video title
Subtitle string `json:"subtitle"` // Video subtitle
Type string `json:"type"` Type string `json:"type"`
Views int `json:"views"` // Number of views Views int `json:"views"` // Number of views
Width int `json:"width"` // Video width Width int `json:"width"` // Video width
@@ -72,6 +82,10 @@ type VideoVideo struct {
ActionButton VideoActionButton `json:"action_button"` ActionButton VideoActionButton `json:"action_button"`
Restriction VideoRestriction `json:"restriction"` Restriction VideoRestriction `json:"restriction"`
ContentRestrictedMessage string `json:"content_restricted_message"` ContentRestrictedMessage string `json:"content_restricted_message"`
MainArtists []AudioAudioArtist `json:"main_artists"`
FeaturedArtists []AudioAudioArtist `json:"featured_artists"`
Genres []BaseObjectWithName `json:"genres"`
OvID string `json:"ov_id,omitempty"`
} }
// ToAttachment return attachment format. // ToAttachment return attachment format.
@@ -112,16 +126,20 @@ type VideoSnippet struct {
// VideoVideoFiles struct. // VideoVideoFiles struct.
type VideoVideoFiles struct { type VideoVideoFiles struct {
External string `json:"external"` // URL of the external player External string `json:"external,omitempty"` // URL of the external player
Mp4_1080 string `json:"mp4_1080"` // URL of the mpeg4 file with 1080p quality Mp4_1080 string `json:"mp4_1080,omitempty"` // URL of the mpeg4 file with 1080p quality
Mp4_1440 string `json:"mp4_1440"` // URL of the mpeg4 file with 2k quality Mp4_1440 string `json:"mp4_1440,omitempty"` // URL of the mpeg4 file with 2k quality
Mp4_2160 string `json:"mp4_2160"` // URL of the mpeg4 file with 4k quality Mp4_2160 string `json:"mp4_2160,omitempty"` // URL of the mpeg4 file with 4k quality
Mp4_240 string `json:"mp4_240"` // URL of the mpeg4 file with 240p quality Mp4_240 string `json:"mp4_240,omitempty"` // URL of the mpeg4 file with 240p quality
Mp4_360 string `json:"mp4_360"` // URL of the mpeg4 file with 360p quality Mp4_360 string `json:"mp4_360,omitempty"` // URL of the mpeg4 file with 360p quality
Mp4_480 string `json:"mp4_480"` // URL of the mpeg4 file with 480p quality Mp4_480 string `json:"mp4_480,omitempty"` // URL of the mpeg4 file with 480p quality
Mp4_720 string `json:"mp4_720"` // URL of the mpeg4 file with 720p quality Mp4_720 string `json:"mp4_720,omitempty"` // URL of the mpeg4 file with 720p quality
Live string `json:"live"` Live string `json:"live,omitempty"`
HLS string `json:"hls"` HLS string `json:"hls,omitempty"`
DashUni string `json:"dash_uni,omitempty"`
DashSep string `json:"dash_sep,omitempty"`
DashWebm string `json:"dash_webm,omitempty"`
FailoverHost string `json:"failover_host,omitempty"`
} }
// VideoCatBlock struct. // VideoCatBlock struct.
@@ -213,6 +231,7 @@ type VideoVideoFull struct {
Description string `json:"description"` // Video description Description string `json:"description"` // Video description
Duration int `json:"duration"` // Video duration in seconds Duration int `json:"duration"` // Video duration in seconds
Files VideoVideoFiles `json:"files"` Files VideoVideoFiles `json:"files"`
Trailer VideoVideoFiles `json:"trailer"`
ID int `json:"id"` // Video ID ID int `json:"id"` // Video ID
Likes BaseLikes `json:"likes"` Likes BaseLikes `json:"likes"`
Live int `json:"live"` // Returns if the video is live translation Live int `json:"live"` // Returns if the video is live translation

View File

@@ -156,14 +156,17 @@ type WallWallpost struct {
IsPinned BaseBoolInt `json:"is_pinned"` IsPinned BaseBoolInt `json:"is_pinned"`
IsFavorite BaseBoolInt `json:"is_favorite"` // Information whether the post in favorites list IsFavorite BaseBoolInt `json:"is_favorite"` // Information whether the post in favorites list
IsArchived BaseBoolInt `json:"is_archived"` // Is post archived, only for post owners IsArchived BaseBoolInt `json:"is_archived"` // Is post archived, only for post owners
IsDeleted BaseBoolInt `json:"is_deleted"`
MarkedAsAds BaseBoolInt `json:"marked_as_ads"` MarkedAsAds BaseBoolInt `json:"marked_as_ads"`
Edited int `json:"edited"` // Date of editing in Unixtime Edited int `json:"edited"` // Date of editing in Unixtime
Copyright WallPostCopyright `json:"copyright"` Copyright WallPostCopyright `json:"copyright"`
PostID int `json:"post_id"` PostID int `json:"post_id"`
ParentsStack []int `json:"parents_stack"` ParentsStack []int `json:"parents_stack"`
Donut WallWallpostDonut `json:"donut"` // need api v5.125 Donut WallWallpostDonut `json:"donut"`
ShortTextRate float64 `json:"short_text_rate"` ShortTextRate float64 `json:"short_text_rate"`
CarouselOffset int `json:"carousel_offset"` CarouselOffset int `json:"carousel_offset"`
Header WallWallpostHeader `json:"header"`
Hash string `json:"hash"`
} }
// Attachment type. // Attachment type.
@@ -235,8 +238,10 @@ type WallWallpostToID struct {
IsFavorite BaseBoolInt `json:"is_favorite"` // Information whether the post in favorites list IsFavorite BaseBoolInt `json:"is_favorite"` // Information whether the post in favorites list
MarkedAsAds BaseBoolInt `json:"marked_as_ads"` MarkedAsAds BaseBoolInt `json:"marked_as_ads"`
ParentsStack []int `json:"parents_stack"` ParentsStack []int `json:"parents_stack"`
Donut WallWallpostDonut `json:"donut"` // need api v5.125 Donut WallWallpostDonut `json:"donut"`
ShortTextRate float64 `json:"short_text_rate"` ShortTextRate float64 `json:"short_text_rate"`
Views WallViews `json:"views"` // Count of views
Header WallWallpostHeader `json:"header"`
} }
// WallWallpostDonut info about VK Donut. // WallWallpostDonut info about VK Donut.
@@ -255,3 +260,15 @@ type WallPostCopyright struct {
Type string `json:"type"` Type string `json:"type"`
Name string `json:"name"` Name string `json:"name"`
} }
// WallWallpostHeader struct.
type WallWallpostHeader struct {
Type string `json:"type"`
CustomDescription WallWallpostHeaderCustomDescription `json:"custom_description"`
}
// WallWallpostHeaderCustomDescription struct.
type WallWallpostHeaderCustomDescription struct {
SourceID int `json:"source_id"`
Date int `json:"date"`
}

View File

@@ -45,6 +45,9 @@ type WidgetsWidgetComment struct {
Views struct { Views struct {
Count int `json:"count"` Count int `json:"count"`
} `json:"views"` } `json:"views"`
Donut WallWallpostDonut `json:"donut"`
ShortTextRate float64 `json:"short_text_rate"`
Header WallWallpostHeader `json:"header"`
} }
// WidgetsWidgetLikes struct. // WidgetsWidgetLikes struct.

View File

@@ -1,7 +1,3 @@
<p align="center">
<img src="https://raw.githubusercontent.com/d5/tengolang-share/master/logo_400.png" width="200" height="200">
</p>
# The Tengo Language # The Tengo Language
[![GoDoc](https://godoc.org/github.com/d5/tengo/v2?status.svg)](https://godoc.org/github.com/d5/tengo/v2) [![GoDoc](https://godoc.org/github.com/d5/tengo/v2?status.svg)](https://godoc.org/github.com/d5/tengo/v2)

View File

@@ -1,9 +1,11 @@
package tengo package tengo
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"strings" "strings"
@@ -45,11 +47,12 @@ type Compiler struct {
parent *Compiler parent *Compiler
modulePath string modulePath string
importDir string importDir string
importFileExt []string
constants []Object constants []Object
symbolTable *SymbolTable symbolTable *SymbolTable
scopes []compilationScope scopes []compilationScope
scopeIndex int scopeIndex int
modules *ModuleMap modules ModuleGetter
compiledModules map[string]*CompiledFunction compiledModules map[string]*CompiledFunction
allowFileImport bool allowFileImport bool
loops []*loop loops []*loop
@@ -63,7 +66,7 @@ func NewCompiler(
file *parser.SourceFile, file *parser.SourceFile,
symbolTable *SymbolTable, symbolTable *SymbolTable,
constants []Object, constants []Object,
modules *ModuleMap, modules ModuleGetter,
trace io.Writer, trace io.Writer,
) *Compiler { ) *Compiler {
mainScope := compilationScope{ mainScope := compilationScope{
@@ -96,6 +99,7 @@ func NewCompiler(
trace: trace, trace: trace,
modules: modules, modules: modules,
compiledModules: make(map[string]*CompiledFunction), compiledModules: make(map[string]*CompiledFunction),
importFileExt: []string{SourceFileExtDefault},
} }
} }
@@ -538,12 +542,8 @@ func (c *Compiler) Compile(node parser.Node) error {
} }
} else if c.allowFileImport { } else if c.allowFileImport {
moduleName := node.ModuleName moduleName := node.ModuleName
if !strings.HasSuffix(moduleName, ".tengo") {
moduleName += ".tengo"
}
modulePath, err := filepath.Abs( modulePath, err := c.getPathModule(moduleName)
filepath.Join(c.importDir, moduleName))
if err != nil { if err != nil {
return c.errorf(node, "module file path error: %s", return c.errorf(node, "module file path error: %s",
err.Error()) err.Error())
@@ -640,6 +640,39 @@ func (c *Compiler) SetImportDir(dir string) {
c.importDir = dir c.importDir = dir
} }
// SetImportFileExt sets the extension name of the source file for loading
// local module files.
//
// Use this method if you want other source file extension than ".tengo".
//
// // this will search for *.tengo, *.foo, *.bar
// err := c.SetImportFileExt(".tengo", ".foo", ".bar")
//
// This function requires at least one argument, since it will replace the
// current list of extension name.
func (c *Compiler) SetImportFileExt(exts ...string) error {
if len(exts) == 0 {
return fmt.Errorf("missing arg: at least one argument is required")
}
for _, ext := range exts {
if ext != filepath.Ext(ext) || ext == "" {
return fmt.Errorf("invalid file extension: %s", ext)
}
}
c.importFileExt = exts // Replace the hole current extension list
return nil
}
// GetImportFileExt returns the current list of extension name.
// Thease are the complementary suffix of the source file to search and load
// local module files.
func (c *Compiler) GetImportFileExt() []string {
return c.importFileExt
}
func (c *Compiler) compileAssign( func (c *Compiler) compileAssign(
node parser.Node, node parser.Node,
lhs, rhs []parser.Expr, lhs, rhs []parser.Expr,
@@ -1098,6 +1131,7 @@ func (c *Compiler) fork(
child.parent = c // parent to set to current compiler child.parent = c // parent to set to current compiler
child.allowFileImport = c.allowFileImport child.allowFileImport = c.allowFileImport
child.importDir = c.importDir child.importDir = c.importDir
child.importFileExt = c.importFileExt
if isFile && c.importDir != "" { if isFile && c.importDir != "" {
child.importDir = filepath.Dir(modulePath) child.importDir = filepath.Dir(modulePath)
} }
@@ -1287,6 +1321,28 @@ func (c *Compiler) printTrace(a ...interface{}) {
_, _ = fmt.Fprintln(c.trace, a...) _, _ = fmt.Fprintln(c.trace, a...)
} }
func (c *Compiler) getPathModule(moduleName string) (pathFile string, err error) {
for _, ext := range c.importFileExt {
nameFile := moduleName
if !strings.HasSuffix(nameFile, ext) {
nameFile += ext
}
pathFile, err = filepath.Abs(filepath.Join(c.importDir, nameFile))
if err != nil {
continue
}
// Check if file exists
if _, err := os.Stat(pathFile); !errors.Is(err, os.ErrNotExist) {
return pathFile, nil
}
}
return "", fmt.Errorf("module '%s' not found at: %s", moduleName, pathFile)
}
func resolveAssignLHS( func resolveAssignLHS(
expr parser.Expr, expr parser.Expr,
) (name string, selectors []parser.Expr) { ) (name string, selectors []parser.Expr) {

View File

@@ -6,6 +6,11 @@ type Importable interface {
Import(moduleName string) (interface{}, error) Import(moduleName string) (interface{}, error)
} }
// ModuleGetter enables implementing dynamic module loading.
type ModuleGetter interface {
Get(name string) Importable
}
// ModuleMap represents a set of named modules. Use NewModuleMap to create a // ModuleMap represents a set of named modules. Use NewModuleMap to create a
// new module map. // new module map.
type ModuleMap struct { type ModuleMap struct {

View File

@@ -12,7 +12,7 @@ import (
// Script can simplify compilation and execution of embedded scripts. // Script can simplify compilation and execution of embedded scripts.
type Script struct { type Script struct {
variables map[string]*Variable variables map[string]*Variable
modules *ModuleMap modules ModuleGetter
input []byte input []byte
maxAllocs int64 maxAllocs int64
maxConstObjects int maxConstObjects int
@@ -54,7 +54,7 @@ func (s *Script) Remove(name string) bool {
} }
// SetImports sets import modules. // SetImports sets import modules.
func (s *Script) SetImports(modules *ModuleMap) { func (s *Script) SetImports(modules ModuleGetter) {
s.modules = modules s.modules = modules
} }
@@ -219,6 +219,18 @@ func (c *Compiled) RunContext(ctx context.Context) (err error) {
v := NewVM(c.bytecode, c.globals, c.maxAllocs) v := NewVM(c.bytecode, c.globals, c.maxAllocs)
ch := make(chan error, 1) ch := make(chan error, 1)
go func() { go func() {
defer func() {
if r := recover(); r != nil {
switch e := r.(type) {
case string:
ch <- fmt.Errorf(e)
case error:
ch <- e
default:
ch <- fmt.Errorf("unknown panic: %v", e)
}
}
}()
ch <- v.Run() ch <- v.Run()
}() }()

View File

@@ -26,6 +26,9 @@ const (
// MaxFrames is the maximum number of function frames for a VM. // MaxFrames is the maximum number of function frames for a VM.
MaxFrames = 1024 MaxFrames = 1024
// SourceFileExtDefault is the default extension for source files.
SourceFileExtDefault = ".tengo"
) )
// CallableFunc is a function signature for the callable functions. // CallableFunc is a function signature for the callable functions.

View File

@@ -293,7 +293,7 @@ func (v *VM) run() {
case parser.OpMap: case parser.OpMap:
v.ip += 2 v.ip += 2
numElements := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8 numElements := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8
kv := make(map[string]Object) kv := make(map[string]Object, numElements)
for i := v.sp - numElements; i < v.sp; i += 2 { for i := v.sp - numElements; i < v.sp; i += 2 {
key := v.stack[i] key := v.stack[i]
value := v.stack[i+1] value := v.stack[i+1]

View File

@@ -1,8 +0,0 @@
language: go
go:
- '1.10'
- '1.11'
- '1.12'
- '1.13'
- tip

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,990 +0,0 @@
package tgbotapi
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strings"
"time"
)
// APIResponse is a response from the Telegram API with the result
// stored raw.
type APIResponse struct {
Ok bool `json:"ok"`
Result json.RawMessage `json:"result"`
ErrorCode int `json:"error_code"`
Description string `json:"description"`
Parameters *ResponseParameters `json:"parameters"`
}
// ResponseParameters are various errors that can be returned in APIResponse.
type ResponseParameters struct {
MigrateToChatID int64 `json:"migrate_to_chat_id"` // optional
RetryAfter int `json:"retry_after"` // optional
}
// Update is an update response, from GetUpdates.
type Update struct {
UpdateID int `json:"update_id"`
Message *Message `json:"message"`
EditedMessage *Message `json:"edited_message"`
ChannelPost *Message `json:"channel_post"`
EditedChannelPost *Message `json:"edited_channel_post"`
InlineQuery *InlineQuery `json:"inline_query"`
ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result"`
CallbackQuery *CallbackQuery `json:"callback_query"`
ShippingQuery *ShippingQuery `json:"shipping_query"`
PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query"`
}
// UpdatesChannel is the channel for getting updates.
type UpdatesChannel <-chan Update
// Clear discards all unprocessed incoming updates.
func (ch UpdatesChannel) Clear() {
for len(ch) != 0 {
<-ch
}
}
// User is a user on Telegram.
type User struct {
ID int `json:"id"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"` // optional
UserName string `json:"username"` // optional
LanguageCode string `json:"language_code"` // optional
IsBot bool `json:"is_bot"` // optional
}
// String displays a simple text version of a user.
//
// It is normally a user's username, but falls back to a first/last
// name as available.
func (u *User) String() string {
if u == nil {
return ""
}
if u.UserName != "" {
return u.UserName
}
name := u.FirstName
if u.LastName != "" {
name += " " + u.LastName
}
return name
}
// GroupChat is a group chat.
type GroupChat struct {
ID int `json:"id"`
Title string `json:"title"`
}
// ChatPhoto represents a chat photo.
type ChatPhoto struct {
SmallFileID string `json:"small_file_id"`
BigFileID string `json:"big_file_id"`
}
// Chat contains information about the place a message was sent.
type Chat struct {
ID int64 `json:"id"`
Type string `json:"type"`
Title string `json:"title"` // optional
UserName string `json:"username"` // optional
FirstName string `json:"first_name"` // optional
LastName string `json:"last_name"` // optional
AllMembersAreAdmins bool `json:"all_members_are_administrators"` // optional
Photo *ChatPhoto `json:"photo"`
Description string `json:"description,omitempty"` // optional
InviteLink string `json:"invite_link,omitempty"` // optional
PinnedMessage *Message `json:"pinned_message"` // optional
}
// IsPrivate returns if the Chat is a private conversation.
func (c Chat) IsPrivate() bool {
return c.Type == "private"
}
// IsGroup returns if the Chat is a group.
func (c Chat) IsGroup() bool {
return c.Type == "group"
}
// IsSuperGroup returns if the Chat is a supergroup.
func (c Chat) IsSuperGroup() bool {
return c.Type == "supergroup"
}
// IsChannel returns if the Chat is a channel.
func (c Chat) IsChannel() bool {
return c.Type == "channel"
}
// ChatConfig returns a ChatConfig struct for chat related methods.
func (c Chat) ChatConfig() ChatConfig {
return ChatConfig{ChatID: c.ID}
}
// Message is returned by almost every request, and contains data about
// almost anything.
type Message struct {
MessageID int `json:"message_id"`
From *User `json:"from"` // optional
Date int `json:"date"`
Chat *Chat `json:"chat"`
ForwardFrom *User `json:"forward_from"` // optional
ForwardFromChat *Chat `json:"forward_from_chat"` // optional
ForwardFromMessageID int `json:"forward_from_message_id"` // optional
ForwardDate int `json:"forward_date"` // optional
ReplyToMessage *Message `json:"reply_to_message"` // optional
EditDate int `json:"edit_date"` // optional
Text string `json:"text"` // optional
Entities *[]MessageEntity `json:"entities"` // optional
CaptionEntities *[]MessageEntity `json:"caption_entities"` // optional
Audio *Audio `json:"audio"` // optional
Document *Document `json:"document"` // optional
Animation *ChatAnimation `json:"animation"` // optional
Game *Game `json:"game"` // optional
Photo *[]PhotoSize `json:"photo"` // optional
Sticker *Sticker `json:"sticker"` // optional
Video *Video `json:"video"` // optional
VideoNote *VideoNote `json:"video_note"` // optional
Voice *Voice `json:"voice"` // optional
Caption string `json:"caption"` // optional
Contact *Contact `json:"contact"` // optional
Location *Location `json:"location"` // optional
Venue *Venue `json:"venue"` // optional
NewChatMembers *[]User `json:"new_chat_members"` // optional
LeftChatMember *User `json:"left_chat_member"` // optional
NewChatTitle string `json:"new_chat_title"` // optional
NewChatPhoto *[]PhotoSize `json:"new_chat_photo"` // optional
DeleteChatPhoto bool `json:"delete_chat_photo"` // optional
GroupChatCreated bool `json:"group_chat_created"` // optional
SuperGroupChatCreated bool `json:"supergroup_chat_created"` // optional
ChannelChatCreated bool `json:"channel_chat_created"` // optional
MigrateToChatID int64 `json:"migrate_to_chat_id"` // optional
MigrateFromChatID int64 `json:"migrate_from_chat_id"` // optional
PinnedMessage *Message `json:"pinned_message"` // optional
Invoice *Invoice `json:"invoice"` // optional
SuccessfulPayment *SuccessfulPayment `json:"successful_payment"` // optional
PassportData *PassportData `json:"passport_data,omitempty"` // optional
}
// Time converts the message timestamp into a Time.
func (m *Message) Time() time.Time {
return time.Unix(int64(m.Date), 0)
}
// IsCommand returns true if message starts with a "bot_command" entity.
func (m *Message) IsCommand() bool {
if m.Entities == nil || len(*m.Entities) == 0 {
return false
}
entity := (*m.Entities)[0]
return entity.Offset == 0 && entity.IsCommand()
}
// Command checks if the message was a command and if it was, returns the
// command. If the Message was not a command, it returns an empty string.
//
// If the command contains the at name syntax, it is removed. Use
// CommandWithAt() if you do not want that.
func (m *Message) Command() string {
command := m.CommandWithAt()
if i := strings.Index(command, "@"); i != -1 {
command = command[:i]
}
return command
}
// CommandWithAt checks if the message was a command and if it was, returns the
// command. If the Message was not a command, it returns an empty string.
//
// If the command contains the at name syntax, it is not removed. Use Command()
// if you want that.
func (m *Message) CommandWithAt() string {
if !m.IsCommand() {
return ""
}
// IsCommand() checks that the message begins with a bot_command entity
entity := (*m.Entities)[0]
return m.Text[1:entity.Length]
}
// CommandArguments checks if the message was a command and if it was,
// returns all text after the command name. If the Message was not a
// command, it returns an empty string.
//
// Note: The first character after the command name is omitted:
// - "/foo bar baz" yields "bar baz", not " bar baz"
// - "/foo-bar baz" yields "bar baz", too
// Even though the latter is not a command conforming to the spec, the API
// marks "/foo" as command entity.
func (m *Message) CommandArguments() string {
if !m.IsCommand() {
return ""
}
// IsCommand() checks that the message begins with a bot_command entity
entity := (*m.Entities)[0]
if len(m.Text) == entity.Length {
return "" // The command makes up the whole message
}
return m.Text[entity.Length+1:]
}
// MessageEntity contains information about data in a Message.
type MessageEntity struct {
Type string `json:"type"`
Offset int `json:"offset"`
Length int `json:"length"`
URL string `json:"url"` // optional
User *User `json:"user"` // optional
}
// ParseURL attempts to parse a URL contained within a MessageEntity.
func (e MessageEntity) ParseURL() (*url.URL, error) {
if e.URL == "" {
return nil, errors.New(ErrBadURL)
}
return url.Parse(e.URL)
}
// IsMention returns true if the type of the message entity is "mention" (@username).
func (e MessageEntity) IsMention() bool {
return e.Type == "mention"
}
// IsHashtag returns true if the type of the message entity is "hashtag".
func (e MessageEntity) IsHashtag() bool {
return e.Type == "hashtag"
}
// IsCommand returns true if the type of the message entity is "bot_command".
func (e MessageEntity) IsCommand() bool {
return e.Type == "bot_command"
}
// IsUrl returns true if the type of the message entity is "url".
func (e MessageEntity) IsUrl() bool {
return e.Type == "url"
}
// IsEmail returns true if the type of the message entity is "email".
func (e MessageEntity) IsEmail() bool {
return e.Type == "email"
}
// IsBold returns true if the type of the message entity is "bold" (bold text).
func (e MessageEntity) IsBold() bool {
return e.Type == "bold"
}
// IsItalic returns true if the type of the message entity is "italic" (italic text).
func (e MessageEntity) IsItalic() bool {
return e.Type == "italic"
}
// IsCode returns true if the type of the message entity is "code" (monowidth string).
func (e MessageEntity) IsCode() bool {
return e.Type == "code"
}
// IsPre returns true if the type of the message entity is "pre" (monowidth block).
func (e MessageEntity) IsPre() bool {
return e.Type == "pre"
}
// IsTextLink returns true if the type of the message entity is "text_link" (clickable text URL).
func (e MessageEntity) IsTextLink() bool {
return e.Type == "text_link"
}
// PhotoSize contains information about photos.
type PhotoSize struct {
FileID string `json:"file_id"`
Width int `json:"width"`
Height int `json:"height"`
FileSize int `json:"file_size"` // optional
}
// Audio contains information about audio.
type Audio struct {
FileID string `json:"file_id"`
Duration int `json:"duration"`
Performer string `json:"performer"` // optional
Title string `json:"title"` // optional
MimeType string `json:"mime_type"` // optional
FileSize int `json:"file_size"` // optional
}
// Document contains information about a document.
type Document struct {
FileID string `json:"file_id"`
Thumbnail *PhotoSize `json:"thumb"` // optional
FileName string `json:"file_name"` // optional
MimeType string `json:"mime_type"` // optional
FileSize int `json:"file_size"` // optional
}
// Sticker contains information about a sticker.
type Sticker struct {
FileUniqueID string `json:"file_unique_id"`
FileID string `json:"file_id"`
Width int `json:"width"`
Height int `json:"height"`
Thumbnail *PhotoSize `json:"thumb"` // optional
Emoji string `json:"emoji"` // optional
FileSize int `json:"file_size"` // optional
SetName string `json:"set_name"` // optional
IsAnimated bool `json:"is_animated"` // optional
}
type StickerSet struct {
Name string `json:"name"`
Title string `json:"title"`
IsAnimated bool `json:"is_animated"`
ContainsMasks bool `json:"contains_masks"`
Stickers []Sticker `json:"stickers"`
}
// ChatAnimation contains information about an animation.
type ChatAnimation struct {
FileID string `json:"file_id"`
Width int `json:"width"`
Height int `json:"height"`
Duration int `json:"duration"`
Thumbnail *PhotoSize `json:"thumb"` // optional
FileName string `json:"file_name"` // optional
MimeType string `json:"mime_type"` // optional
FileSize int `json:"file_size"` // optional
}
// Video contains information about a video.
type Video struct {
FileID string `json:"file_id"`
Width int `json:"width"`
Height int `json:"height"`
Duration int `json:"duration"`
Thumbnail *PhotoSize `json:"thumb"` // optional
MimeType string `json:"mime_type"` // optional
FileSize int `json:"file_size"` // optional
}
// VideoNote contains information about a video.
type VideoNote struct {
FileID string `json:"file_id"`
Length int `json:"length"`
Duration int `json:"duration"`
Thumbnail *PhotoSize `json:"thumb"` // optional
FileSize int `json:"file_size"` // optional
}
// Voice contains information about a voice.
type Voice struct {
FileID string `json:"file_id"`
Duration int `json:"duration"`
MimeType string `json:"mime_type"` // optional
FileSize int `json:"file_size"` // optional
}
// Contact contains information about a contact.
//
// Note that LastName and UserID may be empty.
type Contact struct {
PhoneNumber string `json:"phone_number"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"` // optional
UserID int `json:"user_id"` // optional
}
// Location contains information about a place.
type Location struct {
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
}
// Venue contains information about a venue, including its Location.
type Venue struct {
Location Location `json:"location"`
Title string `json:"title"`
Address string `json:"address"`
FoursquareID string `json:"foursquare_id"` // optional
}
// UserProfilePhotos contains a set of user profile photos.
type UserProfilePhotos struct {
TotalCount int `json:"total_count"`
Photos [][]PhotoSize `json:"photos"`
}
// File contains information about a file to download from Telegram.
type File struct {
FileID string `json:"file_id"`
FileSize int `json:"file_size"` // optional
FilePath string `json:"file_path"` // optional
}
// Link returns a full path to the download URL for a File.
//
// It requires the Bot Token to create the link.
func (f *File) Link(token string) string {
return fmt.Sprintf(FileEndpoint, token, f.FilePath)
}
// ReplyKeyboardMarkup allows the Bot to set a custom keyboard.
type ReplyKeyboardMarkup struct {
Keyboard [][]KeyboardButton `json:"keyboard"`
ResizeKeyboard bool `json:"resize_keyboard"` // optional
OneTimeKeyboard bool `json:"one_time_keyboard"` // optional
Selective bool `json:"selective"` // optional
}
// KeyboardButton is a button within a custom keyboard.
type KeyboardButton struct {
Text string `json:"text"`
RequestContact bool `json:"request_contact"`
RequestLocation bool `json:"request_location"`
}
// ReplyKeyboardHide allows the Bot to hide a custom keyboard.
type ReplyKeyboardHide struct {
HideKeyboard bool `json:"hide_keyboard"`
Selective bool `json:"selective"` // optional
}
// ReplyKeyboardRemove allows the Bot to hide a custom keyboard.
type ReplyKeyboardRemove struct {
RemoveKeyboard bool `json:"remove_keyboard"`
Selective bool `json:"selective"`
}
// InlineKeyboardMarkup is a custom keyboard presented for an inline bot.
type InlineKeyboardMarkup struct {
InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"`
}
// InlineKeyboardButton is a button within a custom keyboard for
// inline query responses.
//
// Note that some values are references as even an empty string
// will change behavior.
//
// CallbackGame, if set, MUST be first button in first row.
type InlineKeyboardButton struct {
Text string `json:"text"`
URL *string `json:"url,omitempty"` // optional
CallbackData *string `json:"callback_data,omitempty"` // optional
SwitchInlineQuery *string `json:"switch_inline_query,omitempty"` // optional
SwitchInlineQueryCurrentChat *string `json:"switch_inline_query_current_chat,omitempty"` // optional
CallbackGame *CallbackGame `json:"callback_game,omitempty"` // optional
Pay bool `json:"pay,omitempty"` // optional
}
// CallbackQuery is data sent when a keyboard button with callback data
// is clicked.
type CallbackQuery struct {
ID string `json:"id"`
From *User `json:"from"`
Message *Message `json:"message"` // optional
InlineMessageID string `json:"inline_message_id"` // optional
ChatInstance string `json:"chat_instance"`
Data string `json:"data"` // optional
GameShortName string `json:"game_short_name"` // optional
}
// ForceReply allows the Bot to have users directly reply to it without
// additional interaction.
type ForceReply struct {
ForceReply bool `json:"force_reply"`
Selective bool `json:"selective"` // optional
}
// ChatMember is information about a member in a chat.
type ChatMember struct {
User *User `json:"user"`
Status string `json:"status"`
UntilDate int64 `json:"until_date,omitempty"` // optional
CanBeEdited bool `json:"can_be_edited,omitempty"` // optional
CanChangeInfo bool `json:"can_change_info,omitempty"` // optional
CanPostMessages bool `json:"can_post_messages,omitempty"` // optional
CanEditMessages bool `json:"can_edit_messages,omitempty"` // optional
CanDeleteMessages bool `json:"can_delete_messages,omitempty"` // optional
CanInviteUsers bool `json:"can_invite_users,omitempty"` // optional
CanRestrictMembers bool `json:"can_restrict_members,omitempty"` // optional
CanPinMessages bool `json:"can_pin_messages,omitempty"` // optional
CanPromoteMembers bool `json:"can_promote_members,omitempty"` // optional
CanSendMessages bool `json:"can_send_messages,omitempty"` // optional
CanSendMediaMessages bool `json:"can_send_media_messages,omitempty"` // optional
CanSendOtherMessages bool `json:"can_send_other_messages,omitempty"` // optional
CanAddWebPagePreviews bool `json:"can_add_web_page_previews,omitempty"` // optional
}
// IsCreator returns if the ChatMember was the creator of the chat.
func (chat ChatMember) IsCreator() bool { return chat.Status == "creator" }
// IsAdministrator returns if the ChatMember is a chat administrator.
func (chat ChatMember) IsAdministrator() bool { return chat.Status == "administrator" }
// IsMember returns if the ChatMember is a current member of the chat.
func (chat ChatMember) IsMember() bool { return chat.Status == "member" }
// HasLeft returns if the ChatMember left the chat.
func (chat ChatMember) HasLeft() bool { return chat.Status == "left" }
// WasKicked returns if the ChatMember was kicked from the chat.
func (chat ChatMember) WasKicked() bool { return chat.Status == "kicked" }
// Game is a game within Telegram.
type Game struct {
Title string `json:"title"`
Description string `json:"description"`
Photo []PhotoSize `json:"photo"`
Text string `json:"text"`
TextEntities []MessageEntity `json:"text_entities"`
Animation Animation `json:"animation"`
}
// Animation is a GIF animation demonstrating the game.
type Animation struct {
FileID string `json:"file_id"`
Thumb PhotoSize `json:"thumb"`
FileName string `json:"file_name"`
MimeType string `json:"mime_type"`
FileSize int `json:"file_size"`
}
// GameHighScore is a user's score and position on the leaderboard.
type GameHighScore struct {
Position int `json:"position"`
User User `json:"user"`
Score int `json:"score"`
}
// CallbackGame is for starting a game in an inline keyboard button.
type CallbackGame struct{}
// WebhookInfo is information about a currently set webhook.
type WebhookInfo struct {
URL string `json:"url"`
HasCustomCertificate bool `json:"has_custom_certificate"`
PendingUpdateCount int `json:"pending_update_count"`
LastErrorDate int `json:"last_error_date"` // optional
LastErrorMessage string `json:"last_error_message"` // optional
}
// IsSet returns true if a webhook is currently set.
func (info WebhookInfo) IsSet() bool {
return info.URL != ""
}
// InputMediaPhoto contains a photo for displaying as part of a media group.
type InputMediaPhoto struct {
Type string `json:"type"`
Media string `json:"media"`
Caption string `json:"caption"`
ParseMode string `json:"parse_mode"`
}
// InputMediaVideo contains a video for displaying as part of a media group.
type InputMediaVideo struct {
Type string `json:"type"`
Media string `json:"media"`
// thumb intentionally missing as it is not currently compatible
Caption string `json:"caption"`
ParseMode string `json:"parse_mode"`
Width int `json:"width"`
Height int `json:"height"`
Duration int `json:"duration"`
SupportsStreaming bool `json:"supports_streaming"`
}
// InlineQuery is a Query from Telegram for an inline request.
type InlineQuery struct {
ID string `json:"id"`
From *User `json:"from"`
Location *Location `json:"location"` // optional
Query string `json:"query"`
Offset string `json:"offset"`
}
// InlineQueryResultArticle is an inline query response article.
type InlineQueryResultArticle struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
Title string `json:"title"` // required
InputMessageContent interface{} `json:"input_message_content,omitempty"` // required
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
URL string `json:"url"`
HideURL bool `json:"hide_url"`
Description string `json:"description"`
ThumbURL string `json:"thumb_url"`
ThumbWidth int `json:"thumb_width"`
ThumbHeight int `json:"thumb_height"`
}
// InlineQueryResultPhoto is an inline query response photo.
type InlineQueryResultPhoto struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
URL string `json:"photo_url"` // required
MimeType string `json:"mime_type"`
Width int `json:"photo_width"`
Height int `json:"photo_height"`
ThumbURL string `json:"thumb_url"`
Title string `json:"title"`
Description string `json:"description"`
Caption string `json:"caption"`
ParseMode string `json:"parse_mode"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
}
// InlineQueryResultCachedPhoto is an inline query response with cached photo.
type InlineQueryResultCachedPhoto struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
PhotoID string `json:"photo_file_id"` // required
Title string `json:"title"`
Description string `json:"description"`
Caption string `json:"caption"`
ParseMode string `json:"parse_mode"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
}
// InlineQueryResultGIF is an inline query response GIF.
type InlineQueryResultGIF struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
URL string `json:"gif_url"` // required
ThumbURL string `json:"thumb_url"` // required
Width int `json:"gif_width,omitempty"`
Height int `json:"gif_height,omitempty"`
Duration int `json:"gif_duration,omitempty"`
Title string `json:"title,omitempty"`
Caption string `json:"caption,omitempty"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
}
// InlineQueryResultCachedGIF is an inline query response with cached gif.
type InlineQueryResultCachedGIF struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
GifID string `json:"gif_file_id"` // required
Title string `json:"title"`
Caption string `json:"caption"`
ParseMode string `json:"parse_mode"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
}
// InlineQueryResultMPEG4GIF is an inline query response MPEG4 GIF.
type InlineQueryResultMPEG4GIF struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
URL string `json:"mpeg4_url"` // required
Width int `json:"mpeg4_width"`
Height int `json:"mpeg4_height"`
Duration int `json:"mpeg4_duration"`
ThumbURL string `json:"thumb_url"`
Title string `json:"title"`
Caption string `json:"caption"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
}
// InlineQueryResultCachedMpeg4Gif is an inline query response with cached
// H.264/MPEG-4 AVC video without sound gif.
type InlineQueryResultCachedMpeg4Gif struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
MGifID string `json:"mpeg4_file_id"` // required
Title string `json:"title"`
Caption string `json:"caption"`
ParseMode string `json:"parse_mode"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
}
// InlineQueryResultVideo is an inline query response video.
type InlineQueryResultVideo struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
URL string `json:"video_url"` // required
MimeType string `json:"mime_type"` // required
ThumbURL string `json:"thumb_url"`
Title string `json:"title"`
Caption string `json:"caption"`
Width int `json:"video_width"`
Height int `json:"video_height"`
Duration int `json:"video_duration"`
Description string `json:"description"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
}
// InlineQueryResultCachedVideo is an inline query response with cached video.
type InlineQueryResultCachedVideo struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
VideoID string `json:"video_file_id"` // required
Title string `json:"title"` // required
Description string `json:"description"`
Caption string `json:"caption"`
ParseMode string `json:"parse_mode"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
}
// InlineQueryResultAudio is an inline query response audio.
type InlineQueryResultAudio struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
URL string `json:"audio_url"` // required
Title string `json:"title"` // required
Caption string `json:"caption"`
Performer string `json:"performer"`
Duration int `json:"audio_duration"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
}
// InlineQueryResultCachedAudio is an inline query response with cached audio.
type InlineQueryResultCachedAudio struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
AudioID string `json:"audio_file_id"` // required
Caption string `json:"caption"`
ParseMode string `json:"parse_mode"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
}
// InlineQueryResultVoice is an inline query response voice.
type InlineQueryResultVoice struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
URL string `json:"voice_url"` // required
Title string `json:"title"` // required
Caption string `json:"caption"`
Duration int `json:"voice_duration"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
}
// InlineQueryResultCachedVoice is an inline query response with cached voice.
type InlineQueryResultCachedVoice struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
VoiceID string `json:"voice_file_id"` // required
Title string `json:"title"` // required
Caption string `json:"caption"`
ParseMode string `json:"parse_mode"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
}
// InlineQueryResultDocument is an inline query response document.
type InlineQueryResultDocument struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
Title string `json:"title"` // required
Caption string `json:"caption"`
URL string `json:"document_url"` // required
MimeType string `json:"mime_type"` // required
Description string `json:"description"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
ThumbURL string `json:"thumb_url"`
ThumbWidth int `json:"thumb_width"`
ThumbHeight int `json:"thumb_height"`
}
// InlineQueryResultCachedDocument is an inline query response with cached document.
type InlineQueryResultCachedDocument struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
DocumentID string `json:"document_file_id"` // required
Title string `json:"title"` // required
Caption string `json:"caption"`
Description string `json:"description"`
ParseMode string `json:"parse_mode"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
}
// InlineQueryResultLocation is an inline query response location.
type InlineQueryResultLocation struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
Latitude float64 `json:"latitude"` // required
Longitude float64 `json:"longitude"` // required
Title string `json:"title"` // required
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
ThumbURL string `json:"thumb_url"`
ThumbWidth int `json:"thumb_width"`
ThumbHeight int `json:"thumb_height"`
}
// InlineQueryResultVenue is an inline query response venue.
type InlineQueryResultVenue struct {
Type string `json:"type"` // required
ID string `json:"id"` // required
Latitude float64 `json:"latitude"` // required
Longitude float64 `json:"longitude"` // required
Title string `json:"title"` // required
Address string `json:"address"` // required
FoursquareID string `json:"foursquare_id"`
FoursquareType string `json:"foursquare_type"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
InputMessageContent interface{} `json:"input_message_content,omitempty"`
ThumbURL string `json:"thumb_url"`
ThumbWidth int `json:"thumb_width"`
ThumbHeight int `json:"thumb_height"`
}
// InlineQueryResultGame is an inline query response game.
type InlineQueryResultGame struct {
Type string `json:"type"`
ID string `json:"id"`
GameShortName string `json:"game_short_name"`
ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"`
}
// ChosenInlineResult is an inline query result chosen by a User
type ChosenInlineResult struct {
ResultID string `json:"result_id"`
From *User `json:"from"`
Location *Location `json:"location"`
InlineMessageID string `json:"inline_message_id"`
Query string `json:"query"`
}
// InputTextMessageContent contains text for displaying
// as an inline query result.
type InputTextMessageContent struct {
Text string `json:"message_text"`
ParseMode string `json:"parse_mode"`
DisableWebPagePreview bool `json:"disable_web_page_preview"`
}
// InputLocationMessageContent contains a location for displaying
// as an inline query result.
type InputLocationMessageContent struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}
// InputVenueMessageContent contains a venue for displaying
// as an inline query result.
type InputVenueMessageContent struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
Title string `json:"title"`
Address string `json:"address"`
FoursquareID string `json:"foursquare_id"`
}
// InputContactMessageContent contains a contact for displaying
// as an inline query result.
type InputContactMessageContent struct {
PhoneNumber string `json:"phone_number"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
// Invoice contains basic information about an invoice.
type Invoice struct {
Title string `json:"title"`
Description string `json:"description"`
StartParameter string `json:"start_parameter"`
Currency string `json:"currency"`
TotalAmount int `json:"total_amount"`
}
// LabeledPrice represents a portion of the price for goods or services.
type LabeledPrice struct {
Label string `json:"label"`
Amount int `json:"amount"`
}
// ShippingAddress represents a shipping address.
type ShippingAddress struct {
CountryCode string `json:"country_code"`
State string `json:"state"`
City string `json:"city"`
StreetLine1 string `json:"street_line1"`
StreetLine2 string `json:"street_line2"`
PostCode string `json:"post_code"`
}
// OrderInfo represents information about an order.
type OrderInfo struct {
Name string `json:"name,omitempty"`
PhoneNumber string `json:"phone_number,omitempty"`
Email string `json:"email,omitempty"`
ShippingAddress *ShippingAddress `json:"shipping_address,omitempty"`
}
// ShippingOption represents one shipping option.
type ShippingOption struct {
ID string `json:"id"`
Title string `json:"title"`
Prices *[]LabeledPrice `json:"prices"`
}
// SuccessfulPayment contains basic information about a successful payment.
type SuccessfulPayment struct {
Currency string `json:"currency"`
TotalAmount int `json:"total_amount"`
InvoicePayload string `json:"invoice_payload"`
ShippingOptionID string `json:"shipping_option_id,omitempty"`
OrderInfo *OrderInfo `json:"order_info,omitempty"`
TelegramPaymentChargeID string `json:"telegram_payment_charge_id"`
ProviderPaymentChargeID string `json:"provider_payment_charge_id"`
}
// ShippingQuery contains information about an incoming shipping query.
type ShippingQuery struct {
ID string `json:"id"`
From *User `json:"from"`
InvoicePayload string `json:"invoice_payload"`
ShippingAddress *ShippingAddress `json:"shipping_address"`
}
// PreCheckoutQuery contains information about an incoming pre-checkout query.
type PreCheckoutQuery struct {
ID string `json:"id"`
From *User `json:"from"`
Currency string `json:"currency"`
TotalAmount int `json:"total_amount"`
InvoicePayload string `json:"invoice_payload"`
ShippingOptionID string `json:"shipping_option_id,omitempty"`
OrderInfo *OrderInfo `json:"order_info,omitempty"`
}
// Error is an error containing extra information returned by the Telegram API.
type Error struct {
Code int
Message string
ResponseParameters
}
func (e Error) Error() string {
return e.Message
}

View File

@@ -1,3 +1,4 @@
.idea/ .idea/
coverage.out coverage.out
tmp/ tmp/
book/

View File

@@ -1,12 +1,14 @@
# Golang bindings for the Telegram Bot API # Golang bindings for the Telegram Bot API
[![GoDoc](https://godoc.org/github.com/go-telegram-bot-api/telegram-bot-api?status.svg)](http://godoc.org/github.com/go-telegram-bot-api/telegram-bot-api) [![Go Reference](https://pkg.go.dev/badge/github.com/go-telegram-bot-api/telegram-bot-api/v5.svg)](https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5)
[![Travis](https://travis-ci.org/go-telegram-bot-api/telegram-bot-api.svg)](https://travis-ci.org/go-telegram-bot-api/telegram-bot-api) [![Test](https://github.com/go-telegram-bot-api/telegram-bot-api/actions/workflows/test.yml/badge.svg)](https://github.com/go-telegram-bot-api/telegram-bot-api/actions/workflows/test.yml)
All methods are fairly self explanatory, and reading the [godoc](http://godoc.org/github.com/go-telegram-bot-api/telegram-bot-api) page should All methods are fairly self-explanatory, and reading the [godoc](https://pkg.go.dev/github.com/go-telegram-bot-api/telegram-bot-api/v5) page should
explain everything. If something isn't clear, open an issue or submit explain everything. If something isn't clear, open an issue or submit
a pull request. a pull request.
There are more tutorials and high-level information on the website, [go-telegram-bot-api.dev](https://go-telegram-bot-api.dev).
The scope of this project is just to provide a wrapper around the API The scope of this project is just to provide a wrapper around the API
without any additional features. There are other projects for creating without any additional features. There are other projects for creating
something with plugins and command handlers without having to design something with plugins and command handlers without having to design
@@ -18,7 +20,7 @@ you want to ask questions or discuss development.
## Example ## Example
First, ensure the library is installed and up to date by running First, ensure the library is installed and up to date by running
`go get -u github.com/go-telegram-bot-api/telegram-bot-api`. `go get -u github.com/go-telegram-bot-api/telegram-bot-api/v5`.
This is a very simple bot that just displays any gotten updates, This is a very simple bot that just displays any gotten updates,
then replies it to that chat. then replies it to that chat.
@@ -29,7 +31,7 @@ package main
import ( import (
"log" "log"
"github.com/go-telegram-bot-api/telegram-bot-api" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
func main() { func main() {
@@ -45,13 +47,10 @@ func main() {
u := tgbotapi.NewUpdate(0) u := tgbotapi.NewUpdate(0)
u.Timeout = 60 u.Timeout = 60
updates, err := bot.GetUpdatesChan(u) updates := bot.GetUpdatesChan(u)
for update := range updates { for update := range updates {
if update.Message == nil { // ignore any non-Message Updates if update.Message != nil { // If we got a message
continue
}
log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text) log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text)
msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text) msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text)
@@ -60,13 +59,9 @@ func main() {
bot.Send(msg) bot.Send(msg)
} }
} }
}
``` ```
There are more examples on the [wiki](https://github.com/go-telegram-bot-api/telegram-bot-api/wiki)
with detailed information on how to do many different kinds of things.
It's a great place to get started on using keyboards, commands, or other
kinds of reply markup.
If you need to use webhooks (if you wish to run on Google App Engine), If you need to use webhooks (if you wish to run on Google App Engine),
you may use a slightly different method. you may use a slightly different method.
@@ -77,7 +72,7 @@ import (
"log" "log"
"net/http" "net/http"
"github.com/go-telegram-bot-api/telegram-bot-api" "github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
func main() { func main() {
@@ -90,17 +85,22 @@ func main() {
log.Printf("Authorized on account %s", bot.Self.UserName) log.Printf("Authorized on account %s", bot.Self.UserName)
_, err = bot.SetWebhook(tgbotapi.NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, "cert.pem")) wh, _ := tgbotapi.NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, "cert.pem")
_, err = bot.SetWebhook(wh)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
info, err := bot.GetWebhookInfo() info, err := bot.GetWebhookInfo()
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
if info.LastErrorDate != 0 { if info.LastErrorDate != 0 {
log.Printf("Telegram callback failed: %s", info.LastErrorMessage) log.Printf("Telegram callback failed: %s", info.LastErrorMessage)
} }
updates := bot.ListenForWebhook("/" + bot.Token) updates := bot.ListenForWebhook("/" + bot.Token)
go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil) go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil)
@@ -110,7 +110,7 @@ func main() {
} }
``` ```
If you need, you may generate a self signed certficate, as this requires If you need, you may generate a self-signed certificate, as this requires
HTTPS / TLS. The above example tells Telegram that this is your HTTPS / TLS. The above example tells Telegram that this is your
certificate and that it should be trusted, even though it is not certificate and that it should be trusted, even though it is not
properly signed. properly signed.

View File

@@ -0,0 +1,9 @@
[book]
authors = ["Syfaro"]
language = "en"
multilingual = false
src = "docs"
title = "Go Telegram Bot API"
[output.html]
git-repository-url = "https://github.com/go-telegram-bot-api/telegram-bot-api"

View File

@@ -0,0 +1,726 @@
// Package tgbotapi has functions and types used for interacting with
// the Telegram Bot API.
package tgbotapi
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"strings"
"time"
)
// HTTPClient is the type needed for the bot to perform HTTP requests.
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
// BotAPI allows you to interact with the Telegram Bot API.
type BotAPI struct {
Token string `json:"token"`
Debug bool `json:"debug"`
Buffer int `json:"buffer"`
Self User `json:"-"`
Client HTTPClient `json:"-"`
shutdownChannel chan interface{}
apiEndpoint string
}
// NewBotAPI creates a new BotAPI instance.
//
// It requires a token, provided by @BotFather on Telegram.
func NewBotAPI(token string) (*BotAPI, error) {
return NewBotAPIWithClient(token, APIEndpoint, &http.Client{})
}
// NewBotAPIWithAPIEndpoint creates a new BotAPI instance
// and allows you to pass API endpoint.
//
// It requires a token, provided by @BotFather on Telegram and API endpoint.
func NewBotAPIWithAPIEndpoint(token, apiEndpoint string) (*BotAPI, error) {
return NewBotAPIWithClient(token, apiEndpoint, &http.Client{})
}
// NewBotAPIWithClient creates a new BotAPI instance
// and allows you to pass a http.Client.
//
// It requires a token, provided by @BotFather on Telegram and API endpoint.
func NewBotAPIWithClient(token, apiEndpoint string, client HTTPClient) (*BotAPI, error) {
bot := &BotAPI{
Token: token,
Client: client,
Buffer: 100,
shutdownChannel: make(chan interface{}),
apiEndpoint: apiEndpoint,
}
self, err := bot.GetMe()
if err != nil {
return nil, err
}
bot.Self = self
return bot, nil
}
// SetAPIEndpoint changes the Telegram Bot API endpoint used by the instance.
func (bot *BotAPI) SetAPIEndpoint(apiEndpoint string) {
bot.apiEndpoint = apiEndpoint
}
func buildParams(in Params) url.Values {
if in == nil {
return url.Values{}
}
out := url.Values{}
for key, value := range in {
out.Set(key, value)
}
return out
}
// MakeRequest makes a request to a specific endpoint with our token.
func (bot *BotAPI) MakeRequest(endpoint string, params Params) (*APIResponse, error) {
if bot.Debug {
log.Printf("Endpoint: %s, params: %v\n", endpoint, params)
}
method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)
values := buildParams(params)
req, err := http.NewRequest("POST", method, strings.NewReader(values.Encode()))
if err != nil {
return &APIResponse{}, err
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := bot.Client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var apiResp APIResponse
bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
if err != nil {
return &apiResp, err
}
if bot.Debug {
log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
}
if !apiResp.Ok {
var parameters ResponseParameters
if apiResp.Parameters != nil {
parameters = *apiResp.Parameters
}
return &apiResp, &Error{
Code: apiResp.ErrorCode,
Message: apiResp.Description,
ResponseParameters: parameters,
}
}
return &apiResp, nil
}
// decodeAPIResponse decode response and return slice of bytes if debug enabled.
// If debug disabled, just decode http.Response.Body stream to APIResponse struct
// for efficient memory usage
func (bot *BotAPI) decodeAPIResponse(responseBody io.Reader, resp *APIResponse) ([]byte, error) {
if !bot.Debug {
dec := json.NewDecoder(responseBody)
err := dec.Decode(resp)
return nil, err
}
// if debug, read response body
data, err := ioutil.ReadAll(responseBody)
if err != nil {
return nil, err
}
err = json.Unmarshal(data, resp)
if err != nil {
return nil, err
}
return data, nil
}
// UploadFiles makes a request to the API with files.
func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFile) (*APIResponse, error) {
r, w := io.Pipe()
m := multipart.NewWriter(w)
// This code modified from the very helpful @HirbodBehnam
// https://github.com/go-telegram-bot-api/telegram-bot-api/issues/354#issuecomment-663856473
go func() {
defer w.Close()
defer m.Close()
for field, value := range params {
if err := m.WriteField(field, value); err != nil {
w.CloseWithError(err)
return
}
}
for _, file := range files {
if file.Data.NeedsUpload() {
name, reader, err := file.Data.UploadData()
if err != nil {
w.CloseWithError(err)
return
}
part, err := m.CreateFormFile(file.Name, name)
if err != nil {
w.CloseWithError(err)
return
}
if _, err := io.Copy(part, reader); err != nil {
w.CloseWithError(err)
return
}
if closer, ok := reader.(io.ReadCloser); ok {
if err = closer.Close(); err != nil {
w.CloseWithError(err)
return
}
}
} else {
value := file.Data.SendData()
if err := m.WriteField(file.Name, value); err != nil {
w.CloseWithError(err)
return
}
}
}
}()
if bot.Debug {
log.Printf("Endpoint: %s, params: %v, with %d files\n", endpoint, params, len(files))
}
method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)
req, err := http.NewRequest("POST", method, r)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", m.FormDataContentType())
resp, err := bot.Client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var apiResp APIResponse
bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
if err != nil {
return &apiResp, err
}
if bot.Debug {
log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
}
if !apiResp.Ok {
var parameters ResponseParameters
if apiResp.Parameters != nil {
parameters = *apiResp.Parameters
}
return &apiResp, &Error{
Message: apiResp.Description,
ResponseParameters: parameters,
}
}
return &apiResp, nil
}
// GetFileDirectURL returns direct URL to file
//
// It requires the FileID.
func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
file, err := bot.GetFile(FileConfig{fileID})
if err != nil {
return "", err
}
return file.Link(bot.Token), nil
}
// GetMe fetches the currently authenticated bot.
//
// This method is called upon creation to validate the token,
// and so you may get this data from BotAPI.Self without the need for
// another request.
func (bot *BotAPI) GetMe() (User, error) {
resp, err := bot.MakeRequest("getMe", nil)
if err != nil {
return User{}, err
}
var user User
err = json.Unmarshal(resp.Result, &user)
return user, err
}
// IsMessageToMe returns true if message directed to this bot.
//
// It requires the Message.
func (bot *BotAPI) IsMessageToMe(message Message) bool {
return strings.Contains(message.Text, "@"+bot.Self.UserName)
}
func hasFilesNeedingUpload(files []RequestFile) bool {
for _, file := range files {
if file.Data.NeedsUpload() {
return true
}
}
return false
}
// Request sends a Chattable to Telegram, and returns the APIResponse.
func (bot *BotAPI) Request(c Chattable) (*APIResponse, error) {
params, err := c.params()
if err != nil {
return nil, err
}
if t, ok := c.(Fileable); ok {
files := t.files()
// If we have files that need to be uploaded, we should delegate the
// request to UploadFile.
if hasFilesNeedingUpload(files) {
return bot.UploadFiles(t.method(), params, files)
}
// However, if there are no files to be uploaded, there's likely things
// that need to be turned into params instead.
for _, file := range files {
params[file.Name] = file.Data.SendData()
}
}
return bot.MakeRequest(c.method(), params)
}
// Send will send a Chattable item to Telegram and provides the
// returned Message.
func (bot *BotAPI) Send(c Chattable) (Message, error) {
resp, err := bot.Request(c)
if err != nil {
return Message{}, err
}
var message Message
err = json.Unmarshal(resp.Result, &message)
return message, err
}
// SendMediaGroup sends a media group and returns the resulting messages.
func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) {
resp, err := bot.Request(config)
if err != nil {
return nil, err
}
var messages []Message
err = json.Unmarshal(resp.Result, &messages)
return messages, err
}
// GetUserProfilePhotos gets a user's profile photos.
//
// It requires UserID.
// Offset and Limit are optional.
func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
resp, err := bot.Request(config)
if err != nil {
return UserProfilePhotos{}, err
}
var profilePhotos UserProfilePhotos
err = json.Unmarshal(resp.Result, &profilePhotos)
return profilePhotos, err
}
// GetFile returns a File which can download a file from Telegram.
//
// Requires FileID.
func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
resp, err := bot.Request(config)
if err != nil {
return File{}, err
}
var file File
err = json.Unmarshal(resp.Result, &file)
return file, err
}
// GetUpdates fetches updates.
// If a WebHook is set, this will not return any data!
//
// Offset, Limit, Timeout, and AllowedUpdates are optional.
// To avoid stale items, set Offset to one higher than the previous item.
// Set Timeout to a large number to reduce requests, so you can get updates
// instantly instead of having to wait between requests.
func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
resp, err := bot.Request(config)
if err != nil {
return []Update{}, err
}
var updates []Update
err = json.Unmarshal(resp.Result, &updates)
return updates, err
}
// GetWebhookInfo allows you to fetch information about a webhook and if
// one currently is set, along with pending update count and error messages.
func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
resp, err := bot.MakeRequest("getWebhookInfo", nil)
if err != nil {
return WebhookInfo{}, err
}
var info WebhookInfo
err = json.Unmarshal(resp.Result, &info)
return info, err
}
// GetUpdatesChan starts and returns a channel for getting updates.
func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel {
ch := make(chan Update, bot.Buffer)
go func() {
for {
select {
case <-bot.shutdownChannel:
close(ch)
return
default:
}
updates, err := bot.GetUpdates(config)
if err != nil {
log.Println(err)
log.Println("Failed to get updates, retrying in 3 seconds...")
time.Sleep(time.Second * 3)
continue
}
for _, update := range updates {
if update.UpdateID >= config.Offset {
config.Offset = update.UpdateID + 1
ch <- update
}
}
}
}()
return ch
}
// StopReceivingUpdates stops the go routine which receives updates
func (bot *BotAPI) StopReceivingUpdates() {
if bot.Debug {
log.Println("Stopping the update receiver routine...")
}
close(bot.shutdownChannel)
}
// ListenForWebhook registers a http handler for a webhook.
func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
ch := make(chan Update, bot.Buffer)
http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
update, err := bot.HandleUpdate(r)
if err != nil {
errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(errMsg)
return
}
ch <- *update
})
return ch
}
// ListenForWebhookRespReqFormat registers a http handler for a single incoming webhook.
func (bot *BotAPI) ListenForWebhookRespReqFormat(w http.ResponseWriter, r *http.Request) UpdatesChannel {
ch := make(chan Update, bot.Buffer)
func(w http.ResponseWriter, r *http.Request) {
update, err := bot.HandleUpdate(r)
if err != nil {
errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})
w.WriteHeader(http.StatusBadRequest)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(errMsg)
return
}
ch <- *update
close(ch)
}(w, r)
return ch
}
// HandleUpdate parses and returns update received via webhook
func (bot *BotAPI) HandleUpdate(r *http.Request) (*Update, error) {
if r.Method != http.MethodPost {
err := errors.New("wrong HTTP method required POST")
return nil, err
}
var update Update
err := json.NewDecoder(r.Body).Decode(&update)
if err != nil {
return nil, err
}
return &update, nil
}
// WriteToHTTPResponse writes the request to the HTTP ResponseWriter.
//
// It doesn't support uploading files.
//
// See https://core.telegram.org/bots/api#making-requests-when-getting-updates
// for details.
func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error {
params, err := c.params()
if err != nil {
return err
}
if t, ok := c.(Fileable); ok {
if hasFilesNeedingUpload(t.files()) {
return errors.New("unable to use http response to upload files")
}
}
values := buildParams(params)
values.Set("method", c.method())
w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
_, err = w.Write([]byte(values.Encode()))
return err
}
// GetChat gets information about a chat.
func (bot *BotAPI) GetChat(config ChatInfoConfig) (Chat, error) {
resp, err := bot.Request(config)
if err != nil {
return Chat{}, err
}
var chat Chat
err = json.Unmarshal(resp.Result, &chat)
return chat, err
}
// GetChatAdministrators gets a list of administrators in the chat.
//
// If none have been appointed, only the creator will be returned.
// Bots are not shown, even if they are an administrator.
func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) {
resp, err := bot.Request(config)
if err != nil {
return []ChatMember{}, err
}
var members []ChatMember
err = json.Unmarshal(resp.Result, &members)
return members, err
}
// GetChatMembersCount gets the number of users in a chat.
func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) {
resp, err := bot.Request(config)
if err != nil {
return -1, err
}
var count int
err = json.Unmarshal(resp.Result, &count)
return count, err
}
// GetChatMember gets a specific chat member.
func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) {
resp, err := bot.Request(config)
if err != nil {
return ChatMember{}, err
}
var member ChatMember
err = json.Unmarshal(resp.Result, &member)
return member, err
}
// GetGameHighScores allows you to get the high scores for a game.
func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
resp, err := bot.Request(config)
if err != nil {
return []GameHighScore{}, err
}
var highScores []GameHighScore
err = json.Unmarshal(resp.Result, &highScores)
return highScores, err
}
// GetInviteLink get InviteLink for a chat
func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) {
resp, err := bot.Request(config)
if err != nil {
return "", err
}
var inviteLink string
err = json.Unmarshal(resp.Result, &inviteLink)
return inviteLink, err
}
// GetStickerSet returns a StickerSet.
func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {
resp, err := bot.Request(config)
if err != nil {
return StickerSet{}, err
}
var stickers StickerSet
err = json.Unmarshal(resp.Result, &stickers)
return stickers, err
}
// StopPoll stops a poll and returns the result.
func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) {
resp, err := bot.Request(config)
if err != nil {
return Poll{}, err
}
var poll Poll
err = json.Unmarshal(resp.Result, &poll)
return poll, err
}
// GetMyCommands gets the currently registered commands.
func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) {
return bot.GetMyCommandsWithConfig(GetMyCommandsConfig{})
}
// GetMyCommandsWithConfig gets the currently registered commands with a config.
func (bot *BotAPI) GetMyCommandsWithConfig(config GetMyCommandsConfig) ([]BotCommand, error) {
resp, err := bot.Request(config)
if err != nil {
return nil, err
}
var commands []BotCommand
err = json.Unmarshal(resp.Result, &commands)
return commands, err
}
// CopyMessage copy messages of any kind. The method is analogous to the method
// forwardMessage, but the copied message doesn't have a link to the original
// message. Returns the MessageID of the sent message on success.
func (bot *BotAPI) CopyMessage(config CopyMessageConfig) (MessageID, error) {
params, err := config.params()
if err != nil {
return MessageID{}, err
}
resp, err := bot.MakeRequest(config.method(), params)
if err != nil {
return MessageID{}, err
}
var messageID MessageID
err = json.Unmarshal(resp.Result, &messageID)
return messageID, err
}
// EscapeText takes an input text and escape Telegram markup symbols.
// In this way we can send a text without being afraid of having to escape the characters manually.
// Note that you don't have to include the formatting style in the input text, or it will be escaped too.
// If there is an error, an empty string will be returned.
//
// parseMode is the text formatting mode (ModeMarkdown, ModeMarkdownV2 or ModeHTML)
// text is the input string that will be escaped
func EscapeText(parseMode string, text string) string {
var replacer *strings.Replacer
if parseMode == ModeHTML {
replacer = strings.NewReplacer("<", "&lt;", ">", "&gt;", "&", "&amp;")
} else if parseMode == ModeMarkdown {
replacer = strings.NewReplacer("_", "\\_", "*", "\\*", "`", "\\`", "[", "\\[")
} else if parseMode == ModeMarkdownV2 {
replacer = strings.NewReplacer(
"_", "\\_", "*", "\\*", "[", "\\[", "]", "\\]", "(",
"\\(", ")", "\\)", "~", "\\~", "`", "\\`", ">", "\\>",
"#", "\\#", "+", "\\+", "-", "\\-", "=", "\\=", "|",
"\\|", "{", "\\{", "}", "\\}", ".", "\\.", "!", "\\!",
)
} else {
return ""
}
return replacer.Replace(text)
}

File diff suppressed because it is too large Load Diff

View File

@@ -52,241 +52,117 @@ func NewForward(chatID int64, fromChatID int64, messageID int) ForwardConfig {
} }
} }
// NewPhotoUpload creates a new photo uploader. // NewCopyMessage creates a new copy message.
//
// chatID is where to send it, fromChatID is the source chat,
// and messageID is the ID of the original message.
func NewCopyMessage(chatID int64, fromChatID int64, messageID int) CopyMessageConfig {
return CopyMessageConfig{
BaseChat: BaseChat{ChatID: chatID},
FromChatID: fromChatID,
MessageID: messageID,
}
}
// NewPhoto creates a new sendPhoto request.
// //
// chatID is where to send it, file is a string path to the file, // chatID is where to send it, file is a string path to the file,
// FileReader, or FileBytes. // FileReader, or FileBytes.
// //
// Note that you must send animated GIFs as a document. // Note that you must send animated GIFs as a document.
func NewPhotoUpload(chatID int64, file interface{}) PhotoConfig { func NewPhoto(chatID int64, file RequestFileData) PhotoConfig {
return PhotoConfig{ return PhotoConfig{
BaseFile: BaseFile{ BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID}, BaseChat: BaseChat{ChatID: chatID},
File: file, File: file,
UseExisting: false,
}, },
} }
} }
// NewPhotoShare shares an existing photo. // NewPhotoToChannel creates a new photo uploader to send a photo to a channel.
// You may use this to reshare an existing photo without reuploading it.
// //
// chatID is where to send it, fileID is the ID of the file // Note that you must send animated GIFs as a document.
// already uploaded. func NewPhotoToChannel(username string, file RequestFileData) PhotoConfig {
func NewPhotoShare(chatID int64, fileID string) PhotoConfig {
return PhotoConfig{ return PhotoConfig{
BaseFile: BaseFile{ BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID}, BaseChat: BaseChat{
FileID: fileID, ChannelUsername: username,
UseExisting: true, },
File: file,
}, },
} }
} }
// NewAudioUpload creates a new audio uploader. // NewAudio creates a new sendAudio request.
// func NewAudio(chatID int64, file RequestFileData) AudioConfig {
// chatID is where to send it, file is a string path to the file,
// FileReader, or FileBytes.
func NewAudioUpload(chatID int64, file interface{}) AudioConfig {
return AudioConfig{ return AudioConfig{
BaseFile: BaseFile{ BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID}, BaseChat: BaseChat{ChatID: chatID},
File: file, File: file,
UseExisting: false,
}, },
} }
} }
// NewAudioShare shares an existing audio file. // NewDocument creates a new sendDocument request.
// You may use this to reshare an existing audio file without func NewDocument(chatID int64, file RequestFileData) DocumentConfig {
// reuploading it.
//
// chatID is where to send it, fileID is the ID of the audio
// already uploaded.
func NewAudioShare(chatID int64, fileID string) AudioConfig {
return AudioConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
FileID: fileID,
UseExisting: true,
},
}
}
// NewDocumentUpload creates a new document uploader.
//
// chatID is where to send it, file is a string path to the file,
// FileReader, or FileBytes.
func NewDocumentUpload(chatID int64, file interface{}) DocumentConfig {
return DocumentConfig{ return DocumentConfig{
BaseFile: BaseFile{ BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID}, BaseChat: BaseChat{ChatID: chatID},
File: file, File: file,
UseExisting: false,
}, },
} }
} }
// NewDocumentShare shares an existing document. // NewSticker creates a new sendSticker request.
// You may use this to reshare an existing document without func NewSticker(chatID int64, file RequestFileData) StickerConfig {
// reuploading it.
//
// chatID is where to send it, fileID is the ID of the document
// already uploaded.
func NewDocumentShare(chatID int64, fileID string) DocumentConfig {
return DocumentConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
FileID: fileID,
UseExisting: true,
},
}
}
// NewStickerUpload creates a new sticker uploader.
//
// chatID is where to send it, file is a string path to the file,
// FileReader, or FileBytes.
func NewStickerUpload(chatID int64, file interface{}) StickerConfig {
return StickerConfig{ return StickerConfig{
BaseFile: BaseFile{ BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID}, BaseChat: BaseChat{ChatID: chatID},
File: file, File: file,
UseExisting: false,
}, },
} }
} }
// NewStickerShare shares an existing sticker. // NewVideo creates a new sendVideo request.
// You may use this to reshare an existing sticker without func NewVideo(chatID int64, file RequestFileData) VideoConfig {
// reuploading it.
//
// chatID is where to send it, fileID is the ID of the sticker
// already uploaded.
func NewStickerShare(chatID int64, fileID string) StickerConfig {
return StickerConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
FileID: fileID,
UseExisting: true,
},
}
}
// NewVideoUpload creates a new video uploader.
//
// chatID is where to send it, file is a string path to the file,
// FileReader, or FileBytes.
func NewVideoUpload(chatID int64, file interface{}) VideoConfig {
return VideoConfig{ return VideoConfig{
BaseFile: BaseFile{ BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID}, BaseChat: BaseChat{ChatID: chatID},
File: file, File: file,
UseExisting: false,
}, },
} }
} }
// NewVideoShare shares an existing video. // NewAnimation creates a new sendAnimation request.
// You may use this to reshare an existing video without reuploading it. func NewAnimation(chatID int64, file RequestFileData) AnimationConfig {
//
// chatID is where to send it, fileID is the ID of the video
// already uploaded.
func NewVideoShare(chatID int64, fileID string) VideoConfig {
return VideoConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
FileID: fileID,
UseExisting: true,
},
}
}
// NewAnimationUpload creates a new animation uploader.
//
// chatID is where to send it, file is a string path to the file,
// FileReader, or FileBytes.
func NewAnimationUpload(chatID int64, file interface{}) AnimationConfig {
return AnimationConfig{ return AnimationConfig{
BaseFile: BaseFile{ BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID}, BaseChat: BaseChat{ChatID: chatID},
File: file, File: file,
UseExisting: false,
}, },
} }
} }
// NewAnimationShare shares an existing animation. // NewVideoNote creates a new sendVideoNote request.
// You may use this to reshare an existing animation without reuploading it.
//
// chatID is where to send it, fileID is the ID of the animation
// already uploaded.
func NewAnimationShare(chatID int64, fileID string) AnimationConfig {
return AnimationConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
FileID: fileID,
UseExisting: true,
},
}
}
// NewVideoNoteUpload creates a new video note uploader.
// //
// chatID is where to send it, file is a string path to the file, // chatID is where to send it, file is a string path to the file,
// FileReader, or FileBytes. // FileReader, or FileBytes.
func NewVideoNoteUpload(chatID int64, length int, file interface{}) VideoNoteConfig { func NewVideoNote(chatID int64, length int, file RequestFileData) VideoNoteConfig {
return VideoNoteConfig{ return VideoNoteConfig{
BaseFile: BaseFile{ BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID}, BaseChat: BaseChat{ChatID: chatID},
File: file, File: file,
UseExisting: false,
}, },
Length: length, Length: length,
} }
} }
// NewVideoNoteShare shares an existing video. // NewVoice creates a new sendVoice request.
// You may use this to reshare an existing video without reuploading it. func NewVoice(chatID int64, file RequestFileData) VoiceConfig {
//
// chatID is where to send it, fileID is the ID of the video
// already uploaded.
func NewVideoNoteShare(chatID int64, length int, fileID string) VideoNoteConfig {
return VideoNoteConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
FileID: fileID,
UseExisting: true,
},
Length: length,
}
}
// NewVoiceUpload creates a new voice uploader.
//
// chatID is where to send it, file is a string path to the file,
// FileReader, or FileBytes.
func NewVoiceUpload(chatID int64, file interface{}) VoiceConfig {
return VoiceConfig{ return VoiceConfig{
BaseFile: BaseFile{ BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID}, BaseChat: BaseChat{ChatID: chatID},
File: file, File: file,
UseExisting: false,
},
}
}
// NewVoiceShare shares an existing voice.
// You may use this to reshare an existing voice without reuploading it.
//
// chatID is where to send it, fileID is the ID of the video
// already uploaded.
func NewVoiceShare(chatID int64, fileID string) VoiceConfig {
return VoiceConfig{
BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID},
FileID: fileID,
UseExisting: true,
}, },
} }
} }
@@ -295,26 +171,58 @@ func NewVoiceShare(chatID int64, fileID string) VoiceConfig {
// two to ten InputMediaPhoto or InputMediaVideo. // two to ten InputMediaPhoto or InputMediaVideo.
func NewMediaGroup(chatID int64, files []interface{}) MediaGroupConfig { func NewMediaGroup(chatID int64, files []interface{}) MediaGroupConfig {
return MediaGroupConfig{ return MediaGroupConfig{
BaseChat: BaseChat{
ChatID: chatID, ChatID: chatID,
}, Media: files,
InputMedia: files,
} }
} }
// NewInputMediaPhoto creates a new InputMediaPhoto. // NewInputMediaPhoto creates a new InputMediaPhoto.
func NewInputMediaPhoto(media string) InputMediaPhoto { func NewInputMediaPhoto(media RequestFileData) InputMediaPhoto {
return InputMediaPhoto{ return InputMediaPhoto{
BaseInputMedia{
Type: "photo", Type: "photo",
Media: media, Media: media,
},
} }
} }
// NewInputMediaVideo creates a new InputMediaVideo. // NewInputMediaVideo creates a new InputMediaVideo.
func NewInputMediaVideo(media string) InputMediaVideo { func NewInputMediaVideo(media RequestFileData) InputMediaVideo {
return InputMediaVideo{ return InputMediaVideo{
BaseInputMedia: BaseInputMedia{
Type: "video", Type: "video",
Media: media, Media: media,
},
}
}
// NewInputMediaAnimation creates a new InputMediaAnimation.
func NewInputMediaAnimation(media RequestFileData) InputMediaAnimation {
return InputMediaAnimation{
BaseInputMedia: BaseInputMedia{
Type: "animation",
Media: media,
},
}
}
// NewInputMediaAudio creates a new InputMediaAudio.
func NewInputMediaAudio(media RequestFileData) InputMediaAudio {
return InputMediaAudio{
BaseInputMedia: BaseInputMedia{
Type: "audio",
Media: media,
},
}
}
// NewInputMediaDocument creates a new InputMediaDocument.
func NewInputMediaDocument(media RequestFileData) InputMediaDocument {
return InputMediaDocument{
BaseInputMedia: BaseInputMedia{
Type: "document",
Media: media,
},
} }
} }
@@ -369,7 +277,7 @@ func NewChatAction(chatID int64, action string) ChatActionConfig {
// NewUserProfilePhotos gets user profile photos. // NewUserProfilePhotos gets user profile photos.
// //
// userID is the ID of the user you wish to get profile photos from. // userID is the ID of the user you wish to get profile photos from.
func NewUserProfilePhotos(userID int) UserProfilePhotosConfig { func NewUserProfilePhotos(userID int64) UserProfilePhotosConfig {
return UserProfilePhotosConfig{ return UserProfilePhotosConfig{
UserID: userID, UserID: userID,
Offset: 0, Offset: 0,
@@ -392,25 +300,33 @@ func NewUpdate(offset int) UpdateConfig {
// NewWebhook creates a new webhook. // NewWebhook creates a new webhook.
// //
// link is the url parsable link you wish to get the updates. // link is the url parsable link you wish to get the updates.
func NewWebhook(link string) WebhookConfig { func NewWebhook(link string) (WebhookConfig, error) {
u, _ := url.Parse(link) u, err := url.Parse(link)
if err != nil {
return WebhookConfig{}, err
}
return WebhookConfig{ return WebhookConfig{
URL: u, URL: u,
} }, nil
} }
// NewWebhookWithCert creates a new webhook with a certificate. // NewWebhookWithCert creates a new webhook with a certificate.
// //
// link is the url you wish to get webhooks, // link is the url you wish to get webhooks,
// file contains a string to a file, FileReader, or FileBytes. // file contains a string to a file, FileReader, or FileBytes.
func NewWebhookWithCert(link string, file interface{}) WebhookConfig { func NewWebhookWithCert(link string, file RequestFileData) (WebhookConfig, error) {
u, _ := url.Parse(link) u, err := url.Parse(link)
if err != nil {
return WebhookConfig{}, err
}
return WebhookConfig{ return WebhookConfig{
URL: u, URL: u,
Certificate: file, Certificate: file,
} }, nil
} }
// NewInlineQueryResultArticle creates a new inline query article. // NewInlineQueryResultArticle creates a new inline query article.
@@ -438,6 +354,19 @@ func NewInlineQueryResultArticleMarkdown(id, title, messageText string) InlineQu
} }
} }
// NewInlineQueryResultArticleMarkdownV2 creates a new inline query article with MarkdownV2 parsing.
func NewInlineQueryResultArticleMarkdownV2(id, title, messageText string) InlineQueryResultArticle {
return InlineQueryResultArticle{
Type: "article",
ID: id,
Title: title,
InputMessageContent: InputTextMessageContent{
Text: messageText,
ParseMode: "MarkdownV2",
},
}
}
// NewInlineQueryResultArticleHTML creates a new inline query article with HTML parsing. // NewInlineQueryResultArticleHTML creates a new inline query article with HTML parsing.
func NewInlineQueryResultArticleHTML(id, title, messageText string) InlineQueryResultArticle { func NewInlineQueryResultArticleHTML(id, title, messageText string) InlineQueryResultArticle {
return InlineQueryResultArticle{ return InlineQueryResultArticle{
@@ -465,7 +394,7 @@ func NewInlineQueryResultCachedGIF(id, gifID string) InlineQueryResultCachedGIF
return InlineQueryResultCachedGIF{ return InlineQueryResultCachedGIF{
Type: "gif", Type: "gif",
ID: id, ID: id,
GifID: gifID, GIFID: gifID,
} }
} }
@@ -478,12 +407,12 @@ func NewInlineQueryResultMPEG4GIF(id, url string) InlineQueryResultMPEG4GIF {
} }
} }
// NewInlineQueryResultCachedPhoto create a new inline query with cached photo. // NewInlineQueryResultCachedMPEG4GIF create a new inline query with cached MPEG4 GIF.
func NewInlineQueryResultCachedMPEG4GIF(id, MPEG4GifID string) InlineQueryResultCachedMpeg4Gif { func NewInlineQueryResultCachedMPEG4GIF(id, MPEG4GIFID string) InlineQueryResultCachedMPEG4GIF {
return InlineQueryResultCachedMpeg4Gif{ return InlineQueryResultCachedMPEG4GIF{
Type: "mpeg4_gif", Type: "mpeg4_gif",
ID: id, ID: id,
MGifID: MPEG4GifID, MPEG4FileID: MPEG4GIFID,
} }
} }
@@ -534,6 +463,16 @@ func NewInlineQueryResultCachedVideo(id, videoID, title string) InlineQueryResul
} }
} }
// NewInlineQueryResultCachedSticker create a new inline query with cached sticker.
func NewInlineQueryResultCachedSticker(id, stickerID, title string) InlineQueryResultCachedSticker {
return InlineQueryResultCachedSticker{
Type: "sticker",
ID: id,
StickerID: stickerID,
Title: title,
}
}
// NewInlineQueryResultAudio creates a new inline query audio. // NewInlineQueryResultAudio creates a new inline query audio.
func NewInlineQueryResultAudio(id, url, title string) InlineQueryResultAudio { func NewInlineQueryResultAudio(id, url, title string) InlineQueryResultAudio {
return InlineQueryResultAudio{ return InlineQueryResultAudio{
@@ -628,6 +567,18 @@ func NewEditMessageText(chatID int64, messageID int, text string) EditMessageTex
} }
} }
// NewEditMessageTextAndMarkup allows you to edit the text and replymarkup of a message.
func NewEditMessageTextAndMarkup(chatID int64, messageID int, text string, replyMarkup InlineKeyboardMarkup) EditMessageTextConfig {
return EditMessageTextConfig{
BaseEdit: BaseEdit{
ChatID: chatID,
MessageID: messageID,
ReplyMarkup: &replyMarkup,
},
Text: text,
}
}
// NewEditMessageCaption allows you to edit the caption of a message. // NewEditMessageCaption allows you to edit the caption of a message.
func NewEditMessageCaption(chatID int64, messageID int, caption string) EditMessageCaptionConfig { func NewEditMessageCaption(chatID int64, messageID int, caption string) EditMessageCaptionConfig {
return EditMessageCaptionConfig{ return EditMessageCaptionConfig{
@@ -651,17 +602,6 @@ func NewEditMessageReplyMarkup(chatID int64, messageID int, replyMarkup InlineKe
} }
} }
// NewHideKeyboard hides the keyboard, with the option for being selective
// or hiding for everyone.
func NewHideKeyboard(selective bool) ReplyKeyboardHide {
log.Println("NewHideKeyboard is deprecated, please use NewRemoveKeyboard")
return ReplyKeyboardHide{
HideKeyboard: true,
Selective: selective,
}
}
// NewRemoveKeyboard hides the keyboard, with the option for being selective // NewRemoveKeyboard hides the keyboard, with the option for being selective
// or hiding for everyone. // or hiding for everyone.
func NewRemoveKeyboard(selective bool) ReplyKeyboardRemove { func NewRemoveKeyboard(selective bool) ReplyKeyboardRemove {
@@ -717,6 +657,13 @@ func NewReplyKeyboard(rows ...[]KeyboardButton) ReplyKeyboardMarkup {
} }
} }
// NewOneTimeReplyKeyboard creates a new one time keyboard.
func NewOneTimeReplyKeyboard(rows ...[]KeyboardButton) ReplyKeyboardMarkup {
markup := NewReplyKeyboard(rows...)
markup.OneTimeKeyboard = true
return markup
}
// NewInlineKeyboardButtonData creates an inline keyboard button with text // NewInlineKeyboardButtonData creates an inline keyboard button with text
// and data for a callback. // and data for a callback.
func NewInlineKeyboardButtonData(text, data string) InlineKeyboardButton { func NewInlineKeyboardButtonData(text, data string) InlineKeyboardButton {
@@ -726,6 +673,15 @@ func NewInlineKeyboardButtonData(text, data string) InlineKeyboardButton {
} }
} }
// NewInlineKeyboardButtonLoginURL creates an inline keyboard button with text
// which goes to a LoginURL.
func NewInlineKeyboardButtonLoginURL(text string, loginURL LoginURL) InlineKeyboardButton {
return InlineKeyboardButton{
Text: text,
LoginURL: &loginURL,
}
}
// NewInlineKeyboardButtonURL creates an inline keyboard button with text // NewInlineKeyboardButtonURL creates an inline keyboard button with text
// which goes to a URL. // which goes to a URL.
func NewInlineKeyboardButtonURL(text, url string) InlineKeyboardButton { func NewInlineKeyboardButtonURL(text, url string) InlineKeyboardButton {
@@ -784,7 +740,7 @@ func NewCallbackWithAlert(id, text string) CallbackConfig {
} }
// NewInvoice creates a new Invoice request to the user. // NewInvoice creates a new Invoice request to the user.
func NewInvoice(chatID int64, title, description, payload, providerToken, startParameter, currency string, prices *[]LabeledPrice) InvoiceConfig { func NewInvoice(chatID int64, title, description, payload, providerToken, startParameter, currency string, prices []LabeledPrice) InvoiceConfig {
return InvoiceConfig{ return InvoiceConfig{
BaseChat: BaseChat{ChatID: chatID}, BaseChat: BaseChat{ChatID: chatID},
Title: title, Title: title,
@@ -796,33 +752,176 @@ func NewInvoice(chatID int64, title, description, payload, providerToken, startP
Prices: prices} Prices: prices}
} }
// NewSetChatPhotoUpload creates a new chat photo uploader. // NewChatTitle allows you to update the title of a chat.
// func NewChatTitle(chatID int64, title string) SetChatTitleConfig {
// chatID is where to send it, file is a string path to the file, return SetChatTitleConfig{
// FileReader, or FileBytes. ChatID: chatID,
// Title: title,
// Note that you must send animated GIFs as a document. }
func NewSetChatPhotoUpload(chatID int64, file interface{}) SetChatPhotoConfig { }
// NewChatDescription allows you to update the description of a chat.
func NewChatDescription(chatID int64, description string) SetChatDescriptionConfig {
return SetChatDescriptionConfig{
ChatID: chatID,
Description: description,
}
}
// NewChatPhoto allows you to update the photo for a chat.
func NewChatPhoto(chatID int64, photo RequestFileData) SetChatPhotoConfig {
return SetChatPhotoConfig{ return SetChatPhotoConfig{
BaseFile: BaseFile{ BaseFile: BaseFile{
BaseChat: BaseChat{ChatID: chatID}, BaseChat: BaseChat{
File: file, ChatID: chatID,
UseExisting: false, },
File: photo,
}, },
} }
} }
// NewSetChatPhotoShare shares an existing photo. // NewDeleteChatPhoto allows you to delete the photo for a chat.
// You may use this to reshare an existing photo without reuploading it. func NewDeleteChatPhoto(chatID int64) DeleteChatPhotoConfig {
// return DeleteChatPhotoConfig{
// chatID is where to send it, fileID is the ID of the file ChatID: chatID,
// already uploaded. }
func NewSetChatPhotoShare(chatID int64, fileID string) SetChatPhotoConfig { }
return SetChatPhotoConfig{
BaseFile: BaseFile{ // NewPoll allows you to create a new poll.
BaseChat: BaseChat{ChatID: chatID}, func NewPoll(chatID int64, question string, options ...string) SendPollConfig {
FileID: fileID, return SendPollConfig{
UseExisting: true, BaseChat: BaseChat{
ChatID: chatID,
},
Question: question,
Options: options,
IsAnonymous: true, // This is Telegram's default.
}
}
// NewStopPoll allows you to stop a poll.
func NewStopPoll(chatID int64, messageID int) StopPollConfig {
return StopPollConfig{
BaseEdit{
ChatID: chatID,
MessageID: messageID,
}, },
} }
} }
// NewDice allows you to send a random dice roll.
func NewDice(chatID int64) DiceConfig {
return DiceConfig{
BaseChat: BaseChat{
ChatID: chatID,
},
}
}
// NewDiceWithEmoji allows you to send a random roll of one of many types.
//
// Emoji may be 🎲 (1-6), 🎯 (1-6), or 🏀 (1-5).
func NewDiceWithEmoji(chatID int64, emoji string) DiceConfig {
return DiceConfig{
BaseChat: BaseChat{
ChatID: chatID,
},
Emoji: emoji,
}
}
// NewBotCommandScopeDefault represents the default scope of bot commands.
func NewBotCommandScopeDefault() BotCommandScope {
return BotCommandScope{Type: "default"}
}
// NewBotCommandScopeAllPrivateChats represents the scope of bot commands,
// covering all private chats.
func NewBotCommandScopeAllPrivateChats() BotCommandScope {
return BotCommandScope{Type: "all_private_chats"}
}
// NewBotCommandScopeAllGroupChats represents the scope of bot commands,
// covering all group and supergroup chats.
func NewBotCommandScopeAllGroupChats() BotCommandScope {
return BotCommandScope{Type: "all_group_chats"}
}
// NewBotCommandScopeAllChatAdministrators represents the scope of bot commands,
// covering all group and supergroup chat administrators.
func NewBotCommandScopeAllChatAdministrators() BotCommandScope {
return BotCommandScope{Type: "all_chat_administrators"}
}
// NewBotCommandScopeChat represents the scope of bot commands, covering a
// specific chat.
func NewBotCommandScopeChat(chatID int64) BotCommandScope {
return BotCommandScope{
Type: "chat",
ChatID: chatID,
}
}
// NewBotCommandScopeChatAdministrators represents the scope of bot commands,
// covering all administrators of a specific group or supergroup chat.
func NewBotCommandScopeChatAdministrators(chatID int64) BotCommandScope {
return BotCommandScope{
Type: "chat_administrators",
ChatID: chatID,
}
}
// NewBotCommandScopeChatMember represents the scope of bot commands, covering a
// specific member of a group or supergroup chat.
func NewBotCommandScopeChatMember(chatID, userID int64) BotCommandScope {
return BotCommandScope{
Type: "chat_member",
ChatID: chatID,
UserID: userID,
}
}
// NewGetMyCommandsWithScope allows you to set the registered commands for a
// given scope.
func NewGetMyCommandsWithScope(scope BotCommandScope) GetMyCommandsConfig {
return GetMyCommandsConfig{Scope: &scope}
}
// NewGetMyCommandsWithScopeAndLanguage allows you to set the registered
// commands for a given scope and language code.
func NewGetMyCommandsWithScopeAndLanguage(scope BotCommandScope, languageCode string) GetMyCommandsConfig {
return GetMyCommandsConfig{Scope: &scope, LanguageCode: languageCode}
}
// NewSetMyCommands allows you to set the registered commands.
func NewSetMyCommands(commands ...BotCommand) SetMyCommandsConfig {
return SetMyCommandsConfig{Commands: commands}
}
// NewSetMyCommandsWithScope allows you to set the registered commands for a given scope.
func NewSetMyCommandsWithScope(scope BotCommandScope, commands ...BotCommand) SetMyCommandsConfig {
return SetMyCommandsConfig{Commands: commands, Scope: &scope}
}
// NewSetMyCommandsWithScopeAndLanguage allows you to set the registered commands for a given scope
// and language code.
func NewSetMyCommandsWithScopeAndLanguage(scope BotCommandScope, languageCode string, commands ...BotCommand) SetMyCommandsConfig {
return SetMyCommandsConfig{Commands: commands, Scope: &scope, LanguageCode: languageCode}
}
// NewDeleteMyCommands allows you to delete the registered commands.
func NewDeleteMyCommands() DeleteMyCommandsConfig {
return DeleteMyCommandsConfig{}
}
// NewDeleteMyCommandsWithScope allows you to delete the registered commands for a given
// scope.
func NewDeleteMyCommandsWithScope(scope BotCommandScope) DeleteMyCommandsConfig {
return DeleteMyCommandsConfig{Scope: &scope}
}
// NewDeleteMyCommandsWithScopeAndLanguage allows you to delete the registered commands for a given
// scope and language code.
func NewDeleteMyCommandsWithScopeAndLanguage(scope BotCommandScope, languageCode string) DeleteMyCommandsConfig {
return DeleteMyCommandsConfig{Scope: &scope, LanguageCode: languageCode}
}

View File

@@ -0,0 +1,97 @@
package tgbotapi
import (
"encoding/json"
"reflect"
"strconv"
)
// Params represents a set of parameters that gets passed to a request.
type Params map[string]string
// AddNonEmpty adds a value if it not an empty string.
func (p Params) AddNonEmpty(key, value string) {
if value != "" {
p[key] = value
}
}
// AddNonZero adds a value if it is not zero.
func (p Params) AddNonZero(key string, value int) {
if value != 0 {
p[key] = strconv.Itoa(value)
}
}
// AddNonZero64 is the same as AddNonZero except uses an int64.
func (p Params) AddNonZero64(key string, value int64) {
if value != 0 {
p[key] = strconv.FormatInt(value, 10)
}
}
// AddBool adds a value of a bool if it is true.
func (p Params) AddBool(key string, value bool) {
if value {
p[key] = strconv.FormatBool(value)
}
}
// AddNonZeroFloat adds a floating point value that is not zero.
func (p Params) AddNonZeroFloat(key string, value float64) {
if value != 0 {
p[key] = strconv.FormatFloat(value, 'f', 6, 64)
}
}
// AddInterface adds an interface if it is not nil and can be JSON marshalled.
func (p Params) AddInterface(key string, value interface{}) error {
if value == nil || (reflect.ValueOf(value).Kind() == reflect.Ptr && reflect.ValueOf(value).IsNil()) {
return nil
}
b, err := json.Marshal(value)
if err != nil {
return err
}
p[key] = string(b)
return nil
}
// AddFirstValid attempts to add the first item that is not a default value.
//
// For example, AddFirstValid(0, "", "test") would add "test".
func (p Params) AddFirstValid(key string, args ...interface{}) error {
for _, arg := range args {
switch v := arg.(type) {
case int:
if v != 0 {
p[key] = strconv.Itoa(v)
return nil
}
case int64:
if v != 0 {
p[key] = strconv.FormatInt(v, 10)
return nil
}
case string:
if v != "" {
p[key] = v
return nil
}
case nil:
default:
b, err := json.Marshal(arg)
if err != nil {
return err
}
p[key] = string(b)
return nil
}
}
return nil
}

View File

@@ -54,13 +54,15 @@ type (
Credentials *EncryptedCredentials `json:"credentials"` Credentials *EncryptedCredentials `json:"credentials"`
} }
// PassportFile represents a file uploaded to Telegram Passport. Currently all // PassportFile represents a file uploaded to Telegram Passport. Currently, all
// Telegram Passport files are in JPEG format when decrypted and don't exceed // Telegram Passport files are in JPEG format when decrypted and don't exceed
// 10MB. // 10MB.
PassportFile struct { PassportFile struct {
// Unique identifier for this file // Unique identifier for this file
FileID string `json:"file_id"` FileID string `json:"file_id"`
FileUniqueID string `json:"file_unique_id"`
// File size // File size
FileSize int `json:"file_size"` FileSize int `json:"file_size"`
@@ -212,7 +214,7 @@ type (
// PassportElementErrorFile represents an issue with a document scan. The // PassportElementErrorFile represents an issue with a document scan. The
// error is considered resolved when the file with the document scan changes. // error is considered resolved when the file with the document scan changes.
PassportElementErrorFile struct { PassportElementErrorFile struct {
// Error source, must be file // Error source, must be a file
Source string `json:"source"` Source string `json:"source"`
// The section of the user's Telegram Passport which has the issue, one // The section of the user's Telegram Passport which has the issue, one

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