Merge branch 'master' into discord-naitive-upload

This commit is contained in:
Joseph Mansy
2023-03-14 22:21:08 -07:00
committed by GitHub
416 changed files with 153418 additions and 126519 deletions

View File

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

View File

@@ -81,17 +81,6 @@ func (b *Bdiscord) Connect() error {
return err
}
b.Log.Info("Connection succeeded")
b.c.AddHandler(b.messageCreate)
b.c.AddHandler(b.messageTyping)
b.c.AddHandler(b.messageUpdate)
b.c.AddHandler(b.messageDelete)
b.c.AddHandler(b.messageDeleteBulk)
b.c.AddHandler(b.memberAdd)
b.c.AddHandler(b.memberRemove)
b.c.AddHandler(b.memberUpdate)
if b.GetInt("debuglevel") == 1 {
b.c.AddHandler(b.messageEvent)
}
// Add privileged intent for guild member tracking. This is needed to track nicks
// for display names and @mention translation
b.c.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged |
@@ -233,6 +222,19 @@ func (b *Bdiscord) Connect() error {
b.nickMemberMap[member.Nick] = member
}
}
b.c.AddHandler(b.messageCreate)
b.c.AddHandler(b.messageTyping)
b.c.AddHandler(b.messageUpdate)
b.c.AddHandler(b.messageDelete)
b.c.AddHandler(b.messageDeleteBulk)
b.c.AddHandler(b.memberAdd)
b.c.AddHandler(b.memberRemove)
b.c.AddHandler(b.memberUpdate)
if b.GetInt("debuglevel") == 1 {
b.c.AddHandler(b.messageEvent)
}
return nil
}

View File

@@ -47,8 +47,9 @@ func (b *Bdiscord) maybeGetLocalAvatar(msg *config.Message) string {
// Returns messageID and error.
func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordgo.Message, error) {
var (
res *discordgo.Message
err error
res *discordgo.Message
res2 *discordgo.Message
err error
)
// If avatar is unset, mutate the message to include the local avatar (but only if settings say we should do this)
@@ -84,7 +85,7 @@ func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordg
}
content := fi.Comment
_, e2 := b.transmitter.Send(
res2, err = b.transmitter.Send(
channelID,
&discordgo.WebhookParams{
Username: msg.Username,
@@ -94,11 +95,16 @@ func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordg
AllowedMentions: b.getAllowedMentions(),
},
)
if e2 != nil {
b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, e2)
if err != nil {
b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, err)
}
}
}
if msg.Text == "" {
res = res2
}
return res, err
}

View File

@@ -161,7 +161,7 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
if err != nil {
b.Log.Errorf("getting post %s failed: %s", msg.ParentID, err)
}
if post.RootId != "" {
if post != nil && post.RootId != "" {
msg.ParentID = post.RootId
}
}

View File

@@ -42,7 +42,14 @@ func (b *Bmumble) handleTextMessage(event *gumble.TextMessageEvent) {
if part.Image == nil {
rmsg.Text = part.Text
} else {
fname := b.Account + "_" + strconv.FormatInt(now.UnixNano(), 10) + "_" + strconv.Itoa(i) + part.FileExtension
fileExt := part.FileExtension
if fileExt == ".jfif" {
fileExt = ".jpg"
}
if fileExt == ".jpe" {
fileExt = ".jpg"
}
fname := b.Account + "_" + strconv.FormatInt(now.UnixNano(), 10) + "_" + strconv.Itoa(i) + fileExt
rmsg.Extra = make(map[string][]interface{})
if err = helper.HandleDownloadSize(b.Log, &rmsg, fname, int64(len(part.Image)), b.General); err != nil {
b.Log.WithError(err).Warn("not including image in message")
@@ -62,7 +69,6 @@ func (b *Bmumble) handleConnect(event *gumble.ConnectEvent) {
}
// No need to talk or listen
event.Client.Self.SetSelfDeafened(true)
event.Client.Self.SetSelfMuted(true)
// if the Channel variable is set, this is a reconnect -> rejoin channel
if b.Channel != nil {
if err := b.doJoin(event.Client, *b.Channel); err != nil {

View File

@@ -250,7 +250,12 @@ func (b *Bmumble) processMessage(msg *config.Message) {
// If there is a maximum message length, split and truncate the lines
var msgLines []string
if maxLength := b.serverConfig.MaximumMessageLength; maxLength != nil {
msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username), b.GetString("MessageClipped"))
if *maxLength != 0 { // Some servers will have unlimited message lengths.
// Not doing this makes underflows happen.
msgLines = helper.GetSubLines(msg.Text, *maxLength-len(msg.Username), b.GetString("MessageClipped"))
} else {
msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
}
} else {
msgLines = helper.GetSubLines(msg.Text, 0, b.GetString("MessageClipped"))
}

View File

@@ -11,7 +11,7 @@ import (
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
"github.com/davecgh/go-spew/spew"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
tgbotapi "github.com/matterbridge/telegram-bot-api/v6"
)
func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *tgbotapi.Message) *tgbotapi.Message {
@@ -20,6 +20,11 @@ func (b *Btelegram) handleUpdate(rmsg *config.Message, message, posted, edited *
if posted.Text == "/chatId" {
chatID := strconv.FormatInt(posted.Chat.ID, 10)
// Handle chat topics
if posted.IsTopicMessage {
chatID = chatID + "/" + strconv.Itoa(posted.MessageThreadID)
}
_, err := b.Send(config.Message{
Channel: chatID,
Text: fmt.Sprintf("ID of this chat: %s", chatID),
@@ -91,7 +96,8 @@ func (b *Btelegram) handleForwarded(rmsg *config.Message, message *tgbotapi.Mess
// handleQuoting handles quoting of previous messages
func (b *Btelegram) handleQuoting(rmsg *config.Message, message *tgbotapi.Message) {
if message.ReplyToMessage != nil {
// Used to check if the message was a reply to the root topic
if message.ReplyToMessage != nil && !(message.ReplyToMessage.MessageID == message.MessageThreadID) { //nolint:nestif
usernameReply := ""
if message.ReplyToMessage.From != nil {
if b.GetBool("UseFirstName") {
@@ -128,7 +134,9 @@ func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Messa
rmsg.Username = message.From.FirstName
}
if b.GetBool("UseFullName") {
rmsg.Username = message.From.FirstName + " " + message.From.LastName
if message.From.FirstName != "" && message.From.LastName != "" {
rmsg.Username = message.From.FirstName + " " + message.From.LastName
}
}
if rmsg.Username == "" {
rmsg.Username = message.From.UserName
@@ -148,7 +156,9 @@ func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Messa
rmsg.Username = message.SenderChat.FirstName
}
if b.GetBool("UseFullName") {
rmsg.Username = message.SenderChat.FirstName + " " + message.SenderChat.LastName
if message.SenderChat.FirstName != "" && message.SenderChat.LastName != "" {
rmsg.Username = message.SenderChat.FirstName + " " + message.SenderChat.LastName
}
}
if rmsg.Username == "" || rmsg.Username == "Channel_Bot" {
@@ -164,6 +174,11 @@ func (b *Btelegram) handleUsername(rmsg *config.Message, message *tgbotapi.Messa
}
}
// Fallback on author signature (used in "channel" type of chat)
if rmsg.Username == "" && message.AuthorSignature != "" {
rmsg.Username = message.AuthorSignature
}
// if we really didn't find a username, set it to unknown
if rmsg.Username == "" {
rmsg.Username = unknownUser
@@ -202,9 +217,14 @@ func (b *Btelegram) handleRecv(updates <-chan tgbotapi.Update) {
// set the ID's from the channel or group message
rmsg.ID = strconv.Itoa(message.MessageID)
rmsg.Channel = strconv.FormatInt(message.Chat.ID, 10)
if message.MessageThreadID != 0 {
rmsg.Channel += "/" + strconv.Itoa(message.MessageThreadID)
}
// preserve threading from telegram reply
if message.ReplyToMessage != nil {
if message.ReplyToMessage != nil &&
// Used to check if the message was a reply to the root topic
!(message.ReplyToMessage.MessageID == message.MessageThreadID) {
rmsg.ParentID = strconv.Itoa(message.ReplyToMessage.MessageID)
}
@@ -317,12 +337,12 @@ func (b *Btelegram) maybeConvertWebp(name *string, data *[]byte) {
// handleDownloadFile handles file download
func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Message) error {
size := 0
size := int64(0)
var url, name, text string
switch {
case message.Sticker != nil:
text, name, url = b.getDownloadInfo(message.Sticker.FileID, ".webp", true)
size = message.Sticker.FileSize
size = int64(message.Sticker.FileSize)
case message.Voice != nil:
text, name, url = b.getDownloadInfo(message.Voice.FileID, ".ogg", true)
size = message.Voice.FileSize
@@ -339,7 +359,7 @@ func (b *Btelegram) handleDownload(rmsg *config.Message, message *tgbotapi.Messa
text = " " + message.Document.FileName + " : " + url
case message.Photo != nil:
photos := message.Photo
size = photos[len(photos)-1].FileSize
size = int64(photos[len(photos)-1].FileSize)
text, name, url = b.getDownloadInfo(photos[len(photos)-1].FileID, "", true)
}
@@ -443,7 +463,7 @@ func (b *Btelegram) handleEdit(msg *config.Message, chatid int64) (string, error
}
// handleUploadFile handles native upload of files
func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64, parentID int) (string, error) {
func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64, threadid int, parentID int) (string, error) {
var media []interface{}
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo)
@@ -493,7 +513,7 @@ func (b *Btelegram) handleUploadFile(msg *config.Message, chatid int64, parentID
}
}
return b.sendMediaFiles(msg, chatid, parentID, media)
return b.sendMediaFiles(msg, chatid, threadid, parentID, media)
}
func (b *Btelegram) handleQuote(message, quoteNick, quoteMessage string) string {

View File

@@ -10,7 +10,7 @@ import (
"github.com/42wim/matterbridge/bridge"
"github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
tgbotapi "github.com/matterbridge/telegram-bot-api/v6"
)
const (
@@ -86,11 +86,41 @@ func TGGetParseMode(b *Btelegram, username string, text string) (textout string,
return textout, parsemode
}
func (b *Btelegram) getIds(channel string) (int64, int, error) {
var chatid int64
topicid := 0
// get the chatid
if strings.Contains(channel, "/") { //nolint:nestif
s := strings.Split(channel, "/")
if len(s) < 2 {
b.Log.Errorf("Invalid channel format: %#v\n", channel)
return 0, 0, nil
}
id, err := strconv.ParseInt(s[0], 10, 64)
if err != nil {
return 0, 0, err
}
chatid = id
tid, err := strconv.Atoi(s[1])
if err != nil {
return 0, 0, err
}
topicid = tid
} else {
id, err := strconv.ParseInt(channel, 10, 64)
if err != nil {
return 0, 0, err
}
chatid = id
}
return chatid, topicid, nil
}
func (b *Btelegram) Send(msg config.Message) (string, error) {
b.Log.Debugf("=> Receiving %#v", msg)
// get the chatid
chatid, err := strconv.ParseInt(msg.Channel, 10, 64)
chatid, topicid, err := b.getIds(msg.Channel)
if err != nil {
return "", err
}
@@ -123,13 +153,13 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
// Upload a file if it exists
if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(&msg, b.General) {
if _, msgErr := b.sendMessage(chatid, rmsg.Username, rmsg.Text, parentID); msgErr != nil {
if _, msgErr := b.sendMessage(chatid, topicid, rmsg.Username, rmsg.Text, parentID); msgErr != nil {
b.Log.Errorf("sendMessage failed: %s", msgErr)
}
}
// check if we have files to upload (from slack, telegram or mattermost)
if len(msg.Extra["file"]) > 0 {
return b.handleUploadFile(&msg, chatid, parentID)
return b.handleUploadFile(&msg, chatid, topicid, parentID)
}
}
@@ -143,7 +173,7 @@ func (b *Btelegram) Send(msg config.Message) (string, error) {
// Ignore empty text field needs for prevent double messages from whatsapp to telegram
// when sending media with text caption
if msg.Text != "" {
return b.sendMessage(chatid, msg.Username, msg.Text, parentID)
return b.sendMessage(chatid, topicid, msg.Username, msg.Text, parentID)
}
return "", nil
@@ -157,9 +187,12 @@ func (b *Btelegram) getFileDirectURL(id string) string {
return res
}
func (b *Btelegram) sendMessage(chatid int64, username, text string, parentID int) (string, error) {
func (b *Btelegram) sendMessage(chatid int64, topicid int, username, text string, parentID int) (string, error) {
m := tgbotapi.NewMessage(chatid, "")
m.Text, m.ParseMode = TGGetParseMode(b, username, text)
if topicid != 0 {
m.BaseChat.MessageThreadID = topicid
}
m.ReplyToMessageID = parentID
m.DisableWebPagePreview = b.GetBool("DisableWebPagePreview")
@@ -171,11 +204,19 @@ func (b *Btelegram) sendMessage(chatid int64, username, text string, parentID in
}
// sendMediaFiles native upload media files via media group
func (b *Btelegram) sendMediaFiles(msg *config.Message, chatid int64, parentID int, media []interface{}) (string, error) {
func (b *Btelegram) sendMediaFiles(msg *config.Message, chatid int64, threadid int, parentID int, media []interface{}) (string, error) {
if len(media) == 0 {
return "", nil
}
mg := tgbotapi.MediaGroupConfig{ChatID: chatid, ChannelUsername: msg.Username, Media: media, ReplyToMessageID: parentID}
mg := tgbotapi.MediaGroupConfig{
BaseChat: tgbotapi.BaseChat{
ChatID: chatid,
MessageThreadID: threadid,
ChannelUsername: msg.Username,
ReplyToMessageID: parentID,
},
Media: media,
}
messages, err := b.c.SendMediaGroup(mg)
if err != nil {
return "", err

View File

@@ -1,3 +1,4 @@
//go:build whatsappmulti
// +build whatsappmulti
package bwhatsapp
@@ -20,9 +21,82 @@ func (b *Bwhatsapp) eventHandler(evt interface{}) {
switch e := evt.(type) {
case *events.Message:
b.handleMessage(e)
case *events.GroupInfo:
b.handleGroupInfo(e)
}
}
func (b *Bwhatsapp) handleGroupInfo(event *events.GroupInfo) {
b.Log.Debugf("Receiving event %#v", event)
switch {
case event.Join != nil:
b.handleUserJoin(event)
case event.Leave != nil:
b.handleUserLeave(event)
case event.Topic != nil:
b.handleTopicChange(event)
}
}
func (b *Bwhatsapp) handleUserJoin(event *events.GroupInfo) {
for _, joinedJid := range event.Join {
senderName := b.getSenderNameFromJID(joinedJid)
rmsg := config.Message{
UserID: joinedJid.String(),
Username: senderName,
Channel: event.JID.String(),
Account: b.Account,
Protocol: b.Protocol,
Event: config.EventJoinLeave,
Text: "joined chat",
}
b.Remote <- rmsg
}
}
func (b *Bwhatsapp) handleUserLeave(event *events.GroupInfo) {
for _, leftJid := range event.Leave {
senderName := b.getSenderNameFromJID(leftJid)
rmsg := config.Message{
UserID: leftJid.String(),
Username: senderName,
Channel: event.JID.String(),
Account: b.Account,
Protocol: b.Protocol,
Event: config.EventJoinLeave,
Text: "left chat",
}
b.Remote <- rmsg
}
}
func (b *Bwhatsapp) handleTopicChange(event *events.GroupInfo) {
msg := event.Topic
senderJid := msg.TopicSetBy
senderName := b.getSenderNameFromJID(senderJid)
text := msg.Topic
if text == "" {
text = "removed topic"
}
rmsg := config.Message{
UserID: senderJid.String(),
Username: senderName,
Channel: event.JID.String(),
Account: b.Account,
Protocol: b.Protocol,
Event: config.EventTopicChange,
Text: "Topic changed: " + text,
}
b.Remote <- rmsg
}
func (b *Bwhatsapp) handleMessage(message *events.Message) {
msg := message.Message
switch {
@@ -30,7 +104,7 @@ func (b *Bwhatsapp) handleMessage(message *events.Message) {
return
}
b.Log.Infof("Receiving message %#v", msg)
b.Log.Debugf("Receiving message %#v", msg)
switch {
case msg.Conversation != nil || msg.ExtendedTextMessage != nil:
@@ -43,6 +117,8 @@ func (b *Bwhatsapp) handleMessage(message *events.Message) {
b.handleDocumentMessage(message)
case msg.ImageMessage != nil:
b.handleImageMessage(message)
case msg.ProtocolMessage != nil && *msg.ProtocolMessage.Type == proto.ProtocolMessage_REVOKE:
b.handleDelete(msg.ProtocolMessage)
}
}
@@ -63,6 +139,10 @@ func (b *Bwhatsapp) handleTextMessage(messageInfo types.MessageInfo, msg *proto.
// nolint:nestif
if msg.GetExtendedTextMessage() == nil {
text = msg.GetConversation()
} else if msg.GetExtendedTextMessage().GetContextInfo() == nil {
// Handle pure text message with a link preview
// A pure text message with a link preview acts as an extended text message but will not contain any context info
text = msg.GetExtendedTextMessage().GetText()
} else {
text = msg.GetExtendedTextMessage().GetText()
ci := msg.GetExtendedTextMessage().GetContextInfo()
@@ -85,6 +165,12 @@ func (b *Bwhatsapp) handleTextMessage(messageInfo types.MessageInfo, msg *proto.
}
}
parentID := ""
if msg.GetExtendedTextMessage() != nil {
ci := msg.GetExtendedTextMessage().GetContextInfo()
parentID = getParentIdFromCtx(ci)
}
rmsg := config.Message{
UserID: senderJID.String(),
Username: senderName,
@@ -93,8 +179,8 @@ func (b *Bwhatsapp) handleTextMessage(messageInfo types.MessageInfo, msg *proto.
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
// ParentID: TODO, // TODO handle thread replies // map from Info.QuotedMessageID string
ID: messageInfo.ID,
ID: getMessageIdFormat(senderJID, messageInfo.ID),
ParentID: parentID,
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
@@ -126,7 +212,8 @@ func (b *Bwhatsapp) handleImageMessage(msg *events.Message) {
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: msg.Info.ID,
ID: getMessageIdFormat(senderJID, msg.Info.ID),
ParentID: getParentIdFromCtx(ci),
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
@@ -189,7 +276,8 @@ func (b *Bwhatsapp) handleVideoMessage(msg *events.Message) {
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: msg.Info.ID,
ID: getMessageIdFormat(senderJID, msg.Info.ID),
ParentID: getParentIdFromCtx(ci),
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
@@ -207,7 +295,16 @@ func (b *Bwhatsapp) handleVideoMessage(msg *events.Message) {
fileExt = append(fileExt, ".mp4")
}
filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[0])
// Prefer .mp4 extension, otherwise fallback to first index
fileExtIndex := 0
for i, n := range fileExt {
if ".mp4" == n {
fileExtIndex = i
break
}
}
filename := fmt.Sprintf("%v%v", msg.Info.ID, fileExt[fileExtIndex])
b.Log.Debugf("Trying to download %s with size %#v and type %s", filename, imsg.GetFileLength(), imsg.GetMimetype())
@@ -238,7 +335,6 @@ func (b *Bwhatsapp) handleAudioMessage(msg *events.Message) {
if senderJID == (types.JID{}) && ci.Participant != nil {
senderJID = types.NewJID(ci.GetParticipant(), types.DefaultUserServer)
}
rmsg := config.Message{
UserID: senderJID.String(),
Username: senderName,
@@ -246,7 +342,8 @@ func (b *Bwhatsapp) handleAudioMessage(msg *events.Message) {
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: msg.Info.ID,
ID: getMessageIdFormat(senderJID, msg.Info.ID),
ParentID: getParentIdFromCtx(ci),
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
@@ -303,7 +400,8 @@ func (b *Bwhatsapp) handleDocumentMessage(msg *events.Message) {
Account: b.Account,
Protocol: b.Protocol,
Extra: make(map[string][]interface{}),
ID: msg.Info.ID,
ID: getMessageIdFormat(senderJID, msg.Info.ID),
ParentID: getParentIdFromCtx(ci),
}
if avatarURL, exists := b.userAvatars[senderJID.String()]; exists {
@@ -336,3 +434,20 @@ func (b *Bwhatsapp) handleDocumentMessage(msg *events.Message) {
b.Remote <- rmsg
}
func (b *Bwhatsapp) handleDelete(messageInfo *proto.ProtocolMessage) {
sender, _ := types.ParseJID(*messageInfo.Key.Participant)
rmsg := config.Message{
Account: b.Account,
Protocol: b.Protocol,
ID: getMessageIdFormat(sender, *messageInfo.Key.Id),
Event: config.EventMsgDelete,
Text: config.EventMsgDelete,
Channel: *messageInfo.Key.RemoteJid,
}
b.Log.Debugf("<= Sending message from %s to gateway", b.Account)
b.Log.Debugf("<= Message is %#v", rmsg)
b.Remote <- rmsg
}

View File

@@ -7,7 +7,10 @@ import (
"fmt"
"strings"
goproto "google.golang.org/protobuf/proto"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/binary/proto"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/store/sqlstore"
"go.mau.fi/whatsmeow/types"
@@ -61,6 +64,29 @@ func (b *Bwhatsapp) getSenderName(info types.MessageInfo) string {
return "Someone"
}
func (b *Bwhatsapp) getSenderNameFromJID(senderJid types.JID) string {
sender, exists := b.contacts[senderJid]
if !exists || (sender.FullName == "" && sender.FirstName == "") {
b.reloadContacts() // Contacts may need to be reloaded
sender, exists = b.contacts[senderJid]
}
if exists && sender.FullName != "" {
return sender.FullName
}
if exists && sender.FirstName != "" {
return sender.FirstName
}
if sender.PushName != "" {
return sender.PushName
}
return "Someone"
}
func (b *Bwhatsapp) getSenderNotify(senderJid types.JID) string {
sender, exists := b.contacts[senderJid]
@@ -122,3 +148,63 @@ func (b *Bwhatsapp) getDevice() (*store.Device, error) {
return device, nil
}
func (b *Bwhatsapp) getNewReplyContext(parentID string) (*proto.ContextInfo, error) {
replyInfo, err := b.parseMessageID(parentID)
if err != nil {
return nil, err
}
sender := fmt.Sprintf("%s@%s", replyInfo.Sender.User, replyInfo.Sender.Server)
ctx := &proto.ContextInfo{
StanzaId: &replyInfo.MessageID,
Participant: &sender,
QuotedMessage: &proto.Message{Conversation: goproto.String("")},
}
return ctx, nil
}
func (b *Bwhatsapp) parseMessageID(id string) (*Replyable, error) {
// No message ID in case action is executed on a message sent before the bridge was started
// and then the bridge cache doesn't have this message ID mapped
if id == "" {
return &Replyable{MessageID: id}, nil
}
replyInfo := strings.Split(id, "/")
if len(replyInfo) == 2 {
sender, err := types.ParseJID(replyInfo[0])
if err == nil {
return &Replyable{
MessageID: types.MessageID(replyInfo[1]),
Sender: sender,
}, nil
}
}
err := fmt.Errorf("MessageID does not match format of {senderJID}:{messageID} : \"%s\"", id)
return &Replyable{MessageID: id}, err
}
func getParentIdFromCtx(ci *proto.ContextInfo) string {
if ci != nil && ci.StanzaId != nil {
senderJid, err := types.ParseJID(*ci.Participant)
if err == nil {
return getMessageIdFormat(senderJid, *ci.StanzaId)
}
}
return ""
}
func getMessageIdFormat(jid types.JID, messageID string) string {
// we're crafting our own JID str as AD JID format messes with how stuff looks on a webclient
jidStr := fmt.Sprintf("%s@%s", jid.User, jid.Server)
return fmt.Sprintf("%s/%s", jidStr, messageID)
}

View File

@@ -35,11 +35,17 @@ const (
type Bwhatsapp struct {
*bridge.Config
startedAt time.Time
wc *whatsmeow.Client
contacts map[types.JID]types.ContactInfo
users map[string]types.ContactInfo
userAvatars map[string]string
startedAt time.Time
wc *whatsmeow.Client
contacts map[types.JID]types.ContactInfo
users map[string]types.ContactInfo
userAvatars map[string]string
joinedGroups []*types.GroupInfo
}
type Replyable struct {
MessageID types.MessageID
Sender types.JID
}
// New Create a new WhatsApp bridge. This will be called for each [whatsapp.<server>] entry you have in the config file
@@ -121,6 +127,11 @@ func (b *Bwhatsapp) Connect() error {
return errors.New("failed to get contacts: " + err.Error())
}
b.joinedGroups, err = b.wc.GetJoinedGroups()
if err != nil {
return errors.New("failed to get list of joined groups: " + err.Error())
}
b.startedAt = time.Now()
// map all the users
@@ -166,11 +177,6 @@ func (b *Bwhatsapp) Disconnect() error {
func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
byJid := isGroupJid(channel.Name)
groups, err := b.wc.GetJoinedGroups()
if err != nil {
return err
}
// verify if we are member of the given group
if byJid {
gJID, err := types.ParseJID(channel.Name)
@@ -178,7 +184,7 @@ func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
return err
}
for _, group := range groups {
for _, group := range b.joinedGroups {
if group.JID == gJID {
return nil
}
@@ -187,7 +193,7 @@ func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
foundGroups := []string{}
for _, group := range groups {
for _, group := range b.joinedGroups {
if group.Name == channel.Name {
foundGroups = append(foundGroups, group.Name)
}
@@ -196,7 +202,7 @@ func (b *Bwhatsapp) JoinChannel(channel config.ChannelInfo) error {
switch len(foundGroups) {
case 0:
// didn't match any group - print out possibilites
for _, group := range groups {
for _, group := range b.joinedGroups {
b.Log.Infof("%s %s", group.JID, group.Name)
}
return fmt.Errorf("please specify group's JID from the list above instead of the name '%s'", channel.Name)
@@ -222,6 +228,10 @@ func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (st
// Post document message
var message proto.Message
var ctx *proto.ContextInfo
if msg.ParentID != "" {
ctx, _ = b.getNewReplyContext(msg.ParentID)
}
message.DocumentMessage = &proto.DocumentMessage{
Title: &fi.Name,
@@ -233,6 +243,8 @@ func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (st
FileSha256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength),
Url: &resp.URL,
DirectPath: &resp.DirectPath,
ContextInfo: ctx,
}
b.Log.Debugf("=> Sending %#v as a document", msg)
@@ -246,8 +258,6 @@ func (b *Bwhatsapp) PostDocumentMessage(msg config.Message, filetype string) (st
// Post an image message from the bridge to WhatsApp
// Handle, for sure image/jpeg, image/png and image/gif MIME types
func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (string, error) {
groupJID, _ := types.ParseJID(msg.Channel)
fi := msg.Extra["file"][0].(config.FileInfo)
caption := msg.Username + fi.Comment
@@ -258,6 +268,10 @@ func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (strin
}
var message proto.Message
var ctx *proto.ContextInfo
if msg.ParentID != "" {
ctx, _ = b.getNewReplyContext(msg.ParentID)
}
message.ImageMessage = &proto.ImageMessage{
Mimetype: &filetype,
@@ -267,20 +281,17 @@ func (b *Bwhatsapp) PostImageMessage(msg config.Message, filetype string) (strin
FileSha256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength),
Url: &resp.URL,
DirectPath: &resp.DirectPath,
ContextInfo: ctx,
}
b.Log.Debugf("=> Sending %#v as an image", msg)
ID := whatsmeow.GenerateMessageID()
_, err = b.wc.SendMessage(context.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID})
return ID, err
return b.sendMessage(msg, &message)
}
// Post a video message from the bridge to WhatsApp
func (b *Bwhatsapp) PostVideoMessage(msg config.Message, filetype string) (string, error) {
groupJID, _ := types.ParseJID(msg.Channel)
fi := msg.Extra["file"][0].(config.FileInfo)
caption := msg.Username + fi.Comment
@@ -291,6 +302,10 @@ func (b *Bwhatsapp) PostVideoMessage(msg config.Message, filetype string) (strin
}
var message proto.Message
var ctx *proto.ContextInfo
if msg.ParentID != "" {
ctx, _ = b.getNewReplyContext(msg.ParentID)
}
message.VideoMessage = &proto.VideoMessage{
Mimetype: &filetype,
@@ -300,14 +315,13 @@ func (b *Bwhatsapp) PostVideoMessage(msg config.Message, filetype string) (strin
FileSha256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength),
Url: &resp.URL,
DirectPath: &resp.DirectPath,
ContextInfo: ctx,
}
b.Log.Debugf("=> Sending %#v as a video", msg)
ID := whatsmeow.GenerateMessageID()
_, err = b.wc.SendMessage(context.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID})
return ID, err
return b.sendMessage(msg, &message)
}
// Post audio inline
@@ -322,6 +336,10 @@ func (b *Bwhatsapp) PostAudioMessage(msg config.Message, filetype string) (strin
}
var message proto.Message
var ctx *proto.ContextInfo
if msg.ParentID != "" {
ctx, _ = b.getNewReplyContext(msg.ParentID)
}
message.AudioMessage = &proto.AudioMessage{
Mimetype: &filetype,
@@ -330,12 +348,13 @@ func (b *Bwhatsapp) PostAudioMessage(msg config.Message, filetype string) (strin
FileSha256: resp.FileSHA256,
FileLength: goproto.Uint64(resp.FileLength),
Url: &resp.URL,
DirectPath: &resp.DirectPath,
ContextInfo: ctx,
}
b.Log.Debugf("=> Sending %#v as audio", msg)
ID := whatsmeow.GenerateMessageID()
_, err = b.wc.SendMessage(context.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID})
ID, err := b.sendMessage(msg, &message)
var captionMessage proto.Message
caption := msg.Username + fi.Comment + "\u2B06" // the char on the end is upwards arrow emoji
@@ -351,6 +370,9 @@ func (b *Bwhatsapp) PostAudioMessage(msg config.Message, filetype string) (strin
func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
groupJID, _ := types.ParseJID(msg.Channel)
extendedMsgID, _ := b.parseMessageID(msg.ID)
msg.ID = extendedMsgID.MessageID
b.Log.Debugf("=> Receiving %#v", msg)
// Delete message
@@ -400,14 +422,35 @@ func (b *Bwhatsapp) Send(msg config.Message) (string, error) {
}
}
var message proto.Message
text := msg.Username + msg.Text
var message proto.Message
// If we have a parent ID send an extended message
if msg.ParentID != "" {
replyContext, err := b.getNewReplyContext(msg.ParentID)
if err == nil {
message = proto.Message{
ExtendedTextMessage: &proto.ExtendedTextMessage{
Text: &text,
ContextInfo: replyContext,
},
}
return b.sendMessage(msg, &message)
}
}
message.Conversation = &text
ID := whatsmeow.GenerateMessageID()
_, err := b.wc.SendMessage(context.TODO(), groupJID, &message, whatsmeow.SendRequestExtra{ID: ID})
return ID, err
return b.sendMessage(msg, &message)
}
func (b *Bwhatsapp) sendMessage(rmsg config.Message, message *proto.Message) (string, error) {
groupJID, _ := types.ParseJID(rmsg.Channel)
ID := whatsmeow.GenerateMessageID()
_, err := b.wc.SendMessage(context.Background(), groupJID, message, whatsmeow.SendRequestExtra{ID: ID})
return getMessageIdFormat(*b.wc.Store.ID, ID), err
}