瀏覽代碼

Update mautrix-go

Tulir Asokan 5 年之前
父節點
當前提交
acc25a02e4
共有 20 個文件被更改,包括 454 次插入465 次删除
  1. 8 7
      commands.go
  2. 4 5
      community.go
  3. 26 26
      config/bridge.go
  4. 59 97
      custompuppet.go
  5. 4 3
      database/message.go
  6. 14 13
      database/portal.go
  7. 9 8
      database/puppet.go
  8. 29 28
      database/statestore.go
  9. 6 6
      database/upgrades/2019-08-25-move-state-store-to-db.go
  10. 5 4
      database/user.go
  11. 8 8
      formatting.go
  12. 4 4
      go.mod
  13. 22 0
      go.sum
  14. 17 17
      main.go
  15. 53 48
      matrix.go
  16. 132 134
      portal.go
  17. 3 3
      provisioning.go
  18. 12 10
      puppet.go
  19. 0 9
      types/types.go
  20. 39 35
      user.go

+ 8 - 7
commands.go

@@ -1,5 +1,5 @@
 // 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
 // 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-appservice"
+	"maunium.net/go/mautrix/event"
 	"maunium.net/go/mautrix/format"
+	"maunium.net/go/mautrix/id"
 
 	"maunium.net/go/mautrix-whatsapp/database"
-	"maunium.net/go/mautrix-whatsapp/types"
 	"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
 )
 
@@ -51,7 +52,7 @@ type CommandEvent struct {
 	Bot     *appservice.IntentAPI
 	Bridge  *Bridge
 	Handler *CommandHandler
-	RoomID  types.MatrixRoomID
+	RoomID  id.RoomID
 	User    *User
 	Command string
 	Args    []string
@@ -59,20 +60,20 @@ type CommandEvent struct {
 
 // Reply sends a reply to command as notice
 func (ce *CommandEvent) Reply(msg string, args ...interface{}) {
-	content := format.RenderMarkdown(fmt.Sprintf(msg, args...))
-	content.MsgType = mautrix.MsgNotice
+	content := format.RenderMarkdown(fmt.Sprintf(msg, args...), true, false)
+	content.MsgType = event.MsgNotice
 	room := ce.User.ManagementRoom
 	if len(room) == 0 {
 		room = ce.RoomID
 	}
-	_, err := ce.Bot.SendMessageEvent(room, mautrix.EventMessage, content)
+	_, err := ce.Bot.SendMessageEvent(room, event.EventMessage, content)
 	if err != nil {
 		ce.Handler.log.Warnfln("Failed to reply to command from %s: %v", ce.User.MXID, err)
 	}
 }
 
 // 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, " ")
 	ce := &CommandEvent{
 		Bot:     handler.bridge.Bot,

+ 4 - 5
community.go

@@ -1,5 +1,5 @@
 // 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
 // it under the terms of the GNU Affero General Public License as published by
@@ -21,7 +21,6 @@ import (
 	"net/http"
 
 	"maunium.net/go/mautrix"
-	appservice "maunium.net/go/mautrix-appservice"
 )
 
 func (user *User) inviteToCommunity() {
@@ -51,7 +50,7 @@ func (user *User) createCommunity() {
 		return
 	}
 
-	localpart, server := appservice.ParseUserID(user.MXID)
+	localpart, server, _ := user.MXID.Parse()
 	community := user.bridge.Config.Bridge.FormatCommunity(localpart, server)
 	user.log.Debugln("Creating personal filtering community", community)
 	bot := user.bridge.Bot
@@ -100,8 +99,8 @@ func (user *User) addPuppetToCommunity(puppet *Puppet) bool {
 			"type": "private",
 		},
 	}
-	url = bot.BuildURLWithQuery([]string{"groups", user.CommunityID, "self", "accept_invite"}, map[string]string{
-		"user_id": puppet.MXID,
+	url = bot.BuildURLWithQuery(mautrix.URLPath{"groups", user.CommunityID, "self", "accept_invite"}, map[string]string{
+		"user_id": puppet.MXID.String(),
 	})
 	_, err = bot.MakeRequest(http.MethodPut, url, &reqBody, nil)
 	if err != nil {

+ 26 - 26
config/bridge.go

@@ -1,5 +1,5 @@
 // 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
 // it under the terms of the GNU Affero General Public License as published by
@@ -24,8 +24,8 @@ import (
 
 	"github.com/Rhymen/go-whatsapp"
 
-	"maunium.net/go/mautrix"
-	"maunium.net/go/mautrix-appservice"
+	"maunium.net/go/mautrix/event"
+	"maunium.net/go/mautrix/id"
 
 	"maunium.net/go/mautrix-whatsapp/types"
 )
@@ -54,8 +54,8 @@ type BridgeConfig struct {
 	RecoverHistory     bool   `yaml:"recovery_history_backfill"`
 	SyncChatMaxAge     uint64 `yaml:"sync_max_chat_age"`
 
-	SyncWithCustomPuppets bool `yaml:"sync_with_custom_puppets"`
-	LoginSharedSecret string `yaml:"login_shared_secret"`
+	SyncWithCustomPuppets bool   `yaml:"sync_with_custom_puppets"`
+	LoginSharedSecret     string `yaml:"login_shared_secret"`
 
 	InviteOwnPuppetForBackfilling bool `yaml:"invite_own_puppet_for_backfilling"`
 	PrivateChatPortalMeta         bool `yaml:"private_chat_portal_meta"`
@@ -127,7 +127,7 @@ func (bc *BridgeConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
 }
 
 type UsernameTemplateArgs struct {
-	UserID string
+	UserID id.UserID
 }
 
 func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) (string, int8) {
@@ -232,25 +232,25 @@ func (pc *PermissionConfig) MarshalYAML() (interface{}, error) {
 	return rawPC, nil
 }
 
-func (pc PermissionConfig) IsRelaybotWhitelisted(userID string) bool {
+func (pc PermissionConfig) IsRelaybotWhitelisted(userID id.UserID) bool {
 	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
 }
 
-func (pc PermissionConfig) IsAdmin(userID string) bool {
+func (pc PermissionConfig) IsAdmin(userID id.UserID) bool {
 	return pc.GetPermissionLevel(userID) >= PermissionLevelAdmin
 }
 
-func (pc PermissionConfig) GetPermissionLevel(userID string) PermissionLevel {
-	permissions, ok := pc[userID]
+func (pc PermissionConfig) GetPermissionLevel(userID id.UserID) PermissionLevel {
+	permissions, ok := pc[string(userID)]
 	if ok {
 		return permissions
 	}
 
-	_, homeserver := appservice.ParseUserID(userID)
+	_, homeserver, _ := userID.Parse()
 	permissions, ok = pc[homeserver]
 	if len(homeserver) > 0 && ok {
 		return permissions
@@ -265,12 +265,12 @@ func (pc PermissionConfig) GetPermissionLevel(userID string) PermissionLevel {
 }
 
 type RelaybotConfig struct {
-	Enabled        bool                 `yaml:"enabled"`
-	ManagementRoom string               `yaml:"management"`
-	InviteUsers    []types.MatrixUserID `yaml:"invites"`
+	Enabled        bool        `yaml:"enabled"`
+	ManagementRoom id.RoomID   `yaml:"management"`
+	InviteUsers    []id.UserID `yaml:"invites"`
 
-	MessageFormats   map[mautrix.MessageType]string `yaml:"message_formats"`
-	messageTemplates *template.Template             `yaml:"-"`
+	MessageFormats   map[event.MessageType]string `yaml:"message_formats"`
+	messageTemplates *template.Template           `yaml:"-"`
 }
 
 type umRelaybotConfig RelaybotConfig
@@ -293,25 +293,25 @@ func (rc *RelaybotConfig) UnmarshalYAML(unmarshal func(interface{}) error) error
 }
 
 type Sender struct {
-	UserID types.MatrixUserID
-	mautrix.Member
+	UserID id.UserID
+	*event.MemberEventContent
 }
 
 type formatData struct {
 	Sender  Sender
 	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
-	err := rc.messageTemplates.ExecuteTemplate(&output, string(evt.Content.MsgType), formatData{
+	err := rc.messageTemplates.ExecuteTemplate(&output, string(content.MsgType), formatData{
 		Sender: Sender{
-			UserID: evt.Sender,
-			Member: member,
+			UserID:             sender,
+			MemberEventContent: member,
 		},
-		Content: evt.Content,
-		Message: evt.Content.FormattedBody,
+		Content: content,
+		Message: content.FormattedBody,
 	})
 	return output.String(), err
 }

+ 59 - 97
custompuppet.go

@@ -1,5 +1,5 @@
 // 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
 // it under the terms of the GNU Affero General Public License as published by
@@ -20,17 +20,16 @@ import (
 	"crypto/hmac"
 	"crypto/sha512"
 	"encoding/hex"
-	"encoding/json"
-	"fmt"
-	"os"
-	"strings"
 	"time"
 
 	"github.com/pkg/errors"
 
 	"github.com/Rhymen/go-whatsapp"
+
 	"maunium.net/go/mautrix"
 	appservice "maunium.net/go/mautrix-appservice"
+	"maunium.net/go/mautrix/event"
+	"maunium.net/go/mautrix/id"
 )
 
 var (
@@ -38,7 +37,7 @@ var (
 	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
 	if puppet.customIntent != nil {
 		puppet.stopSyncing()
@@ -63,12 +62,12 @@ func (puppet *Puppet) SwitchCustomMXID(accessToken string, mxid string) error {
 	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.Write([]byte(mxid))
 	resp, err := puppet.bridge.AS.BotClient().Login(&mautrix.ReqLogin{
 		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)),
 		DeviceID:                 "WhatsApp Bridge",
 		InitialDeviceDisplayName: "WhatsApp Bridge",
@@ -87,13 +86,13 @@ func (puppet *Puppet) newCustomIntent() (*appservice.IntentAPI, error) {
 	if err != nil {
 		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.Store = puppet
 
 	ia := puppet.bridge.AS.NewIntentAPI("custom")
 	ia.Client = client
-	ia.Localpart = puppet.CustomMXID[1:strings.IndexRune(puppet.CustomMXID, ':')]
+	ia.Localpart, _, _ = puppet.CustomMXID.Parse()
 	ia.UserID = puppet.CustomMXID
 	ia.IsCustomPuppet = true
 	return ia, nil
@@ -117,11 +116,7 @@ func (puppet *Puppet) StartCustomMXID() error {
 		puppet.clearCustomMXID()
 		return err
 	}
-	urlPath := intent.BuildURL("account", "whoami")
-	var resp struct {
-		UserID string `json:"user_id"`
-	}
-	_, err = intent.MakeRequest("GET", urlPath, nil, &resp)
+	resp, err := intent.Whoami()
 	if err != nil {
 		puppet.clearCustomMXID()
 		return err
@@ -131,7 +126,7 @@ func (puppet *Puppet) StartCustomMXID() error {
 		return ErrMismatchingMXID
 	}
 	puppet.customIntent = intent
-	puppet.customTypingIn = make(map[string]bool)
+	puppet.customTypingIn = make(map[id.RoomID]bool)
 	puppet.customUser = puppet.bridge.GetUserByMXID(puppet.CustomMXID)
 	puppet.startSyncing()
 	return nil
@@ -158,28 +153,6 @@ func (puppet *Puppet) stopSyncing() {
 	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 {
 	if !puppet.customUser.IsConnected() {
 		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 {
 			continue
 		}
-		for _, data := range events.Ephemeral.Events {
-			event := parseEvent(roomID, data)
-			if event != nil {
-				switch event.Type {
-				case mautrix.EphemeralEventReceipt:
-					go puppet.handleReceiptEvent(portal, event)
-				case mautrix.EphemeralEventTyping:
-					go puppet.handleTypingEvent(portal, event)
-				}
+		for _, evt := range events.Ephemeral.Events {
+			err := evt.Content.ParseRaw(evt.Type)
+			if err != nil {
+				continue
+			}
+			switch evt.Type {
+			case event.EphemeralEventReceipt:
+				go puppet.handleReceiptEvent(portal, evt)
+			case event.EphemeralEventTyping:
+				go puppet.handleTypingEvent(portal, evt)
 			}
 		}
 	}
-	for _, data := range resp.Presence.Events {
-		event := parsePresenceEvent(data)
-		if event != nil {
-			if event.Sender != puppet.CustomMXID {
-				continue
-			}
-			go puppet.handlePresenceEvent(event)
+	for _, evt := range resp.Presence.Events {
+		if evt.Sender != puppet.CustomMXID {
+			continue
 		}
+		err := evt.Content.ParseRaw(evt.Type)
+		if err != nil {
+			continue
+		}
+		go puppet.handlePresenceEvent(evt)
 	}
 	return nil
 }
 
-func (puppet *Puppet) handlePresenceEvent(event *mautrix.Event) {
+func (puppet *Puppet) handlePresenceEvent(event *event.Event) {
 	presence := whatsapp.PresenceAvailable
 	if event.Content.Raw["presence"].(string) != "online" {
 		presence = whatsapp.PresenceUnavailable
@@ -228,13 +203,9 @@ func (puppet *Puppet) handlePresenceEvent(event *mautrix.Event) {
 	}
 }
 
-func (puppet *Puppet) handleReceiptEvent(portal *Portal, event *mautrix.Event) {
-	for eventID, rawReceipts := range event.Content.Raw {
-		if receipts, ok := rawReceipts.(map[string]interface{}); !ok {
-			continue
-		} else if readReceipt, ok := receipts["m.read"].(map[string]interface{}); !ok {
-			continue
-		} else if _, ok = readReceipt[puppet.CustomMXID].(map[string]interface{}); !ok {
+func (puppet *Puppet) handleReceiptEvent(portal *Portal, event *event.Event) {
+	for eventID, receipts := range *event.Content.AsReceipt() {
+		if _, ok := receipts.Read[puppet.CustomMXID]; !ok {
 			continue
 		}
 		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
-	for _, userID := range event.Content.TypingUserIDs {
+	for _, userID := range evt.Content.AsTyping().UserIDs {
 		if userID == puppet.CustomMXID {
 			isTyping = true
 			break
 		}
 	}
-	if puppet.customTypingIn[event.RoomID] != isTyping {
-		puppet.customTypingIn[event.RoomID] = isTyping
+	if puppet.customTypingIn[evt.RoomID] != isTyping {
+		puppet.customTypingIn[evt.RoomID] = isTyping
 		presence := whatsapp.PresenceComposing
 		if !isTyping {
 			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
 }
 
-func (puppet *Puppet) GetFilterJSON(_ string) json.RawMessage {
-	mxid, _ := json.Marshal(puppet.CustomMXID)
-	return json.RawMessage(fmt.Sprintf(`{
-    "account_data": { "types": [] },
-    "presence": {
-        "senders": [
-            %s
-        ],
-        "types": [
-            "m.presence"
-        ]
-    },
-    "room": {
-        "ephemeral": {
-            "types": [
-                "m.typing",
-                "m.receipt"
-            ]
-        },
-        "include_leave": false,
-        "account_data": { "types": [] },
-        "state": { "types": [] },
-        "timeline": { "types": [] }
-    }
-}`, mxid))
+func (puppet *Puppet) GetFilterJSON(_ id.UserID) *mautrix.Filter {
+	everything := []event.Type{{Type: "*"}}
+	return &mautrix.Filter{
+		Presence: mautrix.FilterPart{
+			Senders: []id.UserID{puppet.CustomMXID},
+			Types:   []event.Type{event.EphemeralEventPresence},
+		},
+		AccountData: mautrix.FilterPart{NotTypes: everything},
+		Room: mautrix.RoomFilter{
+			Ephemeral:    mautrix.FilterPart{Types: []event.Type{event.EphemeralEventTyping, event.EphemeralEventReceipt}},
+			IncludeLeave: false,
+			AccountData:  mautrix.FilterPart{NotTypes: everything},
+			State:        mautrix.FilterPart{NotTypes: everything},
+			Timeline:     mautrix.FilterPart{NotTypes: everything},
+		},
+	}
 }
 
-func (puppet *Puppet) SaveFilterID(_, _ string)             {}
-func (puppet *Puppet) SaveNextBatch(_, nbt string)          { puppet.NextBatch = nbt; puppet.Update() }
-func (puppet *Puppet) SaveRoom(room *mautrix.Room)          {}
-func (puppet *Puppet) LoadFilterID(_ string) string         { return "" }
-func (puppet *Puppet) LoadNextBatch(_ string) string        { return puppet.NextBatch }
-func (puppet *Puppet) LoadRoom(roomID string) *mautrix.Room { return nil }
+func (puppet *Puppet) SaveFilterID(_ id.UserID, _ string)      {}
+func (puppet *Puppet) SaveNextBatch(_ id.UserID, nbt string)   { puppet.NextBatch = nbt; puppet.Update() }
+func (puppet *Puppet) SaveRoom(room *mautrix.Room)             {}
+func (puppet *Puppet) LoadFilterID(_ id.UserID) string            { return "" }
+func (puppet *Puppet) LoadNextBatch(_ id.UserID) string           { return puppet.NextBatch }
+func (puppet *Puppet) LoadRoom(roomID id.RoomID) *mautrix.Room { return nil }

+ 4 - 3
database/message.go

@@ -1,5 +1,5 @@
 // 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
 // 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"
 
 	"maunium.net/go/mautrix-whatsapp/types"
+	"maunium.net/go/mautrix/id"
 )
 
 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)
 }
 
-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 " +
 		"FROM message WHERE mxid=$1", mxid)
 }
@@ -86,7 +87,7 @@ type Message struct {
 
 	Chat      PortalKey
 	JID       types.WhatsAppMessageID
-	MXID      types.MatrixEventID
+	MXID      id.EventID
 	Sender    types.WhatsAppID
 	Timestamp uint64
 	Content   *waProto.Message

+ 14 - 13
database/portal.go

@@ -1,5 +1,5 @@
 // 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
 // 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"
 
 	"maunium.net/go/mautrix-whatsapp/types"
+	"maunium.net/go/mautrix/id"
 )
 
 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)
 }
 
-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)
 }
 
@@ -107,12 +108,12 @@ type Portal struct {
 	log log.Logger
 
 	Key  PortalKey
-	MXID types.MatrixRoomID
+	MXID id.RoomID
 
 	Name      string
 	Topic     string
 	Avatar    string
-	AvatarURL string
+	AvatarURL id.ContentURI
 }
 
 func (portal *Portal) Scan(row Scannable) *Portal {
@@ -124,12 +125,12 @@ func (portal *Portal) Scan(row Scannable) *Portal {
 		}
 		return nil
 	}
-	portal.MXID = mxid.String
-	portal.AvatarURL = avatarURL.String
+	portal.MXID = id.RoomID(mxid.String)
+	portal.AvatarURL, _ = id.ParseContentURI(avatarURL.String)
 	return portal
 }
 
-func (portal *Portal) mxidPtr() *string {
+func (portal *Portal) mxidPtr() *id.RoomID {
 	if len(portal.MXID) > 0 {
 		return &portal.MXID
 	}
@@ -138,19 +139,19 @@ func (portal *Portal) mxidPtr() *string {
 
 func (portal *Portal) Insert() {
 	_, 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 {
 		portal.log.Warnfln("Failed to insert %s: %v", portal.Key, err)
 	}
 }
 
 func (portal *Portal) Update() {
-	var mxid *string
+	var mxid *id.RoomID
 	if len(portal.MXID) > 0 {
 		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",
-		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 {
 		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
 		WHERE "user".jid=user_portal.user_jid
 			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)
 		return nil
 	}
-	var userIDs []types.MatrixUserID
+	var userIDs []id.UserID
 	for rows.Next() {
-		var userID types.MatrixUserID
+		var userID id.UserID
 		err = rows.Scan(&userID)
 		if err != nil {
 			portal.log.Warnln("Failed to scan row:", err)

+ 9 - 8
database/puppet.go

@@ -1,5 +1,5 @@
 // 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
 // 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"
 
 	"maunium.net/go/mautrix-whatsapp/types"
+	"maunium.net/go/mautrix/id"
 )
 
 type PuppetQuery struct {
@@ -56,7 +57,7 @@ func (pq *PuppetQuery) Get(jid types.WhatsAppID) *Puppet {
 	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)
 	if row == nil {
 		return nil
@@ -82,11 +83,11 @@ type Puppet struct {
 
 	JID         types.WhatsAppID
 	Avatar      string
-	AvatarURL   string
+	AvatarURL   id.ContentURI
 	Displayname string
 	NameQuality int8
 
-	CustomMXID  string
+	CustomMXID  id.UserID
 	AccessToken string
 	NextBatch   string
 }
@@ -103,9 +104,9 @@ func (puppet *Puppet) Scan(row Scannable) *Puppet {
 	}
 	puppet.Displayname = displayname.String
 	puppet.Avatar = avatar.String
-	puppet.AvatarURL = avatarURL.String
+	puppet.AvatarURL, _ = id.ParseContentURI(avatarURL.String)
 	puppet.NameQuality = int8(quality.Int64)
-	puppet.CustomMXID = customMXID.String
+	puppet.CustomMXID = id.UserID(customMXID.String)
 	puppet.AccessToken = accessToken.String
 	puppet.NextBatch = nextBatch.String
 	return puppet
@@ -113,7 +114,7 @@ func (puppet *Puppet) Scan(row Scannable) *Puppet {
 
 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)",
-		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 {
 		puppet.log.Warnfln("Failed to insert %s: %v", puppet.JID, err)
 	}
@@ -121,7 +122,7 @@ func (puppet *Puppet) Insert() {
 
 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",
-		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 {
 		puppet.log.Warnfln("Failed to update %s->%s: %v", puppet.JID, err)
 	}

+ 29 - 28
database/statestore.go

@@ -1,5 +1,5 @@
 // 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
 // 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"
 
-	"maunium.net/go/mautrix"
 	"maunium.net/go/mautrix-appservice"
+	"maunium.net/go/mautrix/event"
+	"maunium.net/go/mautrix/id"
 )
 
 type SQLStateStore struct {
@@ -34,7 +35,7 @@ type SQLStateStore struct {
 	db  *Database
 	log log.Logger
 
-	Typing     map[string]map[string]int64
+	Typing     map[id.RoomID]map[id.UserID]int64
 	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)
 	var isRegistered bool
 	err := row.Scan(&isRegistered)
@@ -56,7 +57,7 @@ func (store *SQLStateStore) IsRegistered(userID string) bool {
 	return isRegistered
 }
 
-func (store *SQLStateStore) MarkRegistered(userID string) {
+func (store *SQLStateStore) MarkRegistered(userID id.UserID) {
 	var err error
 	if store.db.dialect == "postgres" {
 		_, 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 {
-	members := make(map[string]mautrix.Member)
+func (store *SQLStateStore) GetRoomMembers(roomID id.RoomID) map[id.UserID]*event.MemberEventContent {
+	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)
 	if err != nil {
 		return members
 	}
-	var userID string
-	var member mautrix.Member
+	var userID id.UserID
+	var member event.MemberEventContent
 	for rows.Next() {
 		err := rows.Scan(&userID, &member.Membership, &member.Displayname, &member.AvatarURL)
 		if err != nil {
 			store.log.Warnfln("Failed to scan member in %s: %v", roomID, err)
 		} else {
-			members[userID] = member
+			members[userID] = &member
 		}
 	}
 	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)
-	membership := mautrix.MembershipLeave
+	membership := event.MembershipLeave
 	err := row.Scan(&membership)
 	if err != nil && err != sql.ErrNoRows {
 		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
 }
 
-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)
 	if !ok {
-		member.Membership = mautrix.MembershipLeave
+		member.Membership = event.MembershipLeave
 	}
 	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)
-	var member mautrix.Member
+	var member event.MemberEventContent
 	err := row.Scan(&member.Membership, &member.Displayname, &member.AvatarURL)
 	if err != nil && err != sql.ErrNoRows {
 		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")
 }
 
-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")
 }
 
-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)
 	for _, allowedMembership := range allowedMemberships {
 		if allowedMembership == membership {
@@ -135,7 +136,7 @@ func (store *SQLStateStore) IsMembership(roomID, userID string, allowedMembershi
 	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
 	if store.db.dialect == "postgres" {
 		_, 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
 	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)
@@ -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)
 	if err != nil {
 		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)
 	if row == nil {
 		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)
 		return
 	}
-	levels = &mautrix.PowerLevels{}
+	levels = &event.PowerLevelsEventContent{}
 	err = json.Unmarshal(data, levels)
 	if err != nil {
 		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
 }
 
-func (store *SQLStateStore) GetPowerLevel(roomID, userID string) int {
+func (store *SQLStateStore) GetPowerLevel(roomID id.RoomID, userID id.UserID) int {
 	if store.db.dialect == "postgres" {
 		row := store.db.QueryRow(`SELECT
 			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)
 }
 
-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" {
 		defaultType := "events_default"
 		defaultValue := 0
@@ -249,7 +250,7 @@ func (store *SQLStateStore) GetPowerLevelRequirement(roomID string, eventType ma
 	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" {
 		defaultType := "events_default"
 		defaultValue := 0

+ 6 - 6
database/upgrades/2019-08-25-move-state-store-to-db.go

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

+ 5 - 4
database/user.go

@@ -1,5 +1,5 @@
 // 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
 // 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/whatsapp-ext"
+	"maunium.net/go/mautrix/id"
 )
 
 type UserQuery struct {
@@ -54,7 +55,7 @@ func (uq *UserQuery) GetAll() (users []*User) {
 	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)
 	if row == nil {
 		return nil
@@ -74,9 +75,9 @@ type User struct {
 	db  *Database
 	log log.Logger
 
-	MXID           types.MatrixUserID
+	MXID           id.UserID
 	JID            types.WhatsAppID
-	ManagementRoom types.MatrixRoomID
+	ManagementRoom id.RoomID
 	Session        *whatsapp.Session
 	LastConnection uint64
 }

+ 8 - 8
formatting.go

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

+ 4 - 4
go.mod

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

+ 22 - 0
go.sum

@@ -1,7 +1,10 @@
 github.com/chai2010/webp v1.1.0 h1:4Ei0/BRroMF9FaXDG2e4OxwFcuW2vcXd+A6tyqTJUQQ=
 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/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/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
 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/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.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/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
 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.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
 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/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/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/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/go.mod h1:gyw9zGup1/Y3ZQUueZaqz3iR/WX9a2Lth4aqEbXjkok=
 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/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/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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 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/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.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/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=

+ 17 - 17
main.go

@@ -1,5 +1,5 @@
 // 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
 // it under the terms of the GNU Affero General Public License as published by
@@ -18,7 +18,6 @@ package main
 
 import (
 	"fmt"
-	"net/http"
 	"os"
 	"os/signal"
 	"sync"
@@ -30,6 +29,7 @@ import (
 
 	"maunium.net/go/mautrix"
 	"maunium.net/go/mautrix-appservice"
+	"maunium.net/go/mautrix/id"
 
 	"maunium.net/go/mautrix-whatsapp/config"
 	"maunium.net/go/mautrix-whatsapp/database"
@@ -107,28 +107,28 @@ type Bridge struct {
 	Formatter      *Formatter
 	Relaybot       *User
 
-	usersByMXID         map[types.MatrixUserID]*User
+	usersByMXID         map[id.UserID]*User
 	usersByJID          map[types.WhatsAppID]*User
 	usersLock           sync.Mutex
-	managementRooms     map[types.MatrixRoomID]*User
+	managementRooms     map[id.RoomID]*User
 	managementRoomsLock sync.Mutex
-	portalsByMXID       map[types.MatrixRoomID]*Portal
+	portalsByMXID       map[id.RoomID]*Portal
 	portalsByJID        map[database.PortalKey]*Portal
 	portalsLock         sync.Mutex
 	puppets             map[types.WhatsAppID]*Puppet
-	puppetsByCustomMXID map[types.MatrixUserID]*Puppet
+	puppetsByCustomMXID map[id.UserID]*Puppet
 	puppetsLock         sync.Mutex
 }
 
 func NewBridge() *Bridge {
 	bridge := &Bridge{
-		usersByMXID:         make(map[types.MatrixUserID]*User),
+		usersByMXID:         make(map[id.UserID]*User),
 		usersByJID:          make(map[types.WhatsAppID]*User),
-		managementRooms:     make(map[types.MatrixRoomID]*User),
-		portalsByMXID:       make(map[types.MatrixRoomID]*Portal),
+		managementRooms:     make(map[id.RoomID]*User),
+		portalsByMXID:       make(map[id.RoomID]*Portal),
 		portalsByJID:        make(map[database.PortalKey]*Portal),
 		puppets:             make(map[types.WhatsAppID]*Puppet),
-		puppetsByCustomMXID: make(map[types.MatrixUserID]*Puppet),
+		puppetsByCustomMXID: make(map[id.UserID]*Puppet),
 	}
 
 	var err error
@@ -141,12 +141,8 @@ func NewBridge() *Bridge {
 }
 
 func (bridge *Bridge) ensureConnection() {
-	url := bridge.Bot.BuildURL("account", "whoami")
-	resp := struct {
-		UserID string `json:"user_id"`
-	}{}
 	for {
-		_, err := bridge.Bot.MakeRequest(http.MethodGet, url, nil, &resp)
+		resp, err := bridge.Bot.Whoami()
 		if err != nil {
 			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?")
@@ -262,10 +258,14 @@ func (bridge *Bridge) UpdateBotProfile() {
 	botConfig := bridge.Config.AppService.Bot
 
 	var err error
+	var mxc id.ContentURI
 	if botConfig.Avatar == "remove" {
-		err = bridge.Bot.SetAvatarURL("")
+		err = bridge.Bot.SetAvatarURL(mxc)
 	} 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 {
 		bridge.Log.Warnln("Failed to update bot avatar:", err)

+ 53 - 48
matrix.go

@@ -1,5 +1,5 @@
 // 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
 // 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/mautrix"
 	"maunium.net/go/mautrix-appservice"
+	"maunium.net/go/mautrix/event"
 	"maunium.net/go/mautrix/format"
-
-	"maunium.net/go/mautrix-whatsapp/types"
+	"maunium.net/go/mautrix/id"
 )
 
 type MatrixHandler struct {
@@ -43,17 +42,17 @@ func NewMatrixHandler(bridge *Bridge) *MatrixHandler {
 		log:    bridge.Log.Sub("Matrix"),
 		cmd:    NewCommandHandler(bridge),
 	}
-	bridge.EventProcessor.On(mautrix.EventMessage, handler.HandleMessage)
-	bridge.EventProcessor.On(mautrix.EventSticker, handler.HandleMessage)
-	bridge.EventProcessor.On(mautrix.EventRedaction, handler.HandleRedaction)
-	bridge.EventProcessor.On(mautrix.StateMember, handler.HandleMembership)
-	bridge.EventProcessor.On(mautrix.StateRoomName, handler.HandleRoomMetadata)
-	bridge.EventProcessor.On(mautrix.StateRoomAvatar, handler.HandleRoomMetadata)
-	bridge.EventProcessor.On(mautrix.StateTopic, handler.HandleRoomMetadata)
+	bridge.EventProcessor.On(event.EventMessage, handler.HandleMessage)
+	bridge.EventProcessor.On(event.EventSticker, handler.HandleMessage)
+	bridge.EventProcessor.On(event.EventRedaction, handler.HandleRedaction)
+	bridge.EventProcessor.On(event.StateMember, handler.HandleMembership)
+	bridge.EventProcessor.On(event.StateRoomName, handler.HandleRoomMetadata)
+	bridge.EventProcessor.On(event.StateRoomAvatar, handler.HandleRoomMetadata)
+	bridge.EventProcessor.On(event.StateTopic, handler.HandleRoomMetadata)
 	return handler
 }
 
-func (mx *MatrixHandler) HandleBotInvite(evt *mautrix.Event) {
+func (mx *MatrixHandler) HandleBotInvite(evt *event.Event) {
 	intent := mx.as.BotIntent()
 
 	user := mx.bridge.GetUserByMXID(evt.Sender)
@@ -61,7 +60,7 @@ func (mx *MatrixHandler) HandleBotInvite(evt *mautrix.Event) {
 		return
 	}
 
-	resp, err := intent.JoinRoom(evt.RoomID, "", nil)
+	resp, err := intent.JoinRoomByID(evt.RoomID)
 	if err != nil {
 		mx.log.Debugln("Failed to join room", evt.RoomID, "with invite from", evt.Sender)
 		return
@@ -97,7 +96,7 @@ func (mx *MatrixHandler) HandleBotInvite(evt *mautrix.Event) {
 	for mxid, _ := range members.Joined {
 		if mxid == intent.UserID || mxid == evt.Sender {
 			continue
-		} else if _, ok := mx.bridge.ParsePuppetMXID(types.MatrixUserID(mxid)); ok {
+		} else if _, ok := mx.bridge.ParsePuppetMXID(mxid); ok {
 			hasPuppets = true
 			continue
 		}
@@ -108,15 +107,16 @@ func (mx *MatrixHandler) HandleBotInvite(evt *mautrix.Event) {
 	}
 
 	if !hasPuppets {
-		user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender))
-		user.SetManagementRoom(types.MatrixRoomID(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.")
+		user := mx.bridge.GetUserByMXID(evt.Sender)
+		user.SetManagementRoom(resp.RoomID)
+		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)
 	}
 }
 
-func (mx *MatrixHandler) HandleMembership(evt *mautrix.Event) {
-	if evt.Content.Membership == "invite" && evt.GetStateKey() == mx.as.BotMXID() {
+func (mx *MatrixHandler) HandleMembership(evt *event.Event) {
+	content := evt.Content.AsMember()
+	if content.Membership == event.MembershipInvite && id.UserID(evt.GetStateKey()) == mx.as.BotMXID() {
 		mx.HandleBotInvite(evt)
 	}
 
@@ -125,15 +125,21 @@ func (mx *MatrixHandler) HandleMembership(evt *mautrix.Event) {
 		return
 	}
 
-	user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender))
+	user := mx.bridge.GetUserByMXID(id.UserID(evt.Sender))
 	if user == nil || !user.Whitelisted || !user.IsConnected() {
 		return
 	}
 
-	if evt.Content.Membership == "leave" {
-		if evt.GetStateKey() == evt.Sender {
-			if portal.IsPrivateChat() || evt.Unsigned.PrevContent.Membership == "join" {
-				portal.HandleMatrixLeave(user)
+	if content.Membership == event.MembershipLeave {
+		if id.UserID(evt.GetStateKey()) == evt.Sender {
+			if evt.Unsigned.PrevContent != nil {
+				_ = 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 {
 			portal.HandleMatrixKick(user, evt)
@@ -141,8 +147,8 @@ func (mx *MatrixHandler) HandleMembership(evt *mautrix.Event) {
 	}
 }
 
-func (mx *MatrixHandler) HandleRoomMetadata(evt *mautrix.Event) {
-	user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender))
+func (mx *MatrixHandler) HandleRoomMetadata(evt *event.Event) {
+	user := mx.bridge.GetUserByMXID(id.UserID(evt.Sender))
 	if user == nil || !user.Whitelisted || !user.IsConnected() {
 		return
 	}
@@ -154,12 +160,12 @@ func (mx *MatrixHandler) HandleRoomMetadata(evt *mautrix.Event) {
 
 	var resp <-chan string
 	var err error
-	switch evt.Type {
-	case mautrix.StateRoomName:
-		resp, err = user.Conn.UpdateGroupSubject(evt.Content.Name, portal.Key.JID)
-	case mautrix.StateTopic:
-		resp, err = user.Conn.UpdateGroupDescription(portal.Key.JID, evt.Content.Topic)
-	case mautrix.StateRoomAvatar:
+	switch content := evt.Content.Parsed.(type) {
+	case *event.RoomNameEventContent:
+		resp, err = user.Conn.UpdateGroupSubject(content.Name, portal.Key.JID)
+	case *event.TopicEventContent:
+		resp, err = user.Conn.UpdateGroupDescription(portal.Key.JID, content.Topic)
+	case *event.RoomAvatarEventContent:
 		return
 	}
 	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 {
 		return
 	}
@@ -179,38 +185,37 @@ func (mx *MatrixHandler) HandleMessage(evt *mautrix.Event) {
 		return
 	}
 
-	roomID := types.MatrixRoomID(evt.RoomID)
-	user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender))
+	user := mx.bridge.GetUserByMXID(evt.Sender)
 
 	if !user.RelaybotWhitelisted {
 		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
-		hasCommandPrefix := strings.HasPrefix(evt.Content.Body, commandPrefix)
+		hasCommandPrefix := strings.HasPrefix(content.Body, commandPrefix)
 		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 {
-			mx.cmd.Handle(roomID, user, evt.Content.Body)
+		if hasCommandPrefix || evt.RoomID == user.ManagementRoom {
+			mx.cmd.Handle(evt.RoomID, user, content.Body)
 			return
 		}
 	}
 
-	portal := mx.bridge.GetPortalByMXID(roomID)
+	portal := mx.bridge.GetPortalByMXID(evt.RoomID)
 	if portal != nil && (user.Whitelisted || portal.HasRelaybot()) {
 		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 {
 		return
 	}
 
-	roomID := types.MatrixRoomID(evt.RoomID)
-	user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender))
+	user := mx.bridge.GetUserByMXID(id.UserID(evt.Sender))
 
 	if !user.Whitelisted {
 		return
@@ -221,13 +226,13 @@ func (mx *MatrixHandler) HandleRedaction(evt *mautrix.Event) {
 	} else if !user.IsConnected() {
 		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. "+
-			"Use `%[2]s reconnect` to reconnect.", user.MXID, mx.bridge.Config.Bridge.CommandPrefix))
-		msg.MsgType = mautrix.MsgNotice
-		_, _ = mx.bridge.Bot.SendMessageEvent(roomID, mautrix.EventMessage, msg)
+			"Use `%[2]s reconnect` to reconnect.", user.MXID, mx.bridge.Config.Bridge.CommandPrefix), true, false)
+		msg.MsgType = event.MsgNotice
+		_, _ = mx.bridge.Bot.SendMessageEvent(evt.RoomID, event.EventMessage, msg)
 		return
 	}
 
-	portal := mx.bridge.GetPortalByMXID(roomID)
+	portal := mx.bridge.GetPortalByMXID(evt.RoomID)
 	if portal != nil {
 		portal.HandleMatrixRedaction(user, evt)
 	}

+ 132 - 134
portal.go

@@ -1,5 +1,5 @@
 // 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
 // it under the terms of the GNU Affero General Public License as published by
@@ -20,7 +20,6 @@ import (
 	"bytes"
 	"encoding/gob"
 	"encoding/hex"
-	"encoding/json"
 	"fmt"
 	"html"
 	"image"
@@ -43,14 +42,16 @@ import (
 
 	"maunium.net/go/mautrix"
 	"maunium.net/go/mautrix-appservice"
+	"maunium.net/go/mautrix/event"
 	"maunium.net/go/mautrix/format"
+	"maunium.net/go/mautrix/id"
 
 	"maunium.net/go/mautrix-whatsapp/database"
 	"maunium.net/go/mautrix-whatsapp/types"
 	"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()
 	defer bridge.portalsLock.Unlock()
 	portal, ok := bridge.portalsByMXID[mxid]
@@ -233,7 +234,7 @@ func init() {
 	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.Chat = portal.Key
 	msg.JID = message.GetKey().GetId()
@@ -269,7 +270,7 @@ func (portal *Portal) startHandling(info whatsapp.MessageInfo) bool {
 	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.log.Debugln("Handled message", message.GetKey().GetId(), "->", mxid)
 }
@@ -416,7 +417,7 @@ func (portal *Portal) UpdateMetadata(user *User) bool {
 	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 {
 		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)
 	if err != nil {
 		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
 	nope := 99
 	invite := 99
 	if portal.bridge.Config.Bridge.AllowUserInvite {
 		invite = 0
 	}
-	return &mautrix.PowerLevels{
+	return &event.PowerLevelsEventContent{
 		UsersDefault:    anyone,
 		EventsDefault:   anyone,
 		RedactPtr:       &anyone,
 		StateDefaultPtr: &nope,
 		BanPtr:          &nope,
 		InvitePtr:       &invite,
-		Users: map[string]int{
+		Users: map[id.UserID]int{
 			portal.MainIntent().UserID: 100,
 		},
 		Events: map[string]int{
-			mautrix.StateRoomName.Type:   anyone,
-			mautrix.StateRoomAvatar.Type: anyone,
-			mautrix.StateTopic.Type:      anyone,
+			event.StateRoomName.Type:   anyone,
+			event.StateRoomAvatar.Type: anyone,
+			event.StateTopic.Type:      anyone,
 		},
 	}
 }
@@ -559,9 +560,9 @@ func (portal *Portal) RestrictMetadataChanges(restrict bool) {
 		newLevel = 50
 	}
 	changed := false
-	changed = levels.EnsureEventLevel(mautrix.StateRoomName, newLevel) || changed
-	changed = levels.EnsureEventLevel(mautrix.StateRoomAvatar, newLevel) || changed
-	changed = levels.EnsureEventLevel(mautrix.StateTopic, newLevel) || changed
+	changed = levels.EnsureEventLevel(event.StateRoomName, newLevel) || changed
+	changed = levels.EnsureEventLevel(event.StateRoomAvatar, newLevel) || changed
+	changed = levels.EnsureEventLevel(event.StateTopic, newLevel) || changed
 	if changed {
 		_, err = portal.MainIntent().SetPowerLevels(portal.MXID, levels)
 		if err != nil {
@@ -749,22 +750,22 @@ func (portal *Portal) CreateMatrixRoom(user *User) error {
 		portal.UpdateAvatar(user, nil)
 	}
 
-	initialState := []*mautrix.Event{{
-		Type: mautrix.StatePowerLevels,
-		Content: mautrix.Content{
-			PowerLevels: portal.GetBasePowerLevels(),
+	initialState := []*event.Event{{
+		Type: event.StatePowerLevels,
+		Content: event.Content{
+			Parsed: portal.GetBasePowerLevels(),
 		},
 	}}
-	if len(portal.AvatarURL) > 0 {
-		initialState = append(initialState, &mautrix.Event{
-			Type: mautrix.StateRoomAvatar,
-			Content: mautrix.Content{
-				URL: portal.AvatarURL,
+	if !portal.AvatarURL.IsEmpty() {
+		initialState = append(initialState, &event.Event{
+			Type: event.StateRoomAvatar,
+			Content: event.Content{
+				Parsed: event.RoomAvatarEventContent{URL: portal.AvatarURL},
 			},
 		})
 	}
 
-	invite := []string{user.MXID}
+	invite := []id.UserID{user.MXID}
 	if user.IsRelaybot {
 		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)
 }
 
-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 {
 		return
 	}
 	message := portal.bridge.DB.Message.GetByJID(portal.Key, info.QuotedMessageID)
 	if message != nil {
-		event, err := portal.MainIntent().GetEvent(portal.MXID, message.MXID)
+		evt, err := portal.MainIntent().GetEvent(portal.MXID, message.MXID)
 		if err != nil {
 			portal.log.Warnln("Failed to get reply target:", err)
 			return
 		}
-		event.Content.RemoveReplyFallback()
-		content.SetReply(event)
+		content.SetReply(evt)
 	}
 	return
 }
@@ -908,32 +908,6 @@ func (portal *Portal) HandleFakeMessage(source *User, message FakeMessage) {
 	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) {
 	if !portal.startHandling(message.Info) {
 		return
@@ -944,16 +918,21 @@ func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessa
 		return
 	}
 
-	content := &mautrix.Content{
+	content := &event.MessageEventContent{
 		Body:    message.Text,
-		MsgType: mautrix.MsgText,
+		MsgType: event.MsgText,
 	}
 
 	portal.bridge.Formatter.ParseWhatsApp(content)
 	portal.SetReply(content, message.ContextInfo)
 
 	_, _ = 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 {
 		portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err)
 		return
@@ -1016,10 +995,10 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte,
 		fileName += exts[0]
 	}
 
-	content := &mautrix.Content{
+	content := &event.MessageEventContent{
 		Body: fileName,
-		URL:  uploaded.ContentURI,
-		Info: &mautrix.FileInfo{
+		URL:  uploaded.ContentURI.CUString(),
+		Info: &event.FileInfo{
 			Size:     len(data),
 			MimeType: mimeType,
 		},
@@ -1030,9 +1009,9 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte,
 		thumbnailMime := http.DetectContentType(thumbnail)
 		uploadedThumbnail, _ := intent.UploadBytes(thumbnail, thumbnailMime)
 		if uploadedThumbnail != nil {
-			content.Info.ThumbnailURL = uploadedThumbnail.ContentURI
+			content.Info.ThumbnailURL = uploadedThumbnail.ContentURI.CUString()
 			cfg, _, _ := image.DecodeConfig(bytes.NewReader(data))
-			content.Info.ThumbnailInfo = &mautrix.FileInfo{
+			content.Info.ThumbnailInfo = &event.FileInfo{
 				Size:     len(thumbnail),
 				Width:    cfg.Width,
 				Height:   cfg.Height,
@@ -1044,40 +1023,50 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte,
 	switch strings.ToLower(strings.Split(mimeType, "/")[0]) {
 	case "image":
 		if !sendAsSticker {
-			content.MsgType = mautrix.MsgImage
+			content.MsgType = event.MsgImage
 		}
 		cfg, _, _ := image.DecodeConfig(bytes.NewReader(data))
 		content.Info.Width = cfg.Width
 		content.Info.Height = cfg.Height
 	case "video":
-		content.MsgType = mautrix.MsgVideo
+		content.MsgType = event.MsgVideo
 	case "audio":
-		content.MsgType = mautrix.MsgAudio
+		content.MsgType = event.MsgAudio
 	default:
-		content.MsgType = mautrix.MsgFile
+		content.MsgType = event.MsgFile
 	}
 
 	_, _ = intent.UserTyping(portal.MXID, false, 0)
 	ts := int64(info.Timestamp * 1000)
-	eventType := mautrix.EventMessage
+	eventType := event.EventMessage
 	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 {
 		portal.log.Errorfln("Failed to handle message %s: %v", info.Id, err)
 		return
 	}
 
 	if len(caption) > 0 {
-		captionContent := &mautrix.Content{
+		captionContent := &event.MessageEventContent{
 			Body:    caption,
-			MsgType: mautrix.MsgNotice,
+			MsgType: event.MsgNotice,
 		}
 
 		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 {
 			portal.log.Warnfln("Failed to handle caption of message %s: %v", info.Id, err)
 		}
@@ -1094,14 +1083,17 @@ func makeMessageID() *string {
 	return &str
 }
 
-func (portal *Portal) downloadThumbnail(evt *mautrix.Event) []byte {
-	if evt.Content.Info == nil || len(evt.Content.Info.ThumbnailURL) == 0 {
+func (portal *Portal) downloadThumbnail(content *event.MessageEventContent, id id.EventID) []byte {
+	if len(content.GetInfo().ThumbnailURL) == 0 {
 		return nil
 	}
-
-	thumbnail, err := portal.MainIntent().DownloadBytes(evt.Content.Info.ThumbnailURL)
+	mxc, err := content.GetInfo().ThumbnailURL.Parse()
 	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
 	}
 	thumbnailType := http.DetectContentType(thumbnail)
@@ -1121,30 +1113,31 @@ func (portal *Portal) downloadThumbnail(evt *mautrix.Event) []byte {
 		Quality: jpeg.DefaultQuality,
 	})
 	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 buf.Bytes()
 }
 
-func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool, evt *mautrix.Event, mediaType whatsapp.MediaType) *MediaUpload {
-	if evt.Content.Info == nil {
-		evt.Content.Info = &mautrix.FileInfo{}
-	}
+func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool, content *event.MessageEventContent, id id.EventID, mediaType whatsapp.MediaType) *MediaUpload {
 	var caption string
 	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 {
-		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
 	}
 
-	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 {
-		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
 	}
 
@@ -1155,7 +1148,7 @@ func (portal *Portal) preprocessMatrixMedia(sender *User, relaybotFormatted bool
 		FileEncSHA256: fileEncSHA256,
 		FileSHA256:    fileSHA256,
 		FileLength:    fileLength,
-		Thumbnail:     portal.downloadThumbnail(evt),
+		Thumbnail:     portal.downloadThumbnail(content, id),
 	}
 }
 
@@ -1169,7 +1162,7 @@ type MediaUpload struct {
 	Thumbnail     []byte
 }
 
-func (portal *Portal) sendMatrixConnectionError(sender *User, eventID string) bool {
+func (portal *Portal) sendMatrixConnectionError(sender *User, eventID id.EventID) bool {
 	if !sender.HasSession() {
 		portal.log.Debugln("Ignoring event", eventID, "from", sender.MXID, "as user has no session")
 		return true
@@ -1183,9 +1176,9 @@ func (portal *Portal) sendMatrixConnectionError(sender *User, eventID string) bo
 		if sender.IsLoginInProgress() {
 			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.MsgType = mautrix.MsgNotice
-		_, err := portal.MainIntent().SendMessageEvent(portal.MXID, mautrix.EventMessage, msg)
+		msg := format.RenderMarkdown("\u26a0 You are not connected to WhatsApp, so your message was not bridged. " + reconnect, true, false)
+		msg.MsgType = event.MsgNotice
+		_, err := portal.MainIntent().SendMessageEvent(portal.MXID, event.EventMessage, msg)
 		if err != nil {
 			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
 }
 
-func (portal *Portal) addRelaybotFormat(user *User, evt *mautrix.Event) bool {
-	member := portal.MainIntent().Member(portal.MXID, evt.Sender)
+func (portal *Portal) addRelaybotFormat(sender *User, content *event.MessageEventContent) bool {
+	member := portal.MainIntent().Member(portal.MXID, sender.MXID)
 	if len(member.Displayname) == 0 {
-		member.Displayname = evt.Sender
+		member.Displayname = string(sender.MXID)
 	}
 
-	if evt.Content.Format != mautrix.FormatHTML {
-		evt.Content.FormattedBody = strings.Replace(html.EscapeString(evt.Content.Body), "\n", "<br/>", -1)
-		evt.Content.Format = mautrix.FormatHTML
+	if content.Format != event.FormatHTML {
+		content.FormattedBody = strings.Replace(html.EscapeString(content.Body), "\n", "<br/>", -1)
+		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 {
 		portal.log.Errorln("Failed to apply relaybot format:", err)
 	}
-	evt.Content.FormattedBody = data
+	content.FormattedBody = data
 	return true
 }
 
-func (portal *Portal) HandleMatrixMessage(sender *User, evt *mautrix.Event) {
+func (portal *Portal) HandleMatrixMessage(sender *User, evt *event.Event) {
 	if !portal.HasRelaybot() && (
 		(portal.IsPrivateChat() && sender.JID != portal.Key.Receiver) ||
 			portal.sendMatrixConnectionError(sender, evt.ID)) {
 		return
 	}
+	content := evt.Content.AsMessage()
+	if content == nil {
+		return
+	}
 	portal.log.Debugfln("Received event %s", evt.ID)
 
+
 	ts := uint64(evt.Timestamp / 1000)
 	status := waProto.WebMessageInfo_ERROR
 	fromMe := true
@@ -1234,9 +1232,9 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *mautrix.Event) {
 		Status:           &status,
 	}
 	ctxInfo := &waProto.ContextInfo{}
-	replyToID := evt.Content.GetReplyTo()
+	replyToID := content.GetReplyTo()
 	if len(replyToID) > 0 {
-		evt.Content.RemoveReplyFallback()
+		content.RemoveReplyFallback()
 		msg := portal.bridge.DB.Message.GetByMXID(replyToID)
 		if msg != nil && msg.Content != nil {
 			ctxInfo.StanzaId = &msg.JID
@@ -1254,21 +1252,21 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *mautrix.Event) {
 				return
 			}
 		} else {
-			relaybotFormatted = portal.addRelaybotFormat(sender, evt)
+			relaybotFormatted = portal.addRelaybotFormat(sender, content)
 			sender = portal.bridge.Relaybot
 		}
 	}
-	if evt.Type == mautrix.EventSticker {
-		evt.Content.MsgType = mautrix.MsgImage
+	if evt.Type == event.EventSticker {
+		content.MsgType = event.MsgImage
 	}
 	var err error
-	switch evt.Content.MsgType {
-	case mautrix.MsgText, mautrix.MsgEmote, mautrix.MsgNotice:
-		text := evt.Content.Body
-		if evt.Content.Format == mautrix.FormatHTML {
-			text = portal.bridge.Formatter.ParseMatrix(evt.Content.FormattedBody)
+	switch content.MsgType {
+	case event.MsgText, event.MsgEmote, event.MsgNotice:
+		text := content.Body
+		if content.Format == event.FormatHTML {
+			text = portal.bridge.Formatter.ParseMatrix(content.FormattedBody)
 		}
-		if evt.Content.MsgType == mautrix.MsgEmote && !relaybotFormatted {
+		if content.MsgType == event.MsgEmote && !relaybotFormatted {
 			text = "/me " + text
 		}
 		ctxInfo.MentionedJid = mentionRegex.FindAllString(text, -1)
@@ -1283,8 +1281,8 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *mautrix.Event) {
 		} else {
 			info.Message.Conversation = &text
 		}
-	case mautrix.MsgImage:
-		media := portal.preprocessMatrixMedia(sender, relaybotFormatted, evt, whatsapp.MediaImage)
+	case event.MsgImage:
+		media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaImage)
 		if media == nil {
 			return
 		}
@@ -1293,53 +1291,53 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *mautrix.Event) {
 			JpegThumbnail: media.Thumbnail,
 			Url:           &media.URL,
 			MediaKey:      media.MediaKey,
-			Mimetype:      &evt.Content.GetInfo().MimeType,
+			Mimetype:      &content.GetInfo().MimeType,
 			FileEncSha256: media.FileEncSHA256,
 			FileSha256:    media.FileSHA256,
 			FileLength:    &media.FileLength,
 		}
-	case mautrix.MsgVideo:
-		media := portal.preprocessMatrixMedia(sender, relaybotFormatted, evt, whatsapp.MediaVideo)
+	case event.MsgVideo:
+		media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaVideo)
 		if media == nil {
 			return
 		}
-		duration := uint32(evt.Content.GetInfo().Duration)
+		duration := uint32(content.GetInfo().Duration)
 		info.Message.VideoMessage = &waProto.VideoMessage{
 			Caption:       &media.Caption,
 			JpegThumbnail: media.Thumbnail,
 			Url:           &media.URL,
 			MediaKey:      media.MediaKey,
-			Mimetype:      &evt.Content.GetInfo().MimeType,
+			Mimetype:      &content.GetInfo().MimeType,
 			Seconds:       &duration,
 			FileEncSha256: media.FileEncSHA256,
 			FileSha256:    media.FileSHA256,
 			FileLength:    &media.FileLength,
 		}
-	case mautrix.MsgAudio:
-		media := portal.preprocessMatrixMedia(sender, relaybotFormatted, evt, whatsapp.MediaAudio)
+	case event.MsgAudio:
+		media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaAudio)
 		if media == nil {
 			return
 		}
-		duration := uint32(evt.Content.GetInfo().Duration)
+		duration := uint32(content.GetInfo().Duration)
 		info.Message.AudioMessage = &waProto.AudioMessage{
 			Url:           &media.URL,
 			MediaKey:      media.MediaKey,
-			Mimetype:      &evt.Content.GetInfo().MimeType,
+			Mimetype:      &content.GetInfo().MimeType,
 			Seconds:       &duration,
 			FileEncSha256: media.FileEncSHA256,
 			FileSha256:    media.FileSHA256,
 			FileLength:    &media.FileLength,
 		}
-	case mautrix.MsgFile:
-		media := portal.preprocessMatrixMedia(sender, relaybotFormatted, evt, whatsapp.MediaDocument)
+	case event.MsgFile:
+		media := portal.preprocessMatrixMedia(sender, relaybotFormatted, content, evt.ID, whatsapp.MediaDocument)
 		if media == nil {
 			return
 		}
 		info.Message.DocumentMessage = &waProto.DocumentMessage{
 			Url:           &media.URL,
-			FileName:      &evt.Content.Body,
+			FileName:      &content.Body,
 			MediaKey:      media.MediaKey,
-			Mimetype:      &evt.Content.GetInfo().MimeType,
+			Mimetype:      &content.GetInfo().MimeType,
 			FileEncSha256: media.FileEncSHA256,
 			FileSha256:    media.FileSHA256,
 			FileLength:    &media.FileLength,
@@ -1353,9 +1351,9 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *mautrix.Event) {
 	_, err = sender.Conn.Send(info)
 	if err != nil {
 		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.MsgType = mautrix.MsgNotice
-		_, err := portal.MainIntent().SendMessageEvent(portal.MXID, mautrix.EventMessage, msg)
+		msg := format.RenderMarkdown(fmt.Sprintf("\u26a0 Your message may not have been bridged: %v", err), false, false)
+		msg.MsgType = event.MsgNotice
+		_, err := portal.MainIntent().SendMessageEvent(portal.MXID, event.EventMessage, msg)
 		if err != nil {
 			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 {
 		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
 }

+ 3 - 3
provisioning.go

@@ -26,8 +26,8 @@ import (
 	"github.com/gorilla/websocket"
 	log "maunium.net/go/maulogger/v2"
 
-	"maunium.net/go/mautrix-whatsapp/types"
 	whatsappExt "maunium.net/go/mautrix-whatsapp/whatsapp-ext"
+	"maunium.net/go/mautrix/id"
 )
 
 type ProvisioningAPI struct {
@@ -61,7 +61,7 @@ func (prov *ProvisioningAPI) AuthMiddleware(h http.Handler) http.Handler {
 			return
 		}
 		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)))
 	})
 }
@@ -300,7 +300,7 @@ var upgrader = websocket.Upgrader{}
 
 func (prov *ProvisioningAPI) Login(w http.ResponseWriter, r *http.Request) {
 	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)
 	if err != nil {

+ 12 - 10
puppet.go

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

+ 0 - 9
types/types.go

@@ -21,12 +21,3 @@ type WhatsAppID = string
 
 // WhatsAppMessageID is the internal ID of a WhatsApp message.
 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

+ 39 - 35
user.go

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