Update mautrix-go

This commit is contained in:
Tulir Asokan 2020-05-08 22:32:22 +03:00
parent e0aea74abf
commit acc25a02e4
20 changed files with 454 additions and 465 deletions

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2019 Tulir Asokan // Copyright (C) 2020 Tulir Asokan
// //
// This program is free software: you can redistribute it and/or modify // This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by // it under the terms of the GNU Affero General Public License as published by
@ -26,10 +26,11 @@ import (
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix-appservice" "maunium.net/go/mautrix-appservice"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/database" "maunium.net/go/mautrix-whatsapp/database"
"maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix-whatsapp/whatsapp-ext" "maunium.net/go/mautrix-whatsapp/whatsapp-ext"
) )
@ -51,7 +52,7 @@ type CommandEvent struct {
Bot *appservice.IntentAPI Bot *appservice.IntentAPI
Bridge *Bridge Bridge *Bridge
Handler *CommandHandler Handler *CommandHandler
RoomID types.MatrixRoomID RoomID id.RoomID
User *User User *User
Command string Command string
Args []string Args []string
@ -59,20 +60,20 @@ type CommandEvent struct {
// Reply sends a reply to command as notice // Reply sends a reply to command as notice
func (ce *CommandEvent) Reply(msg string, args ...interface{}) { func (ce *CommandEvent) Reply(msg string, args ...interface{}) {
content := format.RenderMarkdown(fmt.Sprintf(msg, args...)) content := format.RenderMarkdown(fmt.Sprintf(msg, args...), true, false)
content.MsgType = mautrix.MsgNotice content.MsgType = event.MsgNotice
room := ce.User.ManagementRoom room := ce.User.ManagementRoom
if len(room) == 0 { if len(room) == 0 {
room = ce.RoomID room = ce.RoomID
} }
_, err := ce.Bot.SendMessageEvent(room, mautrix.EventMessage, content) _, err := ce.Bot.SendMessageEvent(room, event.EventMessage, content)
if err != nil { if err != nil {
ce.Handler.log.Warnfln("Failed to reply to command from %s: %v", ce.User.MXID, err) ce.Handler.log.Warnfln("Failed to reply to command from %s: %v", ce.User.MXID, err)
} }
} }
// Handle handles messages to the bridge // Handle handles messages to the bridge
func (handler *CommandHandler) Handle(roomID types.MatrixRoomID, user *User, message string) { func (handler *CommandHandler) Handle(roomID id.RoomID, user *User, message string) {
args := strings.Split(message, " ") args := strings.Split(message, " ")
ce := &CommandEvent{ ce := &CommandEvent{
Bot: handler.bridge.Bot, Bot: handler.bridge.Bot,

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2019 Tulir Asokan // Copyright (C) 2020 Tulir Asokan
// //
// This program is free software: you can redistribute it and/or modify // This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by // it under the terms of the GNU Affero General Public License as published by
@ -21,7 +21,6 @@ import (
"net/http" "net/http"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
appservice "maunium.net/go/mautrix-appservice"
) )
func (user *User) inviteToCommunity() { func (user *User) inviteToCommunity() {
@ -51,7 +50,7 @@ func (user *User) createCommunity() {
return return
} }
localpart, server := appservice.ParseUserID(user.MXID) localpart, server, _ := user.MXID.Parse()
community := user.bridge.Config.Bridge.FormatCommunity(localpart, server) community := user.bridge.Config.Bridge.FormatCommunity(localpart, server)
user.log.Debugln("Creating personal filtering community", community) user.log.Debugln("Creating personal filtering community", community)
bot := user.bridge.Bot bot := user.bridge.Bot
@ -100,8 +99,8 @@ func (user *User) addPuppetToCommunity(puppet *Puppet) bool {
"type": "private", "type": "private",
}, },
} }
url = bot.BuildURLWithQuery([]string{"groups", user.CommunityID, "self", "accept_invite"}, map[string]string{ url = bot.BuildURLWithQuery(mautrix.URLPath{"groups", user.CommunityID, "self", "accept_invite"}, map[string]string{
"user_id": puppet.MXID, "user_id": puppet.MXID.String(),
}) })
_, err = bot.MakeRequest(http.MethodPut, url, &reqBody, nil) _, err = bot.MakeRequest(http.MethodPut, url, &reqBody, nil)
if err != nil { if err != nil {

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2019 Tulir Asokan // Copyright (C) 2020 Tulir Asokan
// //
// This program is free software: you can redistribute it and/or modify // This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by // it under the terms of the GNU Affero General Public License as published by
@ -24,8 +24,8 @@ import (
"github.com/Rhymen/go-whatsapp" "github.com/Rhymen/go-whatsapp"
"maunium.net/go/mautrix" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix-appservice" "maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix-whatsapp/types"
) )
@ -54,8 +54,8 @@ type BridgeConfig struct {
RecoverHistory bool `yaml:"recovery_history_backfill"` RecoverHistory bool `yaml:"recovery_history_backfill"`
SyncChatMaxAge uint64 `yaml:"sync_max_chat_age"` SyncChatMaxAge uint64 `yaml:"sync_max_chat_age"`
SyncWithCustomPuppets bool `yaml:"sync_with_custom_puppets"` SyncWithCustomPuppets bool `yaml:"sync_with_custom_puppets"`
LoginSharedSecret string `yaml:"login_shared_secret"` LoginSharedSecret string `yaml:"login_shared_secret"`
InviteOwnPuppetForBackfilling bool `yaml:"invite_own_puppet_for_backfilling"` InviteOwnPuppetForBackfilling bool `yaml:"invite_own_puppet_for_backfilling"`
PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"` PrivateChatPortalMeta bool `yaml:"private_chat_portal_meta"`
@ -127,7 +127,7 @@ func (bc *BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
} }
type UsernameTemplateArgs struct { type UsernameTemplateArgs struct {
UserID string UserID id.UserID
} }
func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) (string, int8) { func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) (string, int8) {
@ -232,25 +232,25 @@ func (pc *PermissionConfig) MarshalYAML() (interface{}, error) {
return rawPC, nil return rawPC, nil
} }
func (pc PermissionConfig) IsRelaybotWhitelisted(userID string) bool { func (pc PermissionConfig) IsRelaybotWhitelisted(userID id.UserID) bool {
return pc.GetPermissionLevel(userID) >= PermissionLevelRelaybot return pc.GetPermissionLevel(userID) >= PermissionLevelRelaybot
} }
func (pc PermissionConfig) IsWhitelisted(userID string) bool { func (pc PermissionConfig) IsWhitelisted(userID id.UserID) bool {
return pc.GetPermissionLevel(userID) >= PermissionLevelUser return pc.GetPermissionLevel(userID) >= PermissionLevelUser
} }
func (pc PermissionConfig) IsAdmin(userID string) bool { func (pc PermissionConfig) IsAdmin(userID id.UserID) bool {
return pc.GetPermissionLevel(userID) >= PermissionLevelAdmin return pc.GetPermissionLevel(userID) >= PermissionLevelAdmin
} }
func (pc PermissionConfig) GetPermissionLevel(userID string) PermissionLevel { func (pc PermissionConfig) GetPermissionLevel(userID id.UserID) PermissionLevel {
permissions, ok := pc[userID] permissions, ok := pc[string(userID)]
if ok { if ok {
return permissions return permissions
} }
_, homeserver := appservice.ParseUserID(userID) _, homeserver, _ := userID.Parse()
permissions, ok = pc[homeserver] permissions, ok = pc[homeserver]
if len(homeserver) > 0 && ok { if len(homeserver) > 0 && ok {
return permissions return permissions
@ -265,12 +265,12 @@ func (pc PermissionConfig) GetPermissionLevel(userID string) PermissionLevel {
} }
type RelaybotConfig struct { type RelaybotConfig struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
ManagementRoom string `yaml:"management"` ManagementRoom id.RoomID `yaml:"management"`
InviteUsers []types.MatrixUserID `yaml:"invites"` InviteUsers []id.UserID `yaml:"invites"`
MessageFormats map[mautrix.MessageType]string `yaml:"message_formats"` MessageFormats map[event.MessageType]string `yaml:"message_formats"`
messageTemplates *template.Template `yaml:"-"` messageTemplates *template.Template `yaml:"-"`
} }
type umRelaybotConfig RelaybotConfig type umRelaybotConfig RelaybotConfig
@ -293,25 +293,25 @@ func (rc *RelaybotConfig) UnmarshalYAML(unmarshal func(interface{}) error) error
} }
type Sender struct { type Sender struct {
UserID types.MatrixUserID UserID id.UserID
mautrix.Member *event.MemberEventContent
} }
type formatData struct { type formatData struct {
Sender Sender Sender Sender
Message string Message string
Content mautrix.Content Content *event.MessageEventContent
} }
func (rc *RelaybotConfig) FormatMessage(evt *mautrix.Event, member mautrix.Member) (string, error) { func (rc *RelaybotConfig) FormatMessage(content *event.MessageEventContent, sender id.UserID, member *event.MemberEventContent) (string, error) {
var output strings.Builder var output strings.Builder
err := rc.messageTemplates.ExecuteTemplate(&output, string(evt.Content.MsgType), formatData{ err := rc.messageTemplates.ExecuteTemplate(&output, string(content.MsgType), formatData{
Sender: Sender{ Sender: Sender{
UserID: evt.Sender, UserID: sender,
Member: member, MemberEventContent: member,
}, },
Content: evt.Content, Content: content,
Message: evt.Content.FormattedBody, Message: content.FormattedBody,
}) })
return output.String(), err return output.String(), err
} }

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2019 Tulir Asokan // Copyright (C) 2020 Tulir Asokan
// //
// This program is free software: you can redistribute it and/or modify // This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by // it under the terms of the GNU Affero General Public License as published by
@ -20,17 +20,16 @@ import (
"crypto/hmac" "crypto/hmac"
"crypto/sha512" "crypto/sha512"
"encoding/hex" "encoding/hex"
"encoding/json"
"fmt"
"os"
"strings"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/Rhymen/go-whatsapp" "github.com/Rhymen/go-whatsapp"
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
appservice "maunium.net/go/mautrix-appservice" appservice "maunium.net/go/mautrix-appservice"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
) )
var ( var (
@ -38,7 +37,7 @@ var (
ErrMismatchingMXID = errors.New("whoami result does not match custom mxid") ErrMismatchingMXID = errors.New("whoami result does not match custom mxid")
) )
func (puppet *Puppet) SwitchCustomMXID(accessToken string, mxid string) error { func (puppet *Puppet) SwitchCustomMXID(accessToken string, mxid id.UserID) error {
prevCustomMXID := puppet.CustomMXID prevCustomMXID := puppet.CustomMXID
if puppet.customIntent != nil { if puppet.customIntent != nil {
puppet.stopSyncing() puppet.stopSyncing()
@ -63,12 +62,12 @@ func (puppet *Puppet) SwitchCustomMXID(accessToken string, mxid string) error {
return nil return nil
} }
func (puppet *Puppet) loginWithSharedSecret(mxid string) (string, error) { func (puppet *Puppet) loginWithSharedSecret(mxid id.UserID) (string, error) {
mac := hmac.New(sha512.New, []byte(puppet.bridge.Config.Bridge.LoginSharedSecret)) mac := hmac.New(sha512.New, []byte(puppet.bridge.Config.Bridge.LoginSharedSecret))
mac.Write([]byte(mxid)) mac.Write([]byte(mxid))
resp, err := puppet.bridge.AS.BotClient().Login(&mautrix.ReqLogin{ resp, err := puppet.bridge.AS.BotClient().Login(&mautrix.ReqLogin{
Type: "m.login.password", Type: "m.login.password",
Identifier: mautrix.UserIdentifier{Type: "m.id.user", User: mxid}, Identifier: mautrix.UserIdentifier{Type: "m.id.user", User: string(mxid)},
Password: hex.EncodeToString(mac.Sum(nil)), Password: hex.EncodeToString(mac.Sum(nil)),
DeviceID: "WhatsApp Bridge", DeviceID: "WhatsApp Bridge",
InitialDeviceDisplayName: "WhatsApp Bridge", InitialDeviceDisplayName: "WhatsApp Bridge",
@ -87,13 +86,13 @@ func (puppet *Puppet) newCustomIntent() (*appservice.IntentAPI, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
client.Logger = puppet.bridge.AS.Log.Sub(puppet.CustomMXID) client.Logger = puppet.bridge.AS.Log.Sub(string(puppet.CustomMXID))
client.Syncer = puppet client.Syncer = puppet
client.Store = puppet client.Store = puppet
ia := puppet.bridge.AS.NewIntentAPI("custom") ia := puppet.bridge.AS.NewIntentAPI("custom")
ia.Client = client ia.Client = client
ia.Localpart = puppet.CustomMXID[1:strings.IndexRune(puppet.CustomMXID, ':')] ia.Localpart, _, _ = puppet.CustomMXID.Parse()
ia.UserID = puppet.CustomMXID ia.UserID = puppet.CustomMXID
ia.IsCustomPuppet = true ia.IsCustomPuppet = true
return ia, nil return ia, nil
@ -117,11 +116,7 @@ func (puppet *Puppet) StartCustomMXID() error {
puppet.clearCustomMXID() puppet.clearCustomMXID()
return err return err
} }
urlPath := intent.BuildURL("account", "whoami") resp, err := intent.Whoami()
var resp struct {
UserID string `json:"user_id"`
}
_, err = intent.MakeRequest("GET", urlPath, nil, &resp)
if err != nil { if err != nil {
puppet.clearCustomMXID() puppet.clearCustomMXID()
return err return err
@ -131,7 +126,7 @@ func (puppet *Puppet) StartCustomMXID() error {
return ErrMismatchingMXID return ErrMismatchingMXID
} }
puppet.customIntent = intent puppet.customIntent = intent
puppet.customTypingIn = make(map[string]bool) puppet.customTypingIn = make(map[id.RoomID]bool)
puppet.customUser = puppet.bridge.GetUserByMXID(puppet.CustomMXID) puppet.customUser = puppet.bridge.GetUserByMXID(puppet.CustomMXID)
puppet.startSyncing() puppet.startSyncing()
return nil return nil
@ -158,28 +153,6 @@ func (puppet *Puppet) stopSyncing() {
puppet.customIntent.StopSync() puppet.customIntent.StopSync()
} }
func parseEvent(roomID string, data json.RawMessage) *mautrix.Event {
event := &mautrix.Event{}
err := json.Unmarshal(data, event)
if err != nil {
// TODO add separate handler for these
_, _ = fmt.Fprintf(os.Stderr, "Failed to unmarshal event: %v\n%s\n", err, string(data))
return nil
}
return event
}
func parsePresenceEvent(data json.RawMessage) *mautrix.Event {
event := &mautrix.Event{}
err := json.Unmarshal(data, event)
if err != nil {
// TODO add separate handler for these
_, _ = fmt.Fprintf(os.Stderr, "Failed to unmarshal event: %v\n%s\n", err, string(data))
return nil
}
return event
}
func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, since string) error { func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, since string) error {
if !puppet.customUser.IsConnected() { if !puppet.customUser.IsConnected() {
puppet.log.Debugln("Skipping sync processing: custom user not connected to whatsapp") puppet.log.Debugln("Skipping sync processing: custom user not connected to whatsapp")
@ -190,31 +163,33 @@ func (puppet *Puppet) ProcessResponse(resp *mautrix.RespSync, since string) erro
if portal == nil { if portal == nil {
continue continue
} }
for _, data := range events.Ephemeral.Events { for _, evt := range events.Ephemeral.Events {
event := parseEvent(roomID, data) err := evt.Content.ParseRaw(evt.Type)
if event != nil { if err != nil {
switch event.Type { continue
case mautrix.EphemeralEventReceipt: }
go puppet.handleReceiptEvent(portal, event) switch evt.Type {
case mautrix.EphemeralEventTyping: case event.EphemeralEventReceipt:
go puppet.handleTypingEvent(portal, event) go puppet.handleReceiptEvent(portal, evt)
} case event.EphemeralEventTyping:
go puppet.handleTypingEvent(portal, evt)
} }
} }
} }
for _, data := range resp.Presence.Events { for _, evt := range resp.Presence.Events {
event := parsePresenceEvent(data) if evt.Sender != puppet.CustomMXID {
if event != nil { continue
if event.Sender != puppet.CustomMXID {
continue
}
go puppet.handlePresenceEvent(event)
} }
err := evt.Content.ParseRaw(evt.Type)
if err != nil {
continue
}
go puppet.handlePresenceEvent(evt)
} }
return nil return nil
} }
func (puppet *Puppet) handlePresenceEvent(event *mautrix.Event) { func (puppet *Puppet) handlePresenceEvent(event *event.Event) {
presence := whatsapp.PresenceAvailable presence := whatsapp.PresenceAvailable
if event.Content.Raw["presence"].(string) != "online" { if event.Content.Raw["presence"].(string) != "online" {
presence = whatsapp.PresenceUnavailable presence = whatsapp.PresenceUnavailable
@ -228,13 +203,9 @@ func (puppet *Puppet) handlePresenceEvent(event *mautrix.Event) {
} }
} }
func (puppet *Puppet) handleReceiptEvent(portal *Portal, event *mautrix.Event) { func (puppet *Puppet) handleReceiptEvent(portal *Portal, event *event.Event) {
for eventID, rawReceipts := range event.Content.Raw { for eventID, receipts := range *event.Content.AsReceipt() {
if receipts, ok := rawReceipts.(map[string]interface{}); !ok { if _, ok := receipts.Read[puppet.CustomMXID]; !ok {
continue
} else if readReceipt, ok := receipts["m.read"].(map[string]interface{}); !ok {
continue
} else if _, ok = readReceipt[puppet.CustomMXID].(map[string]interface{}); !ok {
continue continue
} }
message := puppet.bridge.DB.Message.GetByMXID(eventID) message := puppet.bridge.DB.Message.GetByMXID(eventID)
@ -249,16 +220,16 @@ func (puppet *Puppet) handleReceiptEvent(portal *Portal, event *mautrix.Event) {
} }
} }
func (puppet *Puppet) handleTypingEvent(portal *Portal, event *mautrix.Event) { func (puppet *Puppet) handleTypingEvent(portal *Portal, evt *event.Event) {
isTyping := false isTyping := false
for _, userID := range event.Content.TypingUserIDs { for _, userID := range evt.Content.AsTyping().UserIDs {
if userID == puppet.CustomMXID { if userID == puppet.CustomMXID {
isTyping = true isTyping = true
break break
} }
} }
if puppet.customTypingIn[event.RoomID] != isTyping { if puppet.customTypingIn[evt.RoomID] != isTyping {
puppet.customTypingIn[event.RoomID] = isTyping puppet.customTypingIn[evt.RoomID] = isTyping
presence := whatsapp.PresenceComposing presence := whatsapp.PresenceComposing
if !isTyping { if !isTyping {
puppet.customUser.log.Infofln("Marking not typing in %s/%s", portal.Key.JID, portal.MXID) puppet.customUser.log.Infofln("Marking not typing in %s/%s", portal.Key.JID, portal.MXID)
@ -278,36 +249,27 @@ func (puppet *Puppet) OnFailedSync(res *mautrix.RespSync, err error) (time.Durat
return 10 * time.Second, nil return 10 * time.Second, nil
} }
func (puppet *Puppet) GetFilterJSON(_ string) json.RawMessage { func (puppet *Puppet) GetFilterJSON(_ id.UserID) *mautrix.Filter {
mxid, _ := json.Marshal(puppet.CustomMXID) everything := []event.Type{{Type: "*"}}
return json.RawMessage(fmt.Sprintf(`{ return &mautrix.Filter{
"account_data": { "types": [] }, Presence: mautrix.FilterPart{
"presence": { Senders: []id.UserID{puppet.CustomMXID},
"senders": [ Types: []event.Type{event.EphemeralEventPresence},
%s },
], AccountData: mautrix.FilterPart{NotTypes: everything},
"types": [ Room: mautrix.RoomFilter{
"m.presence" Ephemeral: mautrix.FilterPart{Types: []event.Type{event.EphemeralEventTyping, event.EphemeralEventReceipt}},
] IncludeLeave: false,
}, AccountData: mautrix.FilterPart{NotTypes: everything},
"room": { State: mautrix.FilterPart{NotTypes: everything},
"ephemeral": { Timeline: mautrix.FilterPart{NotTypes: everything},
"types": [ },
"m.typing", }
"m.receipt"
]
},
"include_leave": false,
"account_data": { "types": [] },
"state": { "types": [] },
"timeline": { "types": [] }
}
}`, mxid))
} }
func (puppet *Puppet) SaveFilterID(_, _ string) {} func (puppet *Puppet) SaveFilterID(_ id.UserID, _ string) {}
func (puppet *Puppet) SaveNextBatch(_, nbt string) { puppet.NextBatch = nbt; puppet.Update() } func (puppet *Puppet) SaveNextBatch(_ id.UserID, nbt string) { puppet.NextBatch = nbt; puppet.Update() }
func (puppet *Puppet) SaveRoom(room *mautrix.Room) {} func (puppet *Puppet) SaveRoom(room *mautrix.Room) {}
func (puppet *Puppet) LoadFilterID(_ string) string { return "" } func (puppet *Puppet) LoadFilterID(_ id.UserID) string { return "" }
func (puppet *Puppet) LoadNextBatch(_ string) string { return puppet.NextBatch } func (puppet *Puppet) LoadNextBatch(_ id.UserID) string { return puppet.NextBatch }
func (puppet *Puppet) LoadRoom(roomID string) *mautrix.Room { return nil } func (puppet *Puppet) LoadRoom(roomID id.RoomID) *mautrix.Room { return nil }

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2019 Tulir Asokan // Copyright (C) 2020 Tulir Asokan
// //
// This program is free software: you can redistribute it and/or modify // This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by // it under the terms of the GNU Affero General Public License as published by
@ -26,6 +26,7 @@ import (
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix/id"
) )
type MessageQuery struct { type MessageQuery struct {
@ -57,7 +58,7 @@ func (mq *MessageQuery) GetByJID(chat PortalKey, jid types.WhatsAppMessageID) *M
"FROM message WHERE chat_jid=$1 AND chat_receiver=$2 AND jid=$3", chat.JID, chat.Receiver, jid) "FROM message WHERE chat_jid=$1 AND chat_receiver=$2 AND jid=$3", chat.JID, chat.Receiver, jid)
} }
func (mq *MessageQuery) GetByMXID(mxid types.MatrixEventID) *Message { func (mq *MessageQuery) GetByMXID(mxid id.EventID) *Message {
return mq.get("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, content " + return mq.get("SELECT chat_jid, chat_receiver, jid, mxid, sender, timestamp, content " +
"FROM message WHERE mxid=$1", mxid) "FROM message WHERE mxid=$1", mxid)
} }
@ -86,7 +87,7 @@ type Message struct {
Chat PortalKey Chat PortalKey
JID types.WhatsAppMessageID JID types.WhatsAppMessageID
MXID types.MatrixEventID MXID id.EventID
Sender types.WhatsAppID Sender types.WhatsAppID
Timestamp uint64 Timestamp uint64
Content *waProto.Message Content *waProto.Message

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2019 Tulir Asokan // Copyright (C) 2020 Tulir Asokan
// //
// This program is free software: you can redistribute it and/or modify // This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by // it under the terms of the GNU Affero General Public License as published by
@ -23,6 +23,7 @@ import (
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix/id"
) )
type PortalKey struct { type PortalKey struct {
@ -74,7 +75,7 @@ func (pq *PortalQuery) GetByJID(key PortalKey) *Portal {
return pq.get("SELECT * FROM portal WHERE jid=$1 AND receiver=$2", key.JID, key.Receiver) return pq.get("SELECT * FROM portal WHERE jid=$1 AND receiver=$2", key.JID, key.Receiver)
} }
func (pq *PortalQuery) GetByMXID(mxid types.MatrixRoomID) *Portal { func (pq *PortalQuery) GetByMXID(mxid id.RoomID) *Portal {
return pq.get("SELECT * FROM portal WHERE mxid=$1", mxid) return pq.get("SELECT * FROM portal WHERE mxid=$1", mxid)
} }
@ -107,12 +108,12 @@ type Portal struct {
log log.Logger log log.Logger
Key PortalKey Key PortalKey
MXID types.MatrixRoomID MXID id.RoomID
Name string Name string
Topic string Topic string
Avatar string Avatar string
AvatarURL string AvatarURL id.ContentURI
} }
func (portal *Portal) Scan(row Scannable) *Portal { func (portal *Portal) Scan(row Scannable) *Portal {
@ -124,12 +125,12 @@ func (portal *Portal) Scan(row Scannable) *Portal {
} }
return nil return nil
} }
portal.MXID = mxid.String portal.MXID = id.RoomID(mxid.String)
portal.AvatarURL = avatarURL.String portal.AvatarURL, _ = id.ParseContentURI(avatarURL.String)
return portal return portal
} }
func (portal *Portal) mxidPtr() *string { func (portal *Portal) mxidPtr() *id.RoomID {
if len(portal.MXID) > 0 { if len(portal.MXID) > 0 {
return &portal.MXID return &portal.MXID
} }
@ -138,19 +139,19 @@ func (portal *Portal) mxidPtr() *string {
func (portal *Portal) Insert() { func (portal *Portal) Insert() {
_, err := portal.db.Exec("INSERT INTO portal VALUES ($1, $2, $3, $4, $5, $6, $7)", _, err := portal.db.Exec("INSERT INTO portal VALUES ($1, $2, $3, $4, $5, $6, $7)",
portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL) portal.Key.JID, portal.Key.Receiver, portal.mxidPtr(), portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String())
if err != nil { if err != nil {
portal.log.Warnfln("Failed to insert %s: %v", portal.Key, err) portal.log.Warnfln("Failed to insert %s: %v", portal.Key, err)
} }
} }
func (portal *Portal) Update() { func (portal *Portal) Update() {
var mxid *string var mxid *id.RoomID
if len(portal.MXID) > 0 { if len(portal.MXID) > 0 {
mxid = &portal.MXID mxid = &portal.MXID
} }
_, err := portal.db.Exec("UPDATE portal SET mxid=$1, name=$2, topic=$3, avatar=$4, avatar_url=$5 WHERE jid=$6 AND receiver=$7", _, err := portal.db.Exec("UPDATE portal SET mxid=$1, name=$2, topic=$3, avatar=$4, avatar_url=$5 WHERE jid=$6 AND receiver=$7",
mxid, portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL, portal.Key.JID, portal.Key.Receiver) mxid, portal.Name, portal.Topic, portal.Avatar, portal.AvatarURL.String(), portal.Key.JID, portal.Key.Receiver)
if err != nil { if err != nil {
portal.log.Warnfln("Failed to update %s: %v", portal.Key, err) portal.log.Warnfln("Failed to update %s: %v", portal.Key, err)
} }
@ -163,7 +164,7 @@ func (portal *Portal) Delete() {
} }
} }
func (portal *Portal) GetUserIDs() []types.MatrixUserID { func (portal *Portal) GetUserIDs() []id.UserID {
rows, err := portal.db.Query(`SELECT "user".mxid FROM "user", user_portal rows, err := portal.db.Query(`SELECT "user".mxid FROM "user", user_portal
WHERE "user".jid=user_portal.user_jid WHERE "user".jid=user_portal.user_jid
AND user_portal.portal_jid=$1 AND user_portal.portal_jid=$1
@ -173,9 +174,9 @@ func (portal *Portal) GetUserIDs() []types.MatrixUserID {
portal.log.Debugln("Failed to get portal user ids:", err) portal.log.Debugln("Failed to get portal user ids:", err)
return nil return nil
} }
var userIDs []types.MatrixUserID var userIDs []id.UserID
for rows.Next() { for rows.Next() {
var userID types.MatrixUserID var userID id.UserID
err = rows.Scan(&userID) err = rows.Scan(&userID)
if err != nil { if err != nil {
portal.log.Warnln("Failed to scan row:", err) portal.log.Warnln("Failed to scan row:", err)

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2019 Tulir Asokan // Copyright (C) 2020 Tulir Asokan
// //
// This program is free software: you can redistribute it and/or modify // This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by // it under the terms of the GNU Affero General Public License as published by
@ -22,6 +22,7 @@ import (
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix/id"
) )
type PuppetQuery struct { type PuppetQuery struct {
@ -56,7 +57,7 @@ func (pq *PuppetQuery) Get(jid types.WhatsAppID) *Puppet {
return pq.New().Scan(row) return pq.New().Scan(row)
} }
func (pq *PuppetQuery) GetByCustomMXID(mxid types.MatrixUserID) *Puppet { func (pq *PuppetQuery) GetByCustomMXID(mxid id.UserID) *Puppet {
row := pq.db.QueryRow("SELECT jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch FROM puppet WHERE custom_mxid=$1", mxid) row := pq.db.QueryRow("SELECT jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch FROM puppet WHERE custom_mxid=$1", mxid)
if row == nil { if row == nil {
return nil return nil
@ -82,11 +83,11 @@ type Puppet struct {
JID types.WhatsAppID JID types.WhatsAppID
Avatar string Avatar string
AvatarURL string AvatarURL id.ContentURI
Displayname string Displayname string
NameQuality int8 NameQuality int8
CustomMXID string CustomMXID id.UserID
AccessToken string AccessToken string
NextBatch string NextBatch string
} }
@ -103,9 +104,9 @@ func (puppet *Puppet) Scan(row Scannable) *Puppet {
} }
puppet.Displayname = displayname.String puppet.Displayname = displayname.String
puppet.Avatar = avatar.String puppet.Avatar = avatar.String
puppet.AvatarURL = avatarURL.String puppet.AvatarURL, _ = id.ParseContentURI(avatarURL.String)
puppet.NameQuality = int8(quality.Int64) puppet.NameQuality = int8(quality.Int64)
puppet.CustomMXID = customMXID.String puppet.CustomMXID = id.UserID(customMXID.String)
puppet.AccessToken = accessToken.String puppet.AccessToken = accessToken.String
puppet.NextBatch = nextBatch.String puppet.NextBatch = nextBatch.String
return puppet return puppet
@ -113,7 +114,7 @@ func (puppet *Puppet) Scan(row Scannable) *Puppet {
func (puppet *Puppet) Insert() { func (puppet *Puppet) Insert() {
_, err := puppet.db.Exec("INSERT INTO puppet (jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)", _, err := puppet.db.Exec("INSERT INTO puppet (jid, avatar, avatar_url, displayname, name_quality, custom_mxid, access_token, next_batch) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)",
puppet.JID, puppet.Avatar, puppet.AvatarURL, puppet.Displayname, puppet.NameQuality, puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch) puppet.JID, puppet.Avatar, puppet.AvatarURL.String(), puppet.Displayname, puppet.NameQuality, puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch)
if err != nil { if err != nil {
puppet.log.Warnfln("Failed to insert %s: %v", puppet.JID, err) puppet.log.Warnfln("Failed to insert %s: %v", puppet.JID, err)
} }
@ -121,7 +122,7 @@ func (puppet *Puppet) Insert() {
func (puppet *Puppet) Update() { func (puppet *Puppet) Update() {
_, err := puppet.db.Exec("UPDATE puppet SET displayname=$1, name_quality=$2, avatar=$3, avatar_url=$4, custom_mxid=$5, access_token=$6, next_batch=$7 WHERE jid=$8", _, err := puppet.db.Exec("UPDATE puppet SET displayname=$1, name_quality=$2, avatar=$3, avatar_url=$4, custom_mxid=$5, access_token=$6, next_batch=$7 WHERE jid=$8",
puppet.Displayname, puppet.NameQuality, puppet.Avatar, puppet.AvatarURL, puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch, puppet.JID) puppet.Displayname, puppet.NameQuality, puppet.Avatar, puppet.AvatarURL.String(), puppet.CustomMXID, puppet.AccessToken, puppet.NextBatch, puppet.JID)
if err != nil { if err != nil {
puppet.log.Warnfln("Failed to update %s->%s: %v", puppet.JID, err) puppet.log.Warnfln("Failed to update %s->%s: %v", puppet.JID, err)
} }

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2019 Tulir Asokan // Copyright (C) 2020 Tulir Asokan
// //
// This program is free software: you can redistribute it and/or modify // This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by // it under the terms of the GNU Affero General Public License as published by
@ -24,8 +24,9 @@ import (
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix-appservice" "maunium.net/go/mautrix-appservice"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
) )
type SQLStateStore struct { type SQLStateStore struct {
@ -34,7 +35,7 @@ type SQLStateStore struct {
db *Database db *Database
log log.Logger log log.Logger
Typing map[string]map[string]int64 Typing map[id.RoomID]map[id.UserID]int64
typingLock sync.RWMutex typingLock sync.RWMutex
} }
@ -46,7 +47,7 @@ func NewSQLStateStore(db *Database) *SQLStateStore {
} }
} }
func (store *SQLStateStore) IsRegistered(userID string) bool { func (store *SQLStateStore) IsRegistered(userID id.UserID) bool {
row := store.db.QueryRow("SELECT EXISTS(SELECT 1 FROM mx_registrations WHERE user_id=$1)", userID) row := store.db.QueryRow("SELECT EXISTS(SELECT 1 FROM mx_registrations WHERE user_id=$1)", userID)
var isRegistered bool var isRegistered bool
err := row.Scan(&isRegistered) err := row.Scan(&isRegistered)
@ -56,7 +57,7 @@ func (store *SQLStateStore) IsRegistered(userID string) bool {
return isRegistered return isRegistered
} }
func (store *SQLStateStore) MarkRegistered(userID string) { func (store *SQLStateStore) MarkRegistered(userID id.UserID) {
var err error var err error
if store.db.dialect == "postgres" { if store.db.dialect == "postgres" {
_, err = store.db.Exec("INSERT INTO mx_registrations (user_id) VALUES ($1) ON CONFLICT (user_id) DO NOTHING", userID) _, err = store.db.Exec("INSERT INTO mx_registrations (user_id) VALUES ($1) ON CONFLICT (user_id) DO NOTHING", userID)
@ -70,28 +71,28 @@ func (store *SQLStateStore) MarkRegistered(userID string) {
} }
} }
func (store *SQLStateStore) GetRoomMembers(roomID string) map[string]mautrix.Member { func (store *SQLStateStore) GetRoomMembers(roomID id.RoomID) map[id.UserID]*event.MemberEventContent {
members := make(map[string]mautrix.Member) members := make(map[id.UserID]*event.MemberEventContent)
rows, err := store.db.Query("SELECT user_id, membership, displayname, avatar_url FROM mx_user_profile WHERE room_id=$1", roomID) rows, err := store.db.Query("SELECT user_id, membership, displayname, avatar_url FROM mx_user_profile WHERE room_id=$1", roomID)
if err != nil { if err != nil {
return members return members
} }
var userID string var userID id.UserID
var member mautrix.Member var member event.MemberEventContent
for rows.Next() { for rows.Next() {
err := rows.Scan(&userID, &member.Membership, &member.Displayname, &member.AvatarURL) err := rows.Scan(&userID, &member.Membership, &member.Displayname, &member.AvatarURL)
if err != nil { if err != nil {
store.log.Warnfln("Failed to scan member in %s: %v", roomID, err) store.log.Warnfln("Failed to scan member in %s: %v", roomID, err)
} else { } else {
members[userID] = member members[userID] = &member
} }
} }
return members return members
} }
func (store *SQLStateStore) GetMembership(roomID, userID string) mautrix.Membership { func (store *SQLStateStore) GetMembership(roomID id.RoomID, userID id.UserID) event.Membership {
row := store.db.QueryRow("SELECT membership FROM mx_user_profile WHERE room_id=$1 AND user_id=$2", roomID, userID) row := store.db.QueryRow("SELECT membership FROM mx_user_profile WHERE room_id=$1 AND user_id=$2", roomID, userID)
membership := mautrix.MembershipLeave membership := event.MembershipLeave
err := row.Scan(&membership) err := row.Scan(&membership)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
store.log.Warnfln("Failed to scan membership of %s in %s: %v", userID, roomID, err) store.log.Warnfln("Failed to scan membership of %s in %s: %v", userID, roomID, err)
@ -99,33 +100,33 @@ func (store *SQLStateStore) GetMembership(roomID, userID string) mautrix.Members
return membership return membership
} }
func (store *SQLStateStore) GetMember(roomID, userID string) mautrix.Member { func (store *SQLStateStore) GetMember(roomID id.RoomID, userID id.UserID) *event.MemberEventContent {
member, ok := store.TryGetMember(roomID, userID) member, ok := store.TryGetMember(roomID, userID)
if !ok { if !ok {
member.Membership = mautrix.MembershipLeave member.Membership = event.MembershipLeave
} }
return member return member
} }
func (store *SQLStateStore) TryGetMember(roomID, userID string) (mautrix.Member, bool) { func (store *SQLStateStore) TryGetMember(roomID id.RoomID, userID id.UserID) (*event.MemberEventContent, bool) {
row := store.db.QueryRow("SELECT membership, displayname, avatar_url FROM mx_user_profile WHERE room_id=$1 AND user_id=$2", roomID, userID) row := store.db.QueryRow("SELECT membership, displayname, avatar_url FROM mx_user_profile WHERE room_id=$1 AND user_id=$2", roomID, userID)
var member mautrix.Member var member event.MemberEventContent
err := row.Scan(&member.Membership, &member.Displayname, &member.AvatarURL) err := row.Scan(&member.Membership, &member.Displayname, &member.AvatarURL)
if err != nil && err != sql.ErrNoRows { if err != nil && err != sql.ErrNoRows {
store.log.Warnfln("Failed to scan member info of %s in %s: %v", userID, roomID, err) store.log.Warnfln("Failed to scan member info of %s in %s: %v", userID, roomID, err)
} }
return member, err == nil return &member, err == nil
} }
func (store *SQLStateStore) IsInRoom(roomID, userID string) bool { func (store *SQLStateStore) IsInRoom(roomID id.RoomID, userID id.UserID) bool {
return store.IsMembership(roomID, userID, "join") return store.IsMembership(roomID, userID, "join")
} }
func (store *SQLStateStore) IsInvited(roomID, userID string) bool { func (store *SQLStateStore) IsInvited(roomID id.RoomID, userID id.UserID) bool {
return store.IsMembership(roomID, userID, "join", "invite") return store.IsMembership(roomID, userID, "join", "invite")
} }
func (store *SQLStateStore) IsMembership(roomID, userID string, allowedMemberships ...mautrix.Membership) bool { func (store *SQLStateStore) IsMembership(roomID id.RoomID, userID id.UserID, allowedMemberships ...event.Membership) bool {
membership := store.GetMembership(roomID, userID) membership := store.GetMembership(roomID, userID)
for _, allowedMembership := range allowedMemberships { for _, allowedMembership := range allowedMemberships {
if allowedMembership == membership { if allowedMembership == membership {
@ -135,7 +136,7 @@ func (store *SQLStateStore) IsMembership(roomID, userID string, allowedMembershi
return false return false
} }
func (store *SQLStateStore) SetMembership(roomID, userID string, membership mautrix.Membership) { func (store *SQLStateStore) SetMembership(roomID id.RoomID, userID id.UserID, membership event.Membership) {
var err error var err error
if store.db.dialect == "postgres" { if store.db.dialect == "postgres" {
_, err = store.db.Exec(`INSERT INTO mx_user_profile (room_id, user_id, membership) VALUES ($1, $2, $3) _, err = store.db.Exec(`INSERT INTO mx_user_profile (room_id, user_id, membership) VALUES ($1, $2, $3)
@ -150,7 +151,7 @@ func (store *SQLStateStore) SetMembership(roomID, userID string, membership maut
} }
} }
func (store *SQLStateStore) SetMember(roomID, userID string, member mautrix.Member) { func (store *SQLStateStore) SetMember(roomID id.RoomID, userID id.UserID, member *event.MemberEventContent) {
var err error var err error
if store.db.dialect == "postgres" { if store.db.dialect == "postgres" {
_, err = store.db.Exec(`INSERT INTO mx_user_profile (room_id, user_id, membership, displayname, avatar_url) VALUES ($1, $2, $3, $4, $5) _, err = store.db.Exec(`INSERT INTO mx_user_profile (room_id, user_id, membership, displayname, avatar_url) VALUES ($1, $2, $3, $4, $5)
@ -166,7 +167,7 @@ func (store *SQLStateStore) SetMember(roomID, userID string, member mautrix.Memb
} }
} }
func (store *SQLStateStore) SetPowerLevels(roomID string, levels *mautrix.PowerLevels) { func (store *SQLStateStore) SetPowerLevels(roomID id.RoomID, levels *event.PowerLevelsEventContent) {
levelsBytes, err := json.Marshal(levels) levelsBytes, err := json.Marshal(levels)
if err != nil { if err != nil {
store.log.Errorfln("Failed to marshal power levels of %s: %v", roomID, err) store.log.Errorfln("Failed to marshal power levels of %s: %v", roomID, err)
@ -185,7 +186,7 @@ func (store *SQLStateStore) SetPowerLevels(roomID string, levels *mautrix.PowerL
} }
} }
func (store *SQLStateStore) GetPowerLevels(roomID string) (levels *mautrix.PowerLevels) { func (store *SQLStateStore) GetPowerLevels(roomID id.RoomID) (levels *event.PowerLevelsEventContent) {
row := store.db.QueryRow("SELECT power_levels FROM mx_room_state WHERE room_id=$1", roomID) row := store.db.QueryRow("SELECT power_levels FROM mx_room_state WHERE room_id=$1", roomID)
if row == nil { if row == nil {
return return
@ -196,7 +197,7 @@ func (store *SQLStateStore) GetPowerLevels(roomID string) (levels *mautrix.Power
store.log.Errorln("Failed to scan power levels of %s: %v", roomID, err) store.log.Errorln("Failed to scan power levels of %s: %v", roomID, err)
return return
} }
levels = &mautrix.PowerLevels{} levels = &event.PowerLevelsEventContent{}
err = json.Unmarshal(data, levels) err = json.Unmarshal(data, levels)
if err != nil { if err != nil {
store.log.Errorln("Failed to parse power levels of %s: %v", roomID, err) store.log.Errorln("Failed to parse power levels of %s: %v", roomID, err)
@ -205,7 +206,7 @@ func (store *SQLStateStore) GetPowerLevels(roomID string) (levels *mautrix.Power
return return
} }
func (store *SQLStateStore) GetPowerLevel(roomID, userID string) int { func (store *SQLStateStore) GetPowerLevel(roomID id.RoomID, userID id.UserID) int {
if store.db.dialect == "postgres" { if store.db.dialect == "postgres" {
row := store.db.QueryRow(`SELECT row := store.db.QueryRow(`SELECT
COALESCE((power_levels->'users'->$2)::int, (power_levels->'users_default')::int, 0) COALESCE((power_levels->'users'->$2)::int, (power_levels->'users_default')::int, 0)
@ -224,7 +225,7 @@ func (store *SQLStateStore) GetPowerLevel(roomID, userID string) int {
return store.GetPowerLevels(roomID).GetUserLevel(userID) return store.GetPowerLevels(roomID).GetUserLevel(userID)
} }
func (store *SQLStateStore) GetPowerLevelRequirement(roomID string, eventType mautrix.EventType) int { func (store *SQLStateStore) GetPowerLevelRequirement(roomID id.RoomID, eventType event.Type) int {
if store.db.dialect == "postgres" { if store.db.dialect == "postgres" {
defaultType := "events_default" defaultType := "events_default"
defaultValue := 0 defaultValue := 0
@ -249,7 +250,7 @@ func (store *SQLStateStore) GetPowerLevelRequirement(roomID string, eventType ma
return store.GetPowerLevels(roomID).GetEventLevel(eventType) return store.GetPowerLevels(roomID).GetEventLevel(eventType)
} }
func (store *SQLStateStore) HasPowerLevel(roomID, userID string, eventType mautrix.EventType) bool { func (store *SQLStateStore) HasPowerLevel(roomID id.RoomID, userID id.UserID, eventType event.Type) bool {
if store.db.dialect == "postgres" { if store.db.dialect == "postgres" {
defaultType := "events_default" defaultType := "events_default"
defaultValue := 0 defaultValue := 0

View file

@ -8,7 +8,7 @@ import (
"os" "os"
"strings" "strings"
"maunium.net/go/mautrix" "maunium.net/go/mautrix/event"
) )
func init() { func init() {
@ -46,7 +46,7 @@ func init() {
return executeBatch(tx, valueStrings, values...) return executeBatch(tx, valueStrings, values...)
} }
migrateMemberships := func(tx *sql.Tx, rooms map[string]map[string]mautrix.Membership) error { migrateMemberships := func(tx *sql.Tx, rooms map[string]map[string]event.Membership) error {
for roomID, members := range rooms { for roomID, members := range rooms {
if len(members) == 0 { if len(members) == 0 {
continue continue
@ -68,7 +68,7 @@ func init() {
return nil return nil
} }
migratePowerLevels := func(tx *sql.Tx, rooms map[string]*mautrix.PowerLevels) error { migratePowerLevels := func(tx *sql.Tx, rooms map[string]*event.PowerLevelsEventContent) error {
if len(rooms) == 0 { if len(rooms) == 0 {
return nil return nil
} }
@ -106,9 +106,9 @@ func init() {
)` )`
type TempStateStore struct { type TempStateStore struct {
Registrations map[string]bool `json:"registrations"` Registrations map[string]bool `json:"registrations"`
Members map[string]map[string]mautrix.Membership `json:"memberships"` Members map[string]map[string]event.Membership `json:"memberships"`
PowerLevels map[string]*mautrix.PowerLevels `json:"power_levels"` PowerLevels map[string]*event.PowerLevelsEventContent `json:"power_levels"`
} }
upgrades[9] = upgrade{"Move state store to main DB", func(tx *sql.Tx, ctx context) error { upgrades[9] = upgrade{"Move state store to main DB", func(tx *sql.Tx, ctx context) error {

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2019 Tulir Asokan // Copyright (C) 2020 Tulir Asokan
// //
// This program is free software: you can redistribute it and/or modify // This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by // it under the terms of the GNU Affero General Public License as published by
@ -28,6 +28,7 @@ import (
"maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix-whatsapp/whatsapp-ext" "maunium.net/go/mautrix-whatsapp/whatsapp-ext"
"maunium.net/go/mautrix/id"
) )
type UserQuery struct { type UserQuery struct {
@ -54,7 +55,7 @@ func (uq *UserQuery) GetAll() (users []*User) {
return return
} }
func (uq *UserQuery) GetByMXID(userID types.MatrixUserID) *User { func (uq *UserQuery) GetByMXID(userID id.UserID) *User {
row := uq.db.QueryRow(`SELECT mxid, jid, management_room, last_connection, client_id, client_token, server_token, enc_key, mac_key FROM "user" WHERE mxid=$1`, userID) row := uq.db.QueryRow(`SELECT mxid, jid, management_room, last_connection, client_id, client_token, server_token, enc_key, mac_key FROM "user" WHERE mxid=$1`, userID)
if row == nil { if row == nil {
return nil return nil
@ -74,9 +75,9 @@ type User struct {
db *Database db *Database
log log.Logger log log.Logger
MXID types.MatrixUserID MXID id.UserID
JID types.WhatsAppID JID types.WhatsAppID
ManagementRoom types.MatrixRoomID ManagementRoom id.RoomID
Session *whatsapp.Session Session *whatsapp.Session
LastConnection uint64 LastConnection uint64
} }

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2019 Tulir Asokan // Copyright (C) 2020 Tulir Asokan
// //
// This program is free software: you can redistribute it and/or modify // This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by // it under the terms of the GNU Affero General Public License as published by
@ -22,8 +22,9 @@ import (
"regexp" "regexp"
"strings" "strings"
"maunium.net/go/mautrix" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix-whatsapp/whatsapp-ext" "maunium.net/go/mautrix-whatsapp/whatsapp-ext"
@ -54,8 +55,7 @@ func NewFormatter(bridge *Bridge) *Formatter {
PillConverter: func(mxid, eventID string) string { PillConverter: func(mxid, eventID string) string {
if mxid[0] == '@' { if mxid[0] == '@' {
puppet := bridge.GetPuppetByMXID(mxid) puppet := bridge.GetPuppetByMXID(id.UserID(mxid))
fmt.Println(mxid, puppet)
if puppet != nil { if puppet != nil {
return "@" + puppet.PhoneNumber() return "@" + puppet.PhoneNumber()
} }
@ -106,10 +106,10 @@ func NewFormatter(bridge *Bridge) *Formatter {
return formatter return formatter
} }
func (formatter *Formatter) getMatrixInfoByJID(jid types.WhatsAppID) (mxid, displayname string) { func (formatter *Formatter) getMatrixInfoByJID(jid types.WhatsAppID) (mxid id.UserID, displayname string) {
if user := formatter.bridge.GetUserByJID(jid); user != nil { if user := formatter.bridge.GetUserByJID(jid); user != nil {
mxid = user.MXID mxid = user.MXID
displayname = user.MXID displayname = string(user.MXID)
} else if puppet := formatter.bridge.GetPuppetByJID(jid); puppet != nil { } else if puppet := formatter.bridge.GetPuppetByJID(jid); puppet != nil {
mxid = puppet.MXID mxid = puppet.MXID
displayname = puppet.Displayname displayname = puppet.Displayname
@ -117,7 +117,7 @@ func (formatter *Formatter) getMatrixInfoByJID(jid types.WhatsAppID) (mxid, disp
return return
} }
func (formatter *Formatter) ParseWhatsApp(content *mautrix.Content) { func (formatter *Formatter) ParseWhatsApp(content *event.MessageEventContent) {
output := html.EscapeString(content.Body) output := html.EscapeString(content.Body)
for regex, replacement := range formatter.waReplString { for regex, replacement := range formatter.waReplString {
output = regex.ReplaceAllString(output, replacement) output = regex.ReplaceAllString(output, replacement)
@ -128,7 +128,7 @@ func (formatter *Formatter) ParseWhatsApp(content *mautrix.Content) {
if output != content.Body { if output != content.Body {
output = strings.Replace(output, "\n", "<br/>", -1) output = strings.Replace(output, "\n", "<br/>", -1)
content.FormattedBody = output content.FormattedBody = output
content.Format = mautrix.FormatHTML content.Format = event.FormatHTML
for regex, replacer := range formatter.waReplFuncText { for regex, replacer := range formatter.waReplFuncText {
content.Body = regex.ReplaceAllStringFunc(content.Body, replacer) content.Body = regex.ReplaceAllStringFunc(content.Body, replacer)
} }

8
go.mod
View file

@ -5,8 +5,8 @@ go 1.14
require ( require (
github.com/Rhymen/go-whatsapp v0.1.0 github.com/Rhymen/go-whatsapp v0.1.0
github.com/chai2010/webp v1.1.0 github.com/chai2010/webp v1.1.0
github.com/gorilla/websocket v1.4.1 github.com/gorilla/websocket v1.4.2
github.com/lib/pq v1.3.0 github.com/lib/pq v1.5.2
github.com/mattn/go-sqlite3 v2.0.3+incompatible github.com/mattn/go-sqlite3 v2.0.3+incompatible
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
@ -14,8 +14,8 @@ require (
gopkg.in/yaml.v2 v2.2.8 gopkg.in/yaml.v2 v2.2.8
maunium.net/go/mauflag v1.0.0 maunium.net/go/mauflag v1.0.0
maunium.net/go/maulogger/v2 v2.1.1 maunium.net/go/maulogger/v2 v2.1.1
maunium.net/go/mautrix v0.1.0-beta.2 maunium.net/go/mautrix v0.3.6
maunium.net/go/mautrix-appservice v0.1.0-alpha.6 maunium.net/go/mautrix-appservice v0.2.0
) )
replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.2.6 replace github.com/Rhymen/go-whatsapp => github.com/tulir/go-whatsapp v0.2.6

22
go.sum
View file

@ -1,7 +1,10 @@
github.com/chai2010/webp v1.1.0 h1:4Ei0/BRroMF9FaXDG2e4OxwFcuW2vcXd+A6tyqTJUQQ= github.com/chai2010/webp v1.1.0 h1:4Ei0/BRroMF9FaXDG2e4OxwFcuW2vcXd+A6tyqTJUQQ=
github.com/chai2010/webp v1.1.0/go.mod h1:LP12PG5IFmLGHUU26tBiCBKnghxx3toZFwDjOYvd3Ow= github.com/chai2010/webp v1.1.0/go.mod h1:LP12PG5IFmLGHUU26tBiCBKnghxx3toZFwDjOYvd3Ow=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc= github.com/gorilla/mux v1.7.4 h1:VuZ8uybHlWmqV03+zRzdwKL4tUnIp1MAQtp1mIFE1bc=
@ -12,6 +15,8 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU= github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.5.2 h1:yTSXVswvWUOQ3k1sd7vJfDrbSl8lKuscqFJRqjC0ifw=
github.com/lib/pq v1.5.2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
@ -22,12 +27,24 @@ github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 h1:RYiqpb2ii2Z6J4x0wxK46kvPBbFuZcdhS+CIztmYgZs= github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086 h1:RYiqpb2ii2Z6J4x0wxK46kvPBbFuZcdhS+CIztmYgZs=
github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo= github.com/skip2/go-qrcode v0.0.0-20191027152451-9434209cb086/go.mod h1:PLPIyL7ikehBD1OAjmKKiOEhbvWyHGaNDjquXMcYABo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/tidwall/gjson v1.6.0 h1:9VEQWz6LLMUsUl6PueE49ir4Ka6CzLymOAZDxpFsTDc=
github.com/tidwall/gjson v1.6.0/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/pretty v1.0.1 h1:WE4RBSZ1x6McVVC8S/Md+Qse8YUv6HRObAx6ke00NY8=
github.com/tidwall/pretty v1.0.1/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tidwall/sjson v1.1.1 h1:7h1vk049Jnd5EH9NyzNiEuwYW4b5qgreBbqRC19AS3U=
github.com/tidwall/sjson v1.1.1/go.mod h1:yvVuSnpEQv5cYIrO+AT6kw4QVfd5SDZoGIS7/5+fZFs=
github.com/tulir/go-whatsapp v0.2.0 h1:JWK/Xxrc1qsZsVz6gYVX5AtvzYmqaHNjt34Ipnrgz88= github.com/tulir/go-whatsapp v0.2.0 h1:JWK/Xxrc1qsZsVz6gYVX5AtvzYmqaHNjt34Ipnrgz88=
github.com/tulir/go-whatsapp v0.2.0/go.mod h1:gyw9zGup1/Y3ZQUueZaqz3iR/WX9a2Lth4aqEbXjkok= github.com/tulir/go-whatsapp v0.2.0/go.mod h1:gyw9zGup1/Y3ZQUueZaqz3iR/WX9a2Lth4aqEbXjkok=
github.com/tulir/go-whatsapp v0.2.1 h1:Owoss2AbvZMgt3nxoFlsG+bqLHDnO+PhXNhhoCmb/3M= github.com/tulir/go-whatsapp v0.2.1 h1:Owoss2AbvZMgt3nxoFlsG+bqLHDnO+PhXNhhoCmb/3M=
@ -52,6 +69,7 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
@ -60,5 +78,9 @@ maunium.net/go/maulogger/v2 v2.1.1 h1:NAZNc6XUFJzgzfewCzVoGkxNAsblLCSSEdtDuIjP0X
maunium.net/go/maulogger/v2 v2.1.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A= maunium.net/go/maulogger/v2 v2.1.1/go.mod h1:TYWy7wKwz/tIXTpsx8G3mZseIRiC5DoMxSZazOHy68A=
maunium.net/go/mautrix v0.1.0-beta.2 h1:RxYTqTzW6iXu83gf8ucqGwYx8JLa+a17LWjiPkVV/fU= maunium.net/go/mautrix v0.1.0-beta.2 h1:RxYTqTzW6iXu83gf8ucqGwYx8JLa+a17LWjiPkVV/fU=
maunium.net/go/mautrix v0.1.0-beta.2/go.mod h1:YFMU9DBeXH7cqx7sJLg0DkVxwNPbih8QbpUTYf/IjMM= maunium.net/go/mautrix v0.1.0-beta.2/go.mod h1:YFMU9DBeXH7cqx7sJLg0DkVxwNPbih8QbpUTYf/IjMM=
maunium.net/go/mautrix v0.3.6 h1:bXUo8WFdv7sUpvr7jgJ6TVMEQgVHtw1z1T3eUcLpPCA=
maunium.net/go/mautrix v0.3.6/go.mod h1:SkGZzch8CvU2qKtNpYxtzZ0sQxfVEJ3IsVVLSUBUx9Y=
maunium.net/go/mautrix-appservice v0.1.0-alpha.6 h1:dNE+RykOC0UhSyRNbMHXEk3BzSOp3dj8aQwKuNMELWM= maunium.net/go/mautrix-appservice v0.1.0-alpha.6 h1:dNE+RykOC0UhSyRNbMHXEk3BzSOp3dj8aQwKuNMELWM=
maunium.net/go/mautrix-appservice v0.1.0-alpha.6/go.mod h1:Dfiwiuicvn8s2VKrBDrZ9eCjlKUMbuCi91TE6xeEHRM= maunium.net/go/mautrix-appservice v0.1.0-alpha.6/go.mod h1:Dfiwiuicvn8s2VKrBDrZ9eCjlKUMbuCi91TE6xeEHRM=
maunium.net/go/mautrix-appservice v0.2.0 h1:HmEpBSdGK7/8/xqOhxNP6viSQPkgjFVTfMI33moz51A=
maunium.net/go/mautrix-appservice v0.2.0/go.mod h1:55u7GKZBfxIs6tfAQTpvLvKLUjaJvll5HLcmx5Set1A=

34
main.go
View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2019 Tulir Asokan // Copyright (C) 2020 Tulir Asokan
// //
// This program is free software: you can redistribute it and/or modify // This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by // it under the terms of the GNU Affero General Public License as published by
@ -18,7 +18,6 @@ package main
import ( import (
"fmt" "fmt"
"net/http"
"os" "os"
"os/signal" "os/signal"
"sync" "sync"
@ -30,6 +29,7 @@ import (
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix-appservice" "maunium.net/go/mautrix-appservice"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/config" "maunium.net/go/mautrix-whatsapp/config"
"maunium.net/go/mautrix-whatsapp/database" "maunium.net/go/mautrix-whatsapp/database"
@ -107,28 +107,28 @@ type Bridge struct {
Formatter *Formatter Formatter *Formatter
Relaybot *User Relaybot *User
usersByMXID map[types.MatrixUserID]*User usersByMXID map[id.UserID]*User
usersByJID map[types.WhatsAppID]*User usersByJID map[types.WhatsAppID]*User
usersLock sync.Mutex usersLock sync.Mutex
managementRooms map[types.MatrixRoomID]*User managementRooms map[id.RoomID]*User
managementRoomsLock sync.Mutex managementRoomsLock sync.Mutex
portalsByMXID map[types.MatrixRoomID]*Portal portalsByMXID map[id.RoomID]*Portal
portalsByJID map[database.PortalKey]*Portal portalsByJID map[database.PortalKey]*Portal
portalsLock sync.Mutex portalsLock sync.Mutex
puppets map[types.WhatsAppID]*Puppet puppets map[types.WhatsAppID]*Puppet
puppetsByCustomMXID map[types.MatrixUserID]*Puppet puppetsByCustomMXID map[id.UserID]*Puppet
puppetsLock sync.Mutex puppetsLock sync.Mutex
} }
func NewBridge() *Bridge { func NewBridge() *Bridge {
bridge := &Bridge{ bridge := &Bridge{
usersByMXID: make(map[types.MatrixUserID]*User), usersByMXID: make(map[id.UserID]*User),
usersByJID: make(map[types.WhatsAppID]*User), usersByJID: make(map[types.WhatsAppID]*User),
managementRooms: make(map[types.MatrixRoomID]*User), managementRooms: make(map[id.RoomID]*User),
portalsByMXID: make(map[types.MatrixRoomID]*Portal), portalsByMXID: make(map[id.RoomID]*Portal),
portalsByJID: make(map[database.PortalKey]*Portal), portalsByJID: make(map[database.PortalKey]*Portal),
puppets: make(map[types.WhatsAppID]*Puppet), puppets: make(map[types.WhatsAppID]*Puppet),
puppetsByCustomMXID: make(map[types.MatrixUserID]*Puppet), puppetsByCustomMXID: make(map[id.UserID]*Puppet),
} }
var err error var err error
@ -141,12 +141,8 @@ func NewBridge() *Bridge {
} }
func (bridge *Bridge) ensureConnection() { func (bridge *Bridge) ensureConnection() {
url := bridge.Bot.BuildURL("account", "whoami")
resp := struct {
UserID string `json:"user_id"`
}{}
for { for {
_, err := bridge.Bot.MakeRequest(http.MethodGet, url, nil, &resp) resp, err := bridge.Bot.Whoami()
if err != nil { if err != nil {
if httpErr, ok := err.(mautrix.HTTPError); ok && httpErr.RespError != nil && httpErr.RespError.ErrCode == "M_UNKNOWN_ACCESS_TOKEN" { if httpErr, ok := err.(mautrix.HTTPError); ok && httpErr.RespError != nil && httpErr.RespError.ErrCode == "M_UNKNOWN_ACCESS_TOKEN" {
bridge.Log.Fatalln("Access token invalid. Is the registration installed in your homeserver correctly?") bridge.Log.Fatalln("Access token invalid. Is the registration installed in your homeserver correctly?")
@ -262,10 +258,14 @@ func (bridge *Bridge) UpdateBotProfile() {
botConfig := bridge.Config.AppService.Bot botConfig := bridge.Config.AppService.Bot
var err error var err error
var mxc id.ContentURI
if botConfig.Avatar == "remove" { if botConfig.Avatar == "remove" {
err = bridge.Bot.SetAvatarURL("") err = bridge.Bot.SetAvatarURL(mxc)
} else if len(botConfig.Avatar) > 0 { } else if len(botConfig.Avatar) > 0 {
err = bridge.Bot.SetAvatarURL(botConfig.Avatar) mxc, err = id.ParseContentURI(botConfig.Avatar)
if err == nil {
err = bridge.Bot.SetAvatarURL(mxc)
}
} }
if err != nil { if err != nil {
bridge.Log.Warnln("Failed to update bot avatar:", err) bridge.Log.Warnln("Failed to update bot avatar:", err)

101
matrix.go
View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2019 Tulir Asokan // Copyright (C) 2020 Tulir Asokan
// //
// This program is free software: you can redistribute it and/or modify // This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by // it under the terms of the GNU Affero General Public License as published by
@ -22,11 +22,10 @@ import (
"maunium.net/go/maulogger/v2" "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix-appservice" "maunium.net/go/mautrix-appservice"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/types"
) )
type MatrixHandler struct { type MatrixHandler struct {
@ -43,17 +42,17 @@ func NewMatrixHandler(bridge *Bridge) *MatrixHandler {
log: bridge.Log.Sub("Matrix"), log: bridge.Log.Sub("Matrix"),
cmd: NewCommandHandler(bridge), cmd: NewCommandHandler(bridge),
} }
bridge.EventProcessor.On(mautrix.EventMessage, handler.HandleMessage) bridge.EventProcessor.On(event.EventMessage, handler.HandleMessage)
bridge.EventProcessor.On(mautrix.EventSticker, handler.HandleMessage) bridge.EventProcessor.On(event.EventSticker, handler.HandleMessage)
bridge.EventProcessor.On(mautrix.EventRedaction, handler.HandleRedaction) bridge.EventProcessor.On(event.EventRedaction, handler.HandleRedaction)
bridge.EventProcessor.On(mautrix.StateMember, handler.HandleMembership) bridge.EventProcessor.On(event.StateMember, handler.HandleMembership)
bridge.EventProcessor.On(mautrix.StateRoomName, handler.HandleRoomMetadata) bridge.EventProcessor.On(event.StateRoomName, handler.HandleRoomMetadata)
bridge.EventProcessor.On(mautrix.StateRoomAvatar, handler.HandleRoomMetadata) bridge.EventProcessor.On(event.StateRoomAvatar, handler.HandleRoomMetadata)
bridge.EventProcessor.On(mautrix.StateTopic, handler.HandleRoomMetadata) bridge.EventProcessor.On(event.StateTopic, handler.HandleRoomMetadata)
return handler return handler
} }
func (mx *MatrixHandler) HandleBotInvite(evt *mautrix.Event) { func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) {
intent := mx.as.BotIntent() intent := mx.as.BotIntent()
user := mx.bridge.GetUserByMXID(evt.Sender) user := mx.bridge.GetUserByMXID(evt.Sender)
@ -61,7 +60,7 @@ func (mx *MatrixHandler) HandleBotInvite(evt *mautrix.Event) {
return return
} }
resp, err := intent.JoinRoom(evt.RoomID, "", nil) resp, err := intent.JoinRoomByID(evt.RoomID)
if err != nil { if err != nil {
mx.log.Debugln("Failed to join room", evt.RoomID, "with invite from", evt.Sender) mx.log.Debugln("Failed to join room", evt.RoomID, "with invite from", evt.Sender)
return return
@ -97,7 +96,7 @@ func (mx *MatrixHandler) HandleBotInvite(evt *mautrix.Event) {
for mxid, _ := range members.Joined { for mxid, _ := range members.Joined {
if mxid == intent.UserID || mxid == evt.Sender { if mxid == intent.UserID || mxid == evt.Sender {
continue continue
} else if _, ok := mx.bridge.ParsePuppetMXID(types.MatrixUserID(mxid)); ok { } else if _, ok := mx.bridge.ParsePuppetMXID(mxid); ok {
hasPuppets = true hasPuppets = true
continue continue
} }
@ -108,15 +107,16 @@ func (mx *MatrixHandler) HandleBotInvite(evt *mautrix.Event) {
} }
if !hasPuppets { if !hasPuppets {
user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender)) user := mx.bridge.GetUserByMXID(evt.Sender)
user.SetManagementRoom(types.MatrixRoomID(resp.RoomID)) user.SetManagementRoom(resp.RoomID)
intent.SendNotice(string(user.ManagementRoom), "This room has been registered as your bridge management/status room. Send `help` to get a list of commands.") intent.SendNotice(user.ManagementRoom, "This room has been registered as your bridge management/status room. Send `help` to get a list of commands.")
mx.log.Debugln(resp.RoomID, "registered as a management room with", evt.Sender) mx.log.Debugln(resp.RoomID, "registered as a management room with", evt.Sender)
} }
} }
func (mx *MatrixHandler) HandleMembership(evt *mautrix.Event) { func (mx *MatrixHandler) HandleMembership(evt *event.Event) {
if evt.Content.Membership == "invite" && evt.GetStateKey() == mx.as.BotMXID() { content := evt.Content.AsMember()
if content.Membership == event.MembershipInvite && id.UserID(evt.GetStateKey()) == mx.as.BotMXID() {
mx.HandleBotInvite(evt) mx.HandleBotInvite(evt)
} }
@ -125,15 +125,21 @@ func (mx *MatrixHandler) HandleMembership(evt *mautrix.Event) {
return return
} }
user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender)) user := mx.bridge.GetUserByMXID(id.UserID(evt.Sender))
if user == nil || !user.Whitelisted || !user.IsConnected() { if user == nil || !user.Whitelisted || !user.IsConnected() {
return return
} }
if evt.Content.Membership == "leave" { if content.Membership == event.MembershipLeave {
if evt.GetStateKey() == evt.Sender { if id.UserID(evt.GetStateKey()) == evt.Sender {
if portal.IsPrivateChat() || evt.Unsigned.PrevContent.Membership == "join" { if evt.Unsigned.PrevContent != nil {
portal.HandleMatrixLeave(user) _ = evt.Unsigned.PrevContent.ParseRaw(evt.Type)
prevContent, ok := evt.Unsigned.PrevContent.Parsed.(*event.MemberEventContent)
if ok {
if portal.IsPrivateChat() || prevContent.Membership == "join" {
portal.HandleMatrixLeave(user)
}
}
} }
} else { } else {
portal.HandleMatrixKick(user, evt) portal.HandleMatrixKick(user, evt)
@ -141,8 +147,8 @@ func (mx *MatrixHandler) HandleMembership(evt *mautrix.Event) {
} }
} }
func (mx *MatrixHandler) HandleRoomMetadata(evt *mautrix.Event) { func (mx *MatrixHandler) HandleRoomMetadata(evt *event.Event) {
user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender)) user := mx.bridge.GetUserByMXID(id.UserID(evt.Sender))
if user == nil || !user.Whitelisted || !user.IsConnected() { if user == nil || !user.Whitelisted || !user.IsConnected() {
return return
} }
@ -154,12 +160,12 @@ func (mx *MatrixHandler) HandleRoomMetadata(evt *mautrix.Event) {
var resp <-chan string var resp <-chan string
var err error var err error
switch evt.Type { switch content := evt.Content.Parsed.(type) {
case mautrix.StateRoomName: case *event.RoomNameEventContent:
resp, err = user.Conn.UpdateGroupSubject(evt.Content.Name, portal.Key.JID) resp, err = user.Conn.UpdateGroupSubject(content.Name, portal.Key.JID)
case mautrix.StateTopic: case *event.TopicEventContent:
resp, err = user.Conn.UpdateGroupDescription(portal.Key.JID, evt.Content.Topic) resp, err = user.Conn.UpdateGroupDescription(portal.Key.JID, content.Topic)
case mautrix.StateRoomAvatar: case *event.RoomAvatarEventContent:
return return
} }
if err != nil { if err != nil {
@ -170,7 +176,7 @@ func (mx *MatrixHandler) HandleRoomMetadata(evt *mautrix.Event) {
} }
} }
func (mx *MatrixHandler) HandleMessage(evt *mautrix.Event) { func (mx *MatrixHandler) HandleMessage(evt *event.Event) {
if _, isPuppet := mx.bridge.ParsePuppetMXID(evt.Sender); evt.Sender == mx.bridge.Bot.UserID || isPuppet { if _, isPuppet := mx.bridge.ParsePuppetMXID(evt.Sender); evt.Sender == mx.bridge.Bot.UserID || isPuppet {
return return
} }
@ -179,38 +185,37 @@ func (mx *MatrixHandler) HandleMessage(evt *mautrix.Event) {
return return
} }
roomID := types.MatrixRoomID(evt.RoomID) user := mx.bridge.GetUserByMXID(evt.Sender)
user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender))
if !user.RelaybotWhitelisted { if !user.RelaybotWhitelisted {
return return
} }
if user.Whitelisted && evt.Content.MsgType == mautrix.MsgText { content := evt.Content.AsMessage()
if user.Whitelisted && content.MsgType == event.MsgText {
commandPrefix := mx.bridge.Config.Bridge.CommandPrefix commandPrefix := mx.bridge.Config.Bridge.CommandPrefix
hasCommandPrefix := strings.HasPrefix(evt.Content.Body, commandPrefix) hasCommandPrefix := strings.HasPrefix(content.Body, commandPrefix)
if hasCommandPrefix { if hasCommandPrefix {
evt.Content.Body = strings.TrimLeft(evt.Content.Body[len(commandPrefix):], " ") content.Body = strings.TrimLeft(content.Body[len(commandPrefix):], " ")
} }
if hasCommandPrefix || roomID == user.ManagementRoom { if hasCommandPrefix || evt.RoomID == user.ManagementRoom {
mx.cmd.Handle(roomID, user, evt.Content.Body) mx.cmd.Handle(evt.RoomID, user, content.Body)
return return
} }
} }
portal := mx.bridge.GetPortalByMXID(roomID) portal := mx.bridge.GetPortalByMXID(evt.RoomID)
if portal != nil && (user.Whitelisted || portal.HasRelaybot()) { if portal != nil && (user.Whitelisted || portal.HasRelaybot()) {
portal.HandleMatrixMessage(user, evt) portal.HandleMatrixMessage(user, evt)
} }
} }
func (mx *MatrixHandler) HandleRedaction(evt *mautrix.Event) { func (mx *MatrixHandler) HandleRedaction(evt *event.Event) {
if _, isPuppet := mx.bridge.ParsePuppetMXID(evt.Sender); evt.Sender == mx.bridge.Bot.UserID || isPuppet { if _, isPuppet := mx.bridge.ParsePuppetMXID(evt.Sender); evt.Sender == mx.bridge.Bot.UserID || isPuppet {
return return
} }
roomID := types.MatrixRoomID(evt.RoomID) user := mx.bridge.GetUserByMXID(id.UserID(evt.Sender))
user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender))
if !user.Whitelisted { if !user.Whitelisted {
return return
@ -221,13 +226,13 @@ func (mx *MatrixHandler) HandleRedaction(evt *mautrix.Event) {
} else if !user.IsConnected() { } else if !user.IsConnected() {
msg := format.RenderMarkdown(fmt.Sprintf("[%[1]s](https://matrix.to/#/%[1]s): \u26a0 "+ msg := format.RenderMarkdown(fmt.Sprintf("[%[1]s](https://matrix.to/#/%[1]s): \u26a0 "+
"You are not connected to WhatsApp, so your redaction was not bridged. "+ "You are not connected to WhatsApp, so your redaction was not bridged. "+
"Use `%[2]s reconnect` to reconnect.", user.MXID, mx.bridge.Config.Bridge.CommandPrefix)) "Use `%[2]s reconnect` to reconnect.", user.MXID, mx.bridge.Config.Bridge.CommandPrefix), true, false)
msg.MsgType = mautrix.MsgNotice msg.MsgType = event.MsgNotice
_, _ = mx.bridge.Bot.SendMessageEvent(roomID, mautrix.EventMessage, msg) _, _ = mx.bridge.Bot.SendMessageEvent(evt.RoomID, event.EventMessage, msg)
return return
} }
portal := mx.bridge.GetPortalByMXID(roomID) portal := mx.bridge.GetPortalByMXID(evt.RoomID)
if portal != nil { if portal != nil {
portal.HandleMatrixRedaction(user, evt) portal.HandleMatrixRedaction(user, evt)
} }

266
portal.go
View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2019 Tulir Asokan // Copyright (C) 2020 Tulir Asokan
// //
// This program is free software: you can redistribute it and/or modify // This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by // it under the terms of the GNU Affero General Public License as published by
@ -20,7 +20,6 @@ import (
"bytes" "bytes"
"encoding/gob" "encoding/gob"
"encoding/hex" "encoding/hex"
"encoding/json"
"fmt" "fmt"
"html" "html"
"image" "image"
@ -43,14 +42,16 @@ import (
"maunium.net/go/mautrix" "maunium.net/go/mautrix"
"maunium.net/go/mautrix-appservice" "maunium.net/go/mautrix-appservice"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/database" "maunium.net/go/mautrix-whatsapp/database"
"maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix-whatsapp/whatsapp-ext" "maunium.net/go/mautrix-whatsapp/whatsapp-ext"
) )
func (bridge *Bridge) GetPortalByMXID(mxid types.MatrixRoomID) *Portal { func (bridge *Bridge) GetPortalByMXID(mxid id.RoomID) *Portal {
bridge.portalsLock.Lock() bridge.portalsLock.Lock()
defer bridge.portalsLock.Unlock() defer bridge.portalsLock.Unlock()
portal, ok := bridge.portalsByMXID[mxid] portal, ok := bridge.portalsByMXID[mxid]
@ -233,7 +234,7 @@ func init() {
gob.Register(&waProto.Message{}) gob.Register(&waProto.Message{})
} }
func (portal *Portal) markHandled(source *User, message *waProto.WebMessageInfo, mxid types.MatrixEventID) { func (portal *Portal) markHandled(source *User, message *waProto.WebMessageInfo, mxid id.EventID) {
msg := portal.bridge.DB.Message.New() msg := portal.bridge.DB.Message.New()
msg.Chat = portal.Key msg.Chat = portal.Key
msg.JID = message.GetKey().GetId() msg.JID = message.GetKey().GetId()
@ -269,7 +270,7 @@ func (portal *Portal) startHandling(info whatsapp.MessageInfo) bool {
return true return true
} }
func (portal *Portal) finishHandling(source *User, message *waProto.WebMessageInfo, mxid types.MatrixEventID) { func (portal *Portal) finishHandling(source *User, message *waProto.WebMessageInfo, mxid id.EventID) {
portal.markHandled(source, message, mxid) portal.markHandled(source, message, mxid)
portal.log.Debugln("Handled message", message.GetKey().GetId(), "->", mxid) portal.log.Debugln("Handled message", message.GetKey().GetId(), "->", mxid)
} }
@ -416,7 +417,7 @@ func (portal *Portal) UpdateMetadata(user *User) bool {
return update return update
} }
func (portal *Portal) userMXIDAction(user *User, fn func(mxid types.MatrixUserID)) { func (portal *Portal) userMXIDAction(user *User, fn func(mxid id.UserID)) {
if user == nil { if user == nil {
return return
} }
@ -430,7 +431,7 @@ func (portal *Portal) userMXIDAction(user *User, fn func(mxid types.MatrixUserID
} }
} }
func (portal *Portal) ensureMXIDInvited(mxid types.MatrixUserID) { func (portal *Portal) ensureMXIDInvited(mxid id.UserID) {
err := portal.MainIntent().EnsureInvited(portal.MXID, mxid) err := portal.MainIntent().EnsureInvited(portal.MXID, mxid)
if err != nil { if err != nil {
portal.log.Warnfln("Failed to ensure %s is invited to %s: %v", mxid, portal.MXID, err) portal.log.Warnfln("Failed to ensure %s is invited to %s: %v", mxid, portal.MXID, err)
@ -481,27 +482,27 @@ func (portal *Portal) Sync(user *User, contact whatsapp.Contact) {
} }
} }
func (portal *Portal) GetBasePowerLevels() *mautrix.PowerLevels { func (portal *Portal) GetBasePowerLevels() *event.PowerLevelsEventContent {
anyone := 0 anyone := 0
nope := 99 nope := 99
invite := 99 invite := 99
if portal.bridge.Config.Bridge.AllowUserInvite { if portal.bridge.Config.Bridge.AllowUserInvite {
invite = 0 invite = 0
} }
return &mautrix.PowerLevels{ return &event.PowerLevelsEventContent{
UsersDefault: anyone, UsersDefault: anyone,
EventsDefault: anyone, EventsDefault: anyone,
RedactPtr: &anyone, RedactPtr: &anyone,
StateDefaultPtr: &nope, StateDefaultPtr: &nope,
BanPtr: &nope, BanPtr: &nope,
InvitePtr: &invite, InvitePtr: &invite,
Users: map[string]int{ Users: map[id.UserID]int{
portal.MainIntent().UserID: 100, portal.MainIntent().UserID: 100,
}, },
Events: map[string]int{ Events: map[string]int{
mautrix.StateRoomName.Type: anyone, event.StateRoomName.Type: anyone,
mautrix.StateRoomAvatar.Type: anyone, event.StateRoomAvatar.Type: anyone,
mautrix.StateTopic.Type: anyone, event.StateTopic.Type: anyone,
}, },
} }
} }
@ -559,9 +560,9 @@ func (portal *Portal) RestrictMetadataChanges(restrict bool) {
newLevel = 50 newLevel = 50
} }
changed := false changed := false
changed = levels.EnsureEventLevel(mautrix.StateRoomName, newLevel) || changed changed = levels.EnsureEventLevel(event.StateRoomName, newLevel) || changed
changed = levels.EnsureEventLevel(mautrix.StateRoomAvatar, newLevel) || changed changed = levels.EnsureEventLevel(event.StateRoomAvatar, newLevel) || changed
changed = levels.EnsureEventLevel(mautrix.StateTopic, newLevel) || changed changed = levels.EnsureEventLevel(event.StateTopic, newLevel) || changed
if changed { if changed {
_, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels) _, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels)
if err != nil { if err != nil {
@ -749,22 +750,22 @@ func (portal *Portal) CreateMatrixRoom(user *User) error {
portal.UpdateAvatar(user, nil) portal.UpdateAvatar(user, nil)
} }
initialState := []*mautrix.Event{{ initialState := []*event.Event{{
Type: mautrix.StatePowerLevels, Type: event.StatePowerLevels,
Content: mautrix.Content{ Content: event.Content{
PowerLevels: portal.GetBasePowerLevels(), Parsed: portal.GetBasePowerLevels(),
}, },
}} }}
if len(portal.AvatarURL) > 0 { if !portal.AvatarURL.IsEmpty() {
initialState = append(initialState, &mautrix.Event{ initialState = append(initialState, &event.Event{
Type: mautrix.StateRoomAvatar, Type: event.StateRoomAvatar,
Content: mautrix.Content{ Content: event.Content{
URL: portal.AvatarURL, Parsed: event.RoomAvatarEventContent{URL: portal.AvatarURL},
}, },
}) })
} }
invite := []string{user.MXID} invite := []id.UserID{user.MXID}
if user.IsRelaybot { if user.IsRelaybot {
invite = portal.bridge.Config.Bridge.Relaybot.InviteUsers invite = portal.bridge.Config.Bridge.Relaybot.InviteUsers
} }
@ -847,19 +848,18 @@ func (portal *Portal) GetMessageIntent(user *User, info whatsapp.MessageInfo) *a
return portal.bridge.GetPuppetByJID(info.SenderJid).IntentFor(portal) return portal.bridge.GetPuppetByJID(info.SenderJid).IntentFor(portal)
} }
func (portal *Portal) SetReply(content *mautrix.Content, info whatsapp.ContextInfo) { func (portal *Portal) SetReply(content *event.MessageEventContent, info whatsapp.ContextInfo) {
if len(info.QuotedMessageID) == 0 { if len(info.QuotedMessageID) == 0 {
return return
} }
message := portal.bridge.DB.Message.GetByJID(portal.Key, info.QuotedMessageID) message := portal.bridge.DB.Message.GetByJID(portal.Key, info.QuotedMessageID)
if message != nil { if message != nil {
event, err := portal.MainIntent().GetEvent(portal.MXID, message.MXID) evt, err := portal.MainIntent().GetEvent(portal.MXID, message.MXID)
if err != nil { if err != nil {
portal.log.Warnln("Failed to get reply target:", err) portal.log.Warnln("Failed to get reply target:", err)
return return
} }
event.Content.RemoveReplyFallback() content.SetReply(evt)
content.SetReply(event)
} }
return return
} }
@ -908,32 +908,6 @@ func (portal *Portal) HandleFakeMessage(source *User, message FakeMessage) {
portal.recentlyHandled[index] = message.ID portal.recentlyHandled[index] = message.ID
} }
type MessageContent struct {
*mautrix.Content
IsCustomPuppet bool `json:"net.maunium.whatsapp.puppet,omitempty"`
}
type serializableContent mautrix.Content
type serializableMessageContent struct {
*serializableContent
IsCustomPuppet bool `json:"net.maunium.whatsapp.puppet,omitempty"`
}
// Hacky bypass for mautrix.Content's MarshalSJSON
func (content *MessageContent) MarshalJSON() ([]byte, error) {
if mautrix.DisableFancyEventParsing {
if content.IsCustomPuppet {
content.Raw["net.maunium.whatsapp.puppet"] = content.IsCustomPuppet
}
return json.Marshal(content.Raw)
}
return json.Marshal(&serializableMessageContent{
serializableContent: (*serializableContent)(content.Content),
IsCustomPuppet: content.IsCustomPuppet,
})
}
func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessage) { func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessage) {
if !portal.startHandling(message.Info) { if !portal.startHandling(message.Info) {
return return
@ -944,16 +918,21 @@ func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessa
return return
} }
content := &mautrix.Content{ content := &event.MessageEventContent{
Body: message.Text, Body: message.Text,
MsgType: mautrix.MsgText, MsgType: event.MsgText,
} }
portal.bridge.Formatter.ParseWhatsApp(content) portal.bridge.Formatter.ParseWhatsApp(content)
portal.SetReply(content, message.ContextInfo) portal.SetReply(content, message.ContextInfo)
_, _ = intent.UserTyping(portal.MXID, false, 0) _, _ = intent.UserTyping(portal.MXID, false, 0)
resp, err := intent.SendMassagedMessageEvent(portal.MXID, mautrix.EventMessage, &MessageContent{content, intent.IsCustomPuppet}, int64(message.Info.Timestamp*1000)) resp, err := intent.SendMassagedMessageEvent(portal.MXID, event.EventMessage, &event.Content{
Parsed: content,
Raw: map[string]interface{}{
"net.maunium.whatsapp.puppet": intent.IsCustomPuppet,
},
}, int64(message.Info.Timestamp*1000))
if err != nil { if err != nil {
portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err) portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err)
return return
@ -1016,10 +995,10 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte,
fileName += exts[0] fileName += exts[0]
} }
content := &mautrix.Content{ content := &event.MessageEventContent{
Body: fileName, Body: fileName,
URL: uploaded.ContentURI, URL: uploaded.ContentURI.CUString(),
Info: &mautrix.FileInfo{ Info: &event.FileInfo{
Size: len(data), Size: len(data),
MimeType: mimeType, MimeType: mimeType,
}, },
@ -1030,9 +1009,9 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte,
thumbnailMime := http.DetectContentType(thumbnail) thumbnailMime := http.DetectContentType(thumbnail)
uploadedThumbnail, _ := intent.UploadBytes(thumbnail, thumbnailMime) uploadedThumbnail, _ := intent.UploadBytes(thumbnail, thumbnailMime)
if uploadedThumbnail != nil { if uploadedThumbnail != nil {
content.Info.ThumbnailURL = uploadedThumbnail.ContentURI content.Info.ThumbnailURL = uploadedThumbnail.ContentURI.CUString()
cfg, _, _ := image.DecodeConfig(bytes.NewReader(data)) cfg, _, _ := image.DecodeConfig(bytes.NewReader(data))
content.Info.ThumbnailInfo = &mautrix.FileInfo{ content.Info.ThumbnailInfo = &event.FileInfo{
Size: len(thumbnail), Size: len(thumbnail),
Width: cfg.Width, Width: cfg.Width,
Height: cfg.Height, Height: cfg.Height,
@ -1044,40 +1023,50 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte,
switch strings.ToLower(strings.Split(mimeType, "/")[0]) { switch strings.ToLower(strings.Split(mimeType, "/")[0]) {
case "image": case "image":
if !sendAsSticker { if !sendAsSticker {
content.MsgType = mautrix.MsgImage content.MsgType = event.MsgImage
} }
cfg, _, _ := image.DecodeConfig(bytes.NewReader(data)) cfg, _, _ := image.DecodeConfig(bytes.NewReader(data))
content.Info.Width = cfg.Width content.Info.Width = cfg.Width
content.Info.Height = cfg.Height content.Info.Height = cfg.Height
case "video": case "video":
content.MsgType = mautrix.MsgVideo content.MsgType = event.MsgVideo
case "audio": case "audio":
content.MsgType = mautrix.MsgAudio content.MsgType = event.MsgAudio
default: default:
content.MsgType = mautrix.MsgFile content.MsgType = event.MsgFile
} }
_, _ = intent.UserTyping(portal.MXID, false, 0) _, _ = intent.UserTyping(portal.MXID, false, 0)
ts := int64(info.Timestamp * 1000) ts := int64(info.Timestamp * 1000)
eventType := mautrix.EventMessage eventType := event.EventMessage
if sendAsSticker { if sendAsSticker {
eventType = mautrix.EventSticker eventType = event.EventSticker
} }
resp, err := intent.SendMassagedMessageEvent(portal.MXID, eventType, &MessageContent{content, intent.IsCustomPuppet}, ts) resp, err := intent.SendMassagedMessageEvent(portal.MXID, eventType, &event.Content{
Parsed: content,
Raw: map[string]interface{}{
"net.maunium.whatsapp.puppet": intent.IsCustomPuppet,
},
}, ts)
if err != nil { if err != nil {
portal.log.Errorfln("Failed to handle message %s: %v", info.Id, err) portal.log.Errorfln("Failed to handle message %s: %v", info.Id, err)
return return
} }
if len(caption) > 0 { if len(caption) > 0 {
captionContent := &mautrix.Content{ captionContent := &event.MessageEventContent{
Body: caption, Body: caption,
MsgType: mautrix.MsgNotice, MsgType: event.MsgNotice,
} }
portal.bridge.Formatter.ParseWhatsApp(captionContent) portal.bridge.Formatter.ParseWhatsApp(captionContent)
_, err := intent.SendMassagedMessageEvent(portal.MXID, mautrix.EventMessage, &MessageContent{captionContent, intent.IsCustomPuppet}, ts) _, err := intent.SendMassagedMessageEvent(portal.MXID, event.EventMessage, &event.Content{
Parsed: content,
Raw: map[string]interface{}{
"net.maunium.whatsapp.puppet": intent.IsCustomPuppet,
},
}, ts)
if err != nil { if err != nil {
portal.log.Warnfln("Failed to handle caption of message %s: %v", info.Id, err) portal.log.Warnfln("Failed to handle caption of message %s: %v", info.Id, err)
} }
@ -1094,14 +1083,17 @@ func makeMessageID() *string {
return &str return &str
} }
func (portal *Portal) downloadThumbnail(evt *mautrix.Event) []byte { func (portal *Portal) downloadThumbnail(content *event.MessageEventContent, id id.EventID) []byte {
if evt.Content.Info == nil || len(evt.Content.Info.ThumbnailURL) == 0 { if len(content.GetInfo().ThumbnailURL) == 0 {
return nil return nil
} }
mxc, err := content.GetInfo().ThumbnailURL.Parse()
thumbnail, err := portal.MainIntent().DownloadBytes(evt.Content.Info.ThumbnailURL)
if err != nil { if err != nil {
portal.log.Errorln("Failed to download thumbnail in %s: %v", evt.ID, err) portal.log.Errorln("Malformed thumbnail URL in %s: %v", id, err)
}
thumbnail, err := portal.MainIntent().DownloadBytes(mxc)
if err != nil {
portal.log.Errorln("Failed to download thumbnail in %s: %v", id, err)
return nil return nil
} }
thumbnailType := http.DetectContentType(thumbnail) thumbnailType := http.DetectContentType(thumbnail)
@ -1121,30 +1113,31 @@ func (portal *Portal) downloadThumbnail(evt *mautrix.Event) []byte {
Quality: jpeg.DefaultQuality, Quality: jpeg.DefaultQuality,
}) })
if err != nil { if err != nil {
portal.log.Errorln("Failed to re-encode thumbnail in %s: %v", evt.ID, err) portal.log.Errorln("Failed to re-encode thumbnail in %s: %v", id, err)
return nil return nil
} }
return buf.Bytes() return buf.Bytes()
} }
func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool, evt *mautrix.Event, mediaType whatsapp.MediaType) *MediaUpload { func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool, content *event.MessageEventContent, id id.EventID, mediaType whatsapp.MediaType) *MediaUpload {
if evt.Content.Info == nil {
evt.Content.Info = &mautrix.FileInfo{}
}
var caption string var caption string
if relaybotFormatted { if relaybotFormatted {
caption = portal.bridge.Formatter.ParseMatrix(evt.Content.FormattedBody) caption = portal.bridge.Formatter.ParseMatrix(content.FormattedBody)
} }
content, err := portal.MainIntent().DownloadBytes(evt.Content.URL) mxc, err := content.URL.Parse()
if err != nil { if err != nil {
portal.log.Errorfln("Failed to download media in %s: %v", evt.ID, err) portal.log.Errorln("Malformed content URL in %s: %v", id, err)
}
data, err := portal.MainIntent().DownloadBytes(mxc)
if err != nil {
portal.log.Errorfln("Failed to download media in %s: %v", id, err)
return nil return nil
} }
url, mediaKey, fileEncSHA256, fileSHA256, fileLength, err := sender.Conn.Upload(bytes.NewReader(content), mediaType) url, mediaKey, fileEncSHA256, fileSHA256, fileLength, err := sender.Conn.Upload(bytes.NewReader(data), mediaType)
if err != nil { if err != nil {
portal.log.Errorfln("Failed to upload media in %s: %v", evt.ID, err) portal.log.Errorfln("Failed to upload media in %s: %v", id, err)
return nil return nil
} }
@ -1155,7 +1148,7 @@ func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool
FileEncSHA256: fileEncSHA256, FileEncSHA256: fileEncSHA256,
FileSHA256: fileSHA256, FileSHA256: fileSHA256,
FileLength: fileLength, FileLength: fileLength,
Thumbnail: portal.downloadThumbnail(evt), Thumbnail: portal.downloadThumbnail(content, id),
} }
} }
@ -1169,7 +1162,7 @@ type MediaUpload struct {
Thumbnail []byte Thumbnail []byte
} }
func (portal *Portal) sendMatrixConnectionError(sender *User, eventID string) bool { func (portal *Portal) sendMatrixConnectionError(sender *User, eventID id.EventID) bool {
if !sender.HasSession() { if !sender.HasSession() {
portal.log.Debugln("Ignoring event", eventID, "from", sender.MXID, "as user has no session") portal.log.Debugln("Ignoring event", eventID, "from", sender.MXID, "as user has no session")
return true return true
@ -1183,9 +1176,9 @@ func (portal *Portal) sendMatrixConnectionError(sender *User, eventID string) bo
if sender.IsLoginInProgress() { if sender.IsLoginInProgress() {
reconnect = "You have a login attempt in progress, please wait." reconnect = "You have a login attempt in progress, please wait."
} }
msg := format.RenderMarkdown("\u26a0 You are not connected to WhatsApp, so your message was not bridged. " + reconnect) msg := format.RenderMarkdown("\u26a0 You are not connected to WhatsApp, so your message was not bridged. " + reconnect, true, false)
msg.MsgType = mautrix.MsgNotice msg.MsgType = event.MsgNotice
_, err := portal.MainIntent().SendMessageEvent(portal.MXID, mautrix.EventMessage, msg) _, err := portal.MainIntent().SendMessageEvent(portal.MXID, event.EventMessage, msg)
if err != nil { if err != nil {
portal.log.Errorln("Failed to send bridging failure message:", err) portal.log.Errorln("Failed to send bridging failure message:", err)
} }
@ -1194,32 +1187,37 @@ func (portal *Portal) sendMatrixConnectionError(sender *User, eventID string) bo
return false return false
} }
func (portal *Portal) addRelaybotFormat(user *User, evt *mautrix.Event) bool { func (portal *Portal) addRelaybotFormat(sender *User, content *event.MessageEventContent) bool {
member := portal.MainIntent().Member(portal.MXID, evt.Sender) member := portal.MainIntent().Member(portal.MXID, sender.MXID)
if len(member.Displayname) == 0 { if len(member.Displayname) == 0 {
member.Displayname = evt.Sender member.Displayname = string(sender.MXID)
} }
if evt.Content.Format != mautrix.FormatHTML { if content.Format != event.FormatHTML {
evt.Content.FormattedBody = strings.Replace(html.EscapeString(evt.Content.Body), "\n", "<br/>", -1) content.FormattedBody = strings.Replace(html.EscapeString(content.Body), "\n", "<br/>", -1)
evt.Content.Format = mautrix.FormatHTML content.Format = event.FormatHTML
} }
data, err := portal.bridge.Config.Bridge.Relaybot.FormatMessage(evt, member) data, err := portal.bridge.Config.Bridge.Relaybot.FormatMessage(content, sender.MXID, member)
if err != nil { if err != nil {
portal.log.Errorln("Failed to apply relaybot format:", err) portal.log.Errorln("Failed to apply relaybot format:", err)
} }
evt.Content.FormattedBody = data content.FormattedBody = data
return true return true
} }
func (portal *Portal) HandleMatrixMessage(sender *User, evt *mautrix.Event) { func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) {
if !portal.HasRelaybot() && ( if !portal.HasRelaybot() && (
(portal.IsPrivateChat() && sender.JID != portal.Key.Receiver) || (portal.IsPrivateChat() && sender.JID != portal.Key.Receiver) ||
portal.sendMatrixConnectionError(sender, evt.ID)) { portal.sendMatrixConnectionError(sender, evt.ID)) {
return return
} }
content := evt.Content.AsMessage()
if content == nil {
return
}
portal.log.Debugfln("Received event %s", evt.ID) portal.log.Debugfln("Received event %s", evt.ID)
ts := uint64(evt.Timestamp / 1000) ts := uint64(evt.Timestamp / 1000)
status := waProto.WebMessageInfo_ERROR status := waProto.WebMessageInfo_ERROR
fromMe := true fromMe := true
@ -1234,9 +1232,9 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *mautrix.Event) {
Status: &status, Status: &status,
} }
ctxInfo := &waProto.ContextInfo{} ctxInfo := &waProto.ContextInfo{}
replyToID := evt.Content.GetReplyTo() replyToID := content.GetReplyTo()
if len(replyToID) > 0 { if len(replyToID) > 0 {
evt.Content.RemoveReplyFallback() content.RemoveReplyFallback()
msg := portal.bridge.DB.Message.GetByMXID(replyToID) msg := portal.bridge.DB.Message.GetByMXID(replyToID)
if msg != nil && msg.Content != nil { if msg != nil && msg.Content != nil {
ctxInfo.StanzaId = &msg.JID ctxInfo.StanzaId = &msg.JID
@ -1254,21 +1252,21 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *mautrix.Event) {
return return
} }
} else { } else {
relaybotFormatted = portal.addRelaybotFormat(sender, evt) relaybotFormatted = portal.addRelaybotFormat(sender, content)
sender = portal.bridge.Relaybot sender = portal.bridge.Relaybot
} }
} }
if evt.Type == mautrix.EventSticker { if evt.Type == event.EventSticker {
evt.Content.MsgType = mautrix.MsgImage content.MsgType = event.MsgImage
} }
var err error var err error
switch evt.Content.MsgType { switch content.MsgType {
case mautrix.MsgText, mautrix.MsgEmote, mautrix.MsgNotice: case event.MsgText, event.MsgEmote, event.MsgNotice:
text := evt.Content.Body text := content.Body
if evt.Content.Format == mautrix.FormatHTML { if content.Format == event.FormatHTML {
text = portal.bridge.Formatter.ParseMatrix(evt.Content.FormattedBody) text = portal.bridge.Formatter.ParseMatrix(content.FormattedBody)
} }
if evt.Content.MsgType == mautrix.MsgEmote && !relaybotFormatted { if content.MsgType == event.MsgEmote && !relaybotFormatted {
text = "/me " + text text = "/me " + text
} }
ctxInfo.MentionedJid = mentionRegex.FindAllString(text, -1) ctxInfo.MentionedJid = mentionRegex.FindAllString(text, -1)
@ -1283,8 +1281,8 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *mautrix.Event) {
} else { } else {
info.Message.Conversation = &text info.Message.Conversation = &text
} }
case mautrix.MsgImage: case event.MsgImage:
media := portal.preprocessMatrixMedia(sender, relaybotFormatted, evt, whatsapp.MediaImage) media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaImage)
if media == nil { if media == nil {
return return
} }
@ -1293,53 +1291,53 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *mautrix.Event) {
JpegThumbnail: media.Thumbnail, JpegThumbnail: media.Thumbnail,
Url: &media.URL, Url: &media.URL,
MediaKey: media.MediaKey, MediaKey: media.MediaKey,
Mimetype: &evt.Content.GetInfo().MimeType, Mimetype: &content.GetInfo().MimeType,
FileEncSha256: media.FileEncSHA256, FileEncSha256: media.FileEncSHA256,
FileSha256: media.FileSHA256, FileSha256: media.FileSHA256,
FileLength: &media.FileLength, FileLength: &media.FileLength,
} }
case mautrix.MsgVideo: case event.MsgVideo:
media := portal.preprocessMatrixMedia(sender, relaybotFormatted, evt, whatsapp.MediaVideo) media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaVideo)
if media == nil { if media == nil {
return return
} }
duration := uint32(evt.Content.GetInfo().Duration) duration := uint32(content.GetInfo().Duration)
info.Message.VideoMessage = &waProto.VideoMessage{ info.Message.VideoMessage = &waProto.VideoMessage{
Caption: &media.Caption, Caption: &media.Caption,
JpegThumbnail: media.Thumbnail, JpegThumbnail: media.Thumbnail,
Url: &media.URL, Url: &media.URL,
MediaKey: media.MediaKey, MediaKey: media.MediaKey,
Mimetype: &evt.Content.GetInfo().MimeType, Mimetype: &content.GetInfo().MimeType,
Seconds: &duration, Seconds: &duration,
FileEncSha256: media.FileEncSHA256, FileEncSha256: media.FileEncSHA256,
FileSha256: media.FileSHA256, FileSha256: media.FileSHA256,
FileLength: &media.FileLength, FileLength: &media.FileLength,
} }
case mautrix.MsgAudio: case event.MsgAudio:
media := portal.preprocessMatrixMedia(sender, relaybotFormatted, evt, whatsapp.MediaAudio) media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaAudio)
if media == nil { if media == nil {
return return
} }
duration := uint32(evt.Content.GetInfo().Duration) duration := uint32(content.GetInfo().Duration)
info.Message.AudioMessage = &waProto.AudioMessage{ info.Message.AudioMessage = &waProto.AudioMessage{
Url: &media.URL, Url: &media.URL,
MediaKey: media.MediaKey, MediaKey: media.MediaKey,
Mimetype: &evt.Content.GetInfo().MimeType, Mimetype: &content.GetInfo().MimeType,
Seconds: &duration, Seconds: &duration,
FileEncSha256: media.FileEncSHA256, FileEncSha256: media.FileEncSHA256,
FileSha256: media.FileSHA256, FileSha256: media.FileSHA256,
FileLength: &media.FileLength, FileLength: &media.FileLength,
} }
case mautrix.MsgFile: case event.MsgFile:
media := portal.preprocessMatrixMedia(sender, relaybotFormatted, evt, whatsapp.MediaDocument) media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaDocument)
if media == nil { if media == nil {
return return
} }
info.Message.DocumentMessage = &waProto.DocumentMessage{ info.Message.DocumentMessage = &waProto.DocumentMessage{
Url: &media.URL, Url: &media.URL,
FileName: &evt.Content.Body, FileName: &content.Body,
MediaKey: media.MediaKey, MediaKey: media.MediaKey,
Mimetype: &evt.Content.GetInfo().MimeType, Mimetype: &content.GetInfo().MimeType,
FileEncSha256: media.FileEncSHA256, FileEncSha256: media.FileEncSHA256,
FileSha256: media.FileSHA256, FileSha256: media.FileSHA256,
FileLength: &media.FileLength, FileLength: &media.FileLength,
@ -1353,9 +1351,9 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *mautrix.Event) {
_, err = sender.Conn.Send(info) _, err = sender.Conn.Send(info)
if err != nil { if err != nil {
portal.log.Errorfln("Error handling Matrix event %s: %v", evt.ID, err) portal.log.Errorfln("Error handling Matrix event %s: %v", evt.ID, err)
msg := format.RenderMarkdown(fmt.Sprintf("\u26a0 Your message may not have been bridged: %v", err)) msg := format.RenderMarkdown(fmt.Sprintf("\u26a0 Your message may not have been bridged: %v", err), false, false)
msg.MsgType = mautrix.MsgNotice msg.MsgType = event.MsgNotice
_, err := portal.MainIntent().SendMessageEvent(portal.MXID, mautrix.EventMessage, msg) _, err := portal.MainIntent().SendMessageEvent(portal.MXID, event.EventMessage, msg)
if err != nil { if err != nil {
portal.log.Errorln("Failed to send bridging failure message:", err) portal.log.Errorln("Failed to send bridging failure message:", err)
} }
@ -1364,7 +1362,7 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *mautrix.Event) {
} }
} }
func (portal *Portal) HandleMatrixRedaction(sender *User, evt *mautrix.Event) { func (portal *Portal) HandleMatrixRedaction(sender *User, evt *event.Event) {
if portal.IsPrivateChat() && sender.JID != portal.Key.Receiver { if portal.IsPrivateChat() && sender.JID != portal.Key.Receiver {
return return
} }
@ -1462,6 +1460,6 @@ func (portal *Portal) HandleMatrixLeave(sender *User) {
} }
} }
func (portal *Portal) HandleMatrixKick(sender *User, event *mautrix.Event) { func (portal *Portal) HandleMatrixKick(sender *User, event *event.Event) {
// TODO // TODO
} }

View file

@ -26,8 +26,8 @@ import (
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix-whatsapp/types"
whatsappExt "maunium.net/go/mautrix-whatsapp/whatsapp-ext" whatsappExt "maunium.net/go/mautrix-whatsapp/whatsapp-ext"
"maunium.net/go/mautrix/id"
) )
type ProvisioningAPI struct { type ProvisioningAPI struct {
@ -61,7 +61,7 @@ func (prov *ProvisioningAPI) AuthMiddleware(h http.Handler) http.Handler {
return return
} }
userID := r.URL.Query().Get("user_id") userID := r.URL.Query().Get("user_id")
user := prov.bridge.GetUserByMXID(types.MatrixUserID(userID)) user := prov.bridge.GetUserByMXID(id.UserID(userID))
h.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "user", user))) h.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "user", user)))
}) })
} }
@ -300,7 +300,7 @@ var upgrader = websocket.Upgrader{}
func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) { func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
userID := r.URL.Query().Get("user_id") userID := r.URL.Query().Get("user_id")
user := prov.bridge.GetUserByMXID(types.MatrixUserID(userID)) user := prov.bridge.GetUserByMXID(id.UserID(userID))
c, err := upgrader.Upgrade(w, r, nil) c, err := upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {

View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2019 Tulir Asokan // Copyright (C) 2020 Tulir Asokan
// //
// This program is free software: you can redistribute it and/or modify // This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by // it under the terms of the GNU Affero General Public License as published by
@ -25,14 +25,16 @@ import (
"github.com/Rhymen/go-whatsapp" "github.com/Rhymen/go-whatsapp"
log "maunium.net/go/maulogger/v2" log "maunium.net/go/maulogger/v2"
"maunium.net/go/mautrix-appservice" "maunium.net/go/mautrix-appservice"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/database" "maunium.net/go/mautrix-whatsapp/database"
"maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix-whatsapp/types"
"maunium.net/go/mautrix-whatsapp/whatsapp-ext" "maunium.net/go/mautrix-whatsapp/whatsapp-ext"
) )
func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.WhatsAppID, bool) { func (bridge *Bridge) ParsePuppetMXID(mxid id.UserID) (types.WhatsAppID, bool) {
userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$", userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$",
bridge.Config.Bridge.FormatUsername("([0-9]+)"), bridge.Config.Bridge.FormatUsername("([0-9]+)"),
bridge.Config.Homeserver.Domain)) bridge.Config.Homeserver.Domain))
@ -49,7 +51,7 @@ func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.WhatsAppID
return jid, true return jid, true
} }
func (bridge *Bridge) GetPuppetByMXID(mxid types.MatrixUserID) *Puppet { func (bridge *Bridge) GetPuppetByMXID(mxid id.UserID) *Puppet {
jid, ok := bridge.ParsePuppetMXID(mxid) jid, ok := bridge.ParsePuppetMXID(mxid)
if !ok { if !ok {
return nil return nil
@ -78,7 +80,7 @@ func (bridge *Bridge) GetPuppetByJID(jid types.WhatsAppID) *Puppet {
return puppet return puppet
} }
func (bridge *Bridge) GetPuppetByCustomMXID(mxid types.MatrixUserID) *Puppet { func (bridge *Bridge) GetPuppetByCustomMXID(mxid id.UserID) *Puppet {
bridge.puppetsLock.Lock() bridge.puppetsLock.Lock()
defer bridge.puppetsLock.Unlock() defer bridge.puppetsLock.Unlock()
puppet, ok := bridge.puppetsByCustomMXID[mxid] puppet, ok := bridge.puppetsByCustomMXID[mxid]
@ -129,7 +131,7 @@ func (bridge *Bridge) NewPuppet(dbPuppet *database.Puppet) *Puppet {
bridge: bridge, bridge: bridge,
log: bridge.Log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.JID)), log: bridge.Log.Sub(fmt.Sprintf("Puppet/%s", dbPuppet.JID)),
MXID: fmt.Sprintf("@%s:%s", MXID: id.NewUserID(
bridge.Config.Bridge.FormatUsername( bridge.Config.Bridge.FormatUsername(
strings.Replace( strings.Replace(
dbPuppet.JID, dbPuppet.JID,
@ -144,13 +146,13 @@ type Puppet struct {
bridge *Bridge bridge *Bridge
log log.Logger log log.Logger
typingIn types.MatrixRoomID typingIn id.RoomID
typingAt int64 typingAt int64
MXID types.MatrixUserID MXID id.UserID
customIntent *appservice.IntentAPI customIntent *appservice.IntentAPI
customTypingIn map[string]bool customTypingIn map[id.RoomID]bool
customUser *User customUser *User
} }
@ -192,11 +194,11 @@ func (puppet *Puppet) UpdateAvatar(source *User, avatar *whatsappExt.ProfilePicI
} }
if len(avatar.URL) == 0 { if len(avatar.URL) == 0 {
err := puppet.DefaultIntent().SetAvatarURL("") err := puppet.DefaultIntent().SetAvatarURL(id.ContentURI{})
if err != nil { if err != nil {
puppet.log.Warnln("Failed to remove avatar:", err) puppet.log.Warnln("Failed to remove avatar:", err)
} }
puppet.AvatarURL = "" puppet.AvatarURL = id.ContentURI{}
puppet.Avatar = avatar.Tag puppet.Avatar = avatar.Tag
go puppet.updatePortalAvatar() go puppet.updatePortalAvatar()
return true return true

View file

@ -21,12 +21,3 @@ type WhatsAppID = string
// WhatsAppMessageID is the internal ID of a WhatsApp message. // WhatsAppMessageID is the internal ID of a WhatsApp message.
type WhatsAppMessageID = string type WhatsAppMessageID = string
// MatrixUserID is the ID of a Matrix user.
type MatrixUserID = string
// MatrixRoomID is the internal room ID of a Matrix room.
type MatrixRoomID = string
// MatrixEventID is the internal ID of a Matrix event.
type MatrixEventID = string

74
user.go
View file

@ -1,5 +1,5 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. // mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2019 Tulir Asokan // Copyright (C) 2020 Tulir Asokan
// //
// This program is free software: you can redistribute it and/or modify // This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by // it under the terms of the GNU Affero General Public License as published by
@ -32,8 +32,9 @@ import (
"github.com/Rhymen/go-whatsapp" "github.com/Rhymen/go-whatsapp"
waProto "github.com/Rhymen/go-whatsapp/binary/proto" waProto "github.com/Rhymen/go-whatsapp/binary/proto"
"maunium.net/go/mautrix" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
"maunium.net/go/mautrix/id"
"maunium.net/go/mautrix-whatsapp/database" "maunium.net/go/mautrix-whatsapp/database"
"maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix-whatsapp/types"
@ -65,7 +66,7 @@ type User struct {
syncLock sync.Mutex syncLock sync.Mutex
} }
func (bridge *Bridge) GetUserByMXID(userID types.MatrixUserID) *User { func (bridge *Bridge) GetUserByMXID(userID id.UserID) *User {
_, isPuppet := bridge.ParsePuppetMXID(userID) _, isPuppet := bridge.ParsePuppetMXID(userID)
if isPuppet || userID == bridge.Bot.UserID { if isPuppet || userID == bridge.Bot.UserID {
return nil return nil
@ -104,7 +105,7 @@ func (bridge *Bridge) GetAllUsers() []*User {
return output return output
} }
func (bridge *Bridge) loadDBUser(dbUser *database.User, mxid *types.MatrixUserID) *User { func (bridge *Bridge) loadDBUser(dbUser *database.User, mxid *id.UserID) *User {
if dbUser == nil { if dbUser == nil {
if mxid == nil { if mxid == nil {
return nil return nil
@ -160,7 +161,7 @@ func (bridge *Bridge) NewUser(dbUser *database.User) *User {
return user return user
} }
func (user *User) SetManagementRoom(roomID types.MatrixRoomID) { func (user *User) SetManagementRoom(roomID id.RoomID) {
existingUser, ok := user.bridge.managementRooms[roomID] existingUser, ok := user.bridge.managementRooms[roomID]
if ok { if ok {
existingUser.ManagementRoom = "" existingUser.ManagementRoom = ""
@ -194,9 +195,9 @@ func (user *User) Connect(evenIfNoSession bool) bool {
conn, err := whatsapp.NewConn(timeout * time.Second) conn, err := whatsapp.NewConn(timeout * time.Second)
if err != nil { if err != nil {
user.log.Errorln("Failed to connect to WhatsApp:", err) user.log.Errorln("Failed to connect to WhatsApp:", err)
msg := format.RenderMarkdown("\u26a0 Failed to connect to WhatsApp server. " + msg := format.RenderMarkdown("\u26a0 Failed to connect to WhatsApp server. "+
"This indicates a network problem on the bridge server. See bridge logs for more info.") "This indicates a network problem on the bridge server. See bridge logs for more info.", true, false)
_, _ = user.bridge.Bot.SendMessageEvent(user.ManagementRoom, mautrix.EventMessage, msg) _, _ = user.bridge.Bot.SendMessageEvent(user.ManagementRoom, event.EventMessage, msg)
return false return false
} }
user.Conn = whatsappExt.ExtendConn(conn) user.Conn = whatsappExt.ExtendConn(conn)
@ -213,9 +214,9 @@ func (user *User) RestoreSession() bool {
return true return true
} else if err != nil { } else if err != nil {
user.log.Errorln("Failed to restore session:", err) user.log.Errorln("Failed to restore session:", err)
msg := format.RenderMarkdown("\u26a0 Failed to connect to WhatsApp. Make sure WhatsApp " + msg := format.RenderMarkdown("\u26a0 Failed to connect to WhatsApp. Make sure WhatsApp "+
"on your phone is reachable and use `reconnect` to try connecting again.") "on your phone is reachable and use `reconnect` to try connecting again.", true, false)
_, _ = user.bridge.Bot.SendMessageEvent(user.ManagementRoom, mautrix.EventMessage, msg) _, _ = user.bridge.Bot.SendMessageEvent(user.ManagementRoom, event.EventMessage, msg)
user.log.Debugln("Disconnecting due to failed session restore...") user.log.Debugln("Disconnecting due to failed session restore...")
_, err := user.Conn.Disconnect() _, err := user.Conn.Disconnect()
if err != nil { if err != nil {
@ -243,8 +244,8 @@ func (user *User) IsLoginInProgress() bool {
return user.Conn != nil && user.Conn.IsLoginInProgress() return user.Conn != nil && user.Conn.IsLoginInProgress()
} }
func (user *User) loginQrChannel(ce *CommandEvent, qrChan <-chan string, eventIDChan chan<- string) { func (user *User) loginQrChannel(ce *CommandEvent, qrChan <-chan string, eventIDChan chan<- id.EventID) {
var qrEventID string var qrEventID id.EventID
for code := range qrChan { for code := range qrChan {
if code == "stop" { if code == "stop" {
return return
@ -274,17 +275,17 @@ func (user *User) loginQrChannel(ce *CommandEvent, qrChan <-chan string, eventID
qrEventID = sendResp.EventID qrEventID = sendResp.EventID
eventIDChan <- qrEventID eventIDChan <- qrEventID
} else { } else {
_, err = bot.SendMessageEvent(ce.RoomID, mautrix.EventMessage, &mautrix.Content{ _, err = bot.SendMessageEvent(ce.RoomID, event.EventMessage, &event.MessageEventContent{
MsgType: mautrix.MsgImage, MsgType: event.MsgImage,
Body: code, Body: code,
URL: resp.ContentURI, URL: resp.ContentURI.CUString(),
NewContent: &mautrix.Content{ NewContent: &event.MessageEventContent{
MsgType: mautrix.MsgImage, MsgType: event.MsgImage,
Body: code, Body: code,
URL: resp.ContentURI, URL: resp.ContentURI.CUString(),
}, },
RelatesTo: &mautrix.RelatesTo{ RelatesTo: &event.RelatesTo{
Type: mautrix.RelReplace, Type: event.RelReplace,
EventID: qrEventID, EventID: qrEventID,
}, },
}) })
@ -297,18 +298,18 @@ func (user *User) loginQrChannel(ce *CommandEvent, qrChan <-chan string, eventID
func (user *User) Login(ce *CommandEvent) { func (user *User) Login(ce *CommandEvent) {
qrChan := make(chan string, 3) qrChan := make(chan string, 3)
eventIDChan := make(chan string, 1) eventIDChan := make(chan id.EventID, 1)
go user.loginQrChannel(ce, qrChan, eventIDChan) go user.loginQrChannel(ce, qrChan, eventIDChan)
session, err := user.Conn.LoginWithRetry(qrChan, user.bridge.Config.Bridge.LoginQRRegenCount) session, err := user.Conn.LoginWithRetry(qrChan, user.bridge.Config.Bridge.LoginQRRegenCount)
qrChan <- "stop" qrChan <- "stop"
if err != nil { if err != nil {
var eventID string var eventID id.EventID
select { select {
case eventID = <-eventIDChan: case eventID = <-eventIDChan:
default: default:
} }
reply := mautrix.Content{ reply := event.MessageEventContent{
MsgType: mautrix.MsgText, MsgType: event.MsgText,
} }
if err == whatsapp.ErrAlreadyLoggedIn { if err == whatsapp.ErrAlreadyLoggedIn {
reply.Body = "You're already logged in" reply.Body = "You're already logged in"
@ -323,12 +324,12 @@ func (user *User) Login(ce *CommandEvent) {
msg := reply msg := reply
if eventID != "" { if eventID != "" {
msg.NewContent = &reply msg.NewContent = &reply
msg.RelatesTo = &mautrix.RelatesTo{ msg.RelatesTo = &event.RelatesTo{
Type: mautrix.RelReplace, Type: event.RelReplace,
EventID: eventID, EventID: eventID,
} }
} }
_, _ = ce.Bot.SendMessageEvent(ce.RoomID, mautrix.EventMessage, &msg) _, _ = ce.Bot.SendMessageEvent(ce.RoomID, event.EventMessage, &msg)
return return
} }
user.ConnectionErrors = 0 user.ConnectionErrors = 0
@ -365,8 +366,11 @@ func (user *User) PostLogin() {
} }
func (user *User) tryAutomaticDoublePuppeting() { func (user *User) tryAutomaticDoublePuppeting() {
if len(user.bridge.Config.Bridge.LoginSharedSecret) == 0 || !strings.HasSuffix(user.MXID, user.bridge.Config.Homeserver.Domain) { if len(user.bridge.Config.Bridge.LoginSharedSecret) == 0 {
// Automatic login not enabled or user is on another homeserver // Automatic login not enabled
return
} else if _, homeserver, _ := user.MXID.Parse(); homeserver != user.bridge.Config.Homeserver.Domain {
// user is on another homeserver
return return
} }
@ -535,8 +539,8 @@ func (user *User) HandleError(err error) {
func (user *User) tryReconnect(msg string) { func (user *User) tryReconnect(msg string) {
if user.ConnectionErrors > user.bridge.Config.Bridge.MaxConnectionAttempts { if user.ConnectionErrors > user.bridge.Config.Bridge.MaxConnectionAttempts {
content := format.RenderMarkdown(fmt.Sprintf("%s. Use the `reconnect` command to reconnect.", msg)) content := format.RenderMarkdown(fmt.Sprintf("%s. Use the `reconnect` command to reconnect.", msg), true, false)
_, _ = user.bridge.Bot.SendMessageEvent(user.ManagementRoom, mautrix.EventMessage, content) _, _ = user.bridge.Bot.SendMessageEvent(user.ManagementRoom, event.EventMessage, content)
return return
} }
if user.bridge.Config.Bridge.ReportConnectionRetry { if user.bridge.Config.Bridge.ReportConnectionRetry {
@ -591,8 +595,8 @@ func (user *User) tryReconnect(msg string) {
"Use the `reconnect` command to try to reconnect.", msg, tries) "Use the `reconnect` command to try to reconnect.", msg, tries)
} }
content := format.RenderMarkdown(msg) content := format.RenderMarkdown(msg, true, false)
_, _ = user.bridge.Bot.SendMessageEvent(user.ManagementRoom, mautrix.EventMessage, content) _, _ = user.bridge.Bot.SendMessageEvent(user.ManagementRoom, event.EventMessage, content)
} }
func (user *User) ShouldCallSynchronously() bool { func (user *User) ShouldCallSynchronously() bool {
@ -766,7 +770,7 @@ func (user *User) HandleCommand(cmd whatsappExt.Command) {
"Use the `reconnect` command to reconnect.", cmd.Kind) "Use the `reconnect` command to reconnect.", cmd.Kind)
} }
user.cleanDisconnection = true user.cleanDisconnection = true
go user.bridge.Bot.SendMessageEvent(user.ManagementRoom, mautrix.EventMessage, format.RenderMarkdown(msg)) go user.bridge.Bot.SendMessageEvent(user.ManagementRoom, event.EventMessage, format.RenderMarkdown(msg, true, false))
} }
} }