Эх сурвалжийг харах

Add Matrix->WhatsApp replies and other stuff

Tulir Asokan 6 жил өмнө
parent
commit
060516f9cf
5 өөрчлөгдсөн 303 нэмэгдсэн , 103 устгасан
  1. 2 0
      ROADMAP.md
  2. 86 0
      formatting.go
  3. 193 99
      portal.go
  4. 5 1
      puppet.go
  5. 17 3
      user.go

+ 2 - 0
ROADMAP.md

@@ -4,6 +4,7 @@
     * [x] Plain text
     * [x] Plain text
     * [x] Formatted messages
     * [x] Formatted messages
     * [x] Media/files
     * [x] Media/files
+    * [x] Replies
   * [ ] Message redactions
   * [ ] Message redactions
   * [ ] Presence
   * [ ] Presence
   * [ ] Typing notifications
   * [ ] Typing notifications
@@ -24,6 +25,7 @@
     * [x] Plain text
     * [x] Plain text
     * [x] Formatted messages
     * [x] Formatted messages
     * [x] Media/files
     * [x] Media/files
+    * [x] Replies
   * [ ] Message deletions
   * [ ] Message deletions
   * [x] Avatars
   * [x] Avatars
   * [x] Presence
   * [x] Presence

+ 86 - 0
formatting.go

@@ -0,0 +1,86 @@
+// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
+// Copyright (C) 2018 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
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU Affero General Public License for more details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with this program.  If not, see <https://www.gnu.org/licenses/>.
+
+package main
+
+import (
+	"fmt"
+	"regexp"
+	"strings"
+
+	"maunium.net/go/gomatrix/format"
+	"maunium.net/go/mautrix-whatsapp/whatsapp-ext"
+)
+
+func (user *User) newHTMLParser() *format.HTMLParser {
+	return &format.HTMLParser{
+		TabsToSpaces: 4,
+		Newline:      "\n",
+
+		PillConverter: func(mxid, eventID string) string {
+			if mxid[0] == '@' {
+				puppet := user.GetPuppetByMXID(mxid)
+				fmt.Println(mxid, puppet)
+				if puppet != nil {
+					return "@" + puppet.PhoneNumber()
+				}
+			}
+			return mxid
+		},
+		BoldConverter: func(text string) string {
+			return fmt.Sprintf("*%s*", text)
+		},
+		ItalicConverter: func(text string) string {
+			return fmt.Sprintf("_%s_", text)
+		},
+		StrikethroughConverter: func(text string) string {
+			return fmt.Sprintf("~%s~", text)
+		},
+		MonospaceConverter: func(text string) string {
+			return fmt.Sprintf("```%s```", text)
+		},
+		MonospaceBlockConverter: func(text string) string {
+			return fmt.Sprintf("```%s```", text)
+		},
+	}
+}
+
+var italicRegex = regexp.MustCompile("([\\s>~*]|^)_(.+?)_([^a-zA-Z\\d]|$)")
+var boldRegex = regexp.MustCompile("([\\s>_~]|^)\\*(.+?)\\*([^a-zA-Z\\d]|$)")
+var strikethroughRegex = regexp.MustCompile("([\\s>_*]|^)~(.+?)~([^a-zA-Z\\d]|$)")
+var codeBlockRegex = regexp.MustCompile("```(?:.|\n)+?```")
+var mentionRegex = regexp.MustCompile("@[0-9]+")
+
+func (user *User) newWhatsAppFormatMaps() (map[*regexp.Regexp]string, map[*regexp.Regexp]func(string) string) {
+	return map[*regexp.Regexp]string{
+		italicRegex:        "$1<em>$2</em>$3",
+		boldRegex:          "$1<strong>$2</strong>$3",
+		strikethroughRegex: "$1<del>$2</del>$3",
+	}, map[*regexp.Regexp]func(string) string{
+		codeBlockRegex: func(str string) string {
+			str = str[3 : len(str)-3]
+			if strings.ContainsRune(str, '\n') {
+				return fmt.Sprintf("<pre><code>%s</code></pre>", str)
+			}
+			return fmt.Sprintf("<code>%s</code>", str)
+		},
+		mentionRegex: func(str string) string {
+			jid := str[1:] + whatsapp_ext.NewUserSuffix
+			puppet := user.GetPuppetByJID(jid)
+			return fmt.Sprintf(`<a href="https://matrix.to/#/%s">%s</a>`, puppet.MXID, puppet.Displayname)
+		},
+	}
+}

+ 193 - 99
portal.go

@@ -22,17 +22,18 @@ import (
 	"fmt"
 	"fmt"
 	"html"
 	"html"
 	"image"
 	"image"
-	"io"
+	"image/gif"
+	"image/jpeg"
+	"image/png"
 	"math/rand"
 	"math/rand"
 	"mime"
 	"mime"
 	"net/http"
 	"net/http"
-	"regexp"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 
 
 	"github.com/Rhymen/go-whatsapp"
 	"github.com/Rhymen/go-whatsapp"
+	waProto "github.com/Rhymen/go-whatsapp/binary/proto"
 	"maunium.net/go/gomatrix"
 	"maunium.net/go/gomatrix"
-	"maunium.net/go/gomatrix/format"
 	log "maunium.net/go/maulogger"
 	log "maunium.net/go/maulogger"
 	"maunium.net/go/mautrix-appservice"
 	"maunium.net/go/mautrix-appservice"
 	"maunium.net/go/mautrix-whatsapp/database"
 	"maunium.net/go/mautrix-whatsapp/database"
@@ -278,13 +279,11 @@ func (portal *Portal) MarkHandled(jid types.WhatsAppMessageID, mxid types.Matrix
 
 
 func (portal *Portal) GetMessageIntent(info whatsapp.MessageInfo) *appservice.IntentAPI {
 func (portal *Portal) GetMessageIntent(info whatsapp.MessageInfo) *appservice.IntentAPI {
 	if info.FromMe {
 	if info.FromMe {
-		portal.log.Debugln("Unhandled message from me:", info.Id)
-		return nil
+		return portal.user.GetPuppetByJID(portal.user.JID()).Intent()
 	} else if portal.IsPrivateChat() {
 	} else if portal.IsPrivateChat() {
 		return portal.MainIntent()
 		return portal.MainIntent()
 	}
 	}
-	puppet := portal.user.GetPuppetByJID(info.SenderJid)
-	return puppet.Intent()
+	return portal.user.GetPuppetByJID(info.SenderJid).Intent()
 }
 }
 
 
 func (portal *Portal) SetReply(content *gomatrix.Content, info whatsapp.MessageInfo) {
 func (portal *Portal) SetReply(content *gomatrix.Content, info whatsapp.MessageInfo) {
@@ -303,29 +302,14 @@ func (portal *Portal) SetReply(content *gomatrix.Content, info whatsapp.MessageI
 	return
 	return
 }
 }
 
 
-var codeBlockRegex = regexp.MustCompile("```((?:.|\n)+?)```")
-var italicRegex = regexp.MustCompile("([\\s>~*]|^)_(.+?)_([^a-zA-Z\\d]|$)")
-var boldRegex = regexp.MustCompile("([\\s>_~]|^)\\*(.+?)\\*([^a-zA-Z\\d]|$)")
-var strikethroughRegex = regexp.MustCompile("([\\s>_*]|^)~(.+?)~([^a-zA-Z\\d]|$)")
-
-var whatsAppFormat = map[*regexp.Regexp]string{
-	italicRegex:        "$1<em>$2</em>$3",
-	boldRegex:          "$1<strong>$2</strong>$3",
-	strikethroughRegex: "$1<del>$2</del>$3",
-}
-
 func (portal *Portal) ParseWhatsAppFormat(input string) string {
 func (portal *Portal) ParseWhatsAppFormat(input string) string {
 	output := html.EscapeString(input)
 	output := html.EscapeString(input)
-	for regex, replacement := range whatsAppFormat {
+	for regex, replacement := range portal.user.waReplString {
 		output = regex.ReplaceAllString(output, replacement)
 		output = regex.ReplaceAllString(output, replacement)
 	}
 	}
-	output = codeBlockRegex.ReplaceAllStringFunc(output, func(str string) string {
-		str = str[3 : len(str)-3]
-		if strings.ContainsRune(str, '\n') {
-			return fmt.Sprintf("<pre><code>%s</code></pre>", str)
-		}
-		return fmt.Sprintf("<code>%s</code>", str)
-	})
+	for regex, replacer := range portal.user.waReplFunc {
+		output = regex.ReplaceAllStringFunc(output, replacer)
+	}
 	return output
 	return output
 }
 }
 
 
@@ -451,37 +435,47 @@ func (portal *Portal) HandleMediaMessage(download func() ([]byte, error), thumbn
 	portal.log.Debugln("Handled message", info.Id, "->", resp.EventID)
 	portal.log.Debugln("Handled message", info.Id, "->", resp.EventID)
 }
 }
 
 
-var htmlParser = format.HTMLParser{
-	TabsToSpaces: 4,
-	Newline:      "\n",
-
-	PillConverter: func(mxid, eventID string) string {
-		return mxid
-	},
-	BoldConverter: func(text string) string {
-		return fmt.Sprintf("*%s*", text)
-	},
-	ItalicConverter: func(text string) string {
-		return fmt.Sprintf("_%s_", text)
-	},
-	StrikethroughConverter: func(text string) string {
-		return fmt.Sprintf("~%s~", text)
-	},
-	MonospaceConverter: func(text string) string {
-		return fmt.Sprintf("```%s```", text)
-	},
-	MonospaceBlockConverter: func(text string) string {
-		return fmt.Sprintf("```%s```", text)
-	},
-}
-
-func makeMessageID() string {
+func makeMessageID() *string {
 	b := make([]byte, 10)
 	b := make([]byte, 10)
 	rand.Read(b)
 	rand.Read(b)
-	return strings.ToUpper(hex.EncodeToString(b))
+	str := strings.ToUpper(hex.EncodeToString(b))
+	return &str
 }
 }
 
 
-func (portal *Portal) PreprocessMatrixMedia(evt *gomatrix.Event) (string, io.ReadCloser, []byte) {
+func (portal *Portal) downloadThumbnail(evt *gomatrix.Event) []byte {
+	if evt.Content.Info == nil || len(evt.Content.Info.ThumbnailURL) == 0 {
+		return nil
+	}
+
+	thumbnail, err := portal.MainIntent().DownloadBytes(evt.Content.Info.ThumbnailURL)
+	if err != nil {
+		portal.log.Errorln("Failed to download thumbnail in %s: %v", evt.ID, err)
+		return nil
+	}
+	thumbnailType := http.DetectContentType(thumbnail)
+	var img image.Image
+	switch thumbnailType {
+	case "image/png":
+		img, err = png.Decode(bytes.NewReader(thumbnail))
+	case "image/gif":
+		img, err = gif.Decode(bytes.NewReader(thumbnail))
+	case "image/jpeg":
+		return thumbnail
+	default:
+		return nil
+	}
+	var buf bytes.Buffer
+	err = jpeg.Encode(&buf, img, &jpeg.Options{
+		Quality: jpeg.DefaultQuality,
+	})
+	if err != nil {
+		portal.log.Errorln("Failed to re-encode thumbnail in %s: %v", evt.ID, err)
+		return nil
+	}
+	return buf.Bytes()
+}
+
+func (portal *Portal) preprocessMatrixMedia(evt *gomatrix.Event, mediaType whatsapp.MediaType) *MediaUpload {
 	if evt.Content.Info == nil {
 	if evt.Content.Info == nil {
 		evt.Content.Info = &gomatrix.FileInfo{}
 		evt.Content.Info = &gomatrix.FileInfo{}
 	}
 	}
@@ -493,84 +487,184 @@ func (portal *Portal) PreprocessMatrixMedia(evt *gomatrix.Event) (string, io.Rea
 			break
 			break
 		}
 		}
 	}
 	}
-	content, err := portal.MainIntent().Download(evt.Content.URL)
+	content, err := portal.MainIntent().DownloadBytes(evt.Content.URL)
 	if err != nil {
 	if err != nil {
 		portal.log.Errorln("Failed to download media in %s: %v", evt.ID, err)
 		portal.log.Errorln("Failed to download media in %s: %v", evt.ID, err)
-		return "", nil, nil
+		return nil
 	}
 	}
-	thumbnail, err := portal.MainIntent().DownloadBytes(evt.Content.Info.ThumbnailURL)
-	return caption, content, thumbnail
+
+	url, mediaKey, fileEncSHA256, fileSHA256, fileLength, err := portal.user.Conn.Upload(bytes.NewReader(content), mediaType)
+	if err != nil {
+		portal.log.Error("Failed to upload media in %s: %v", evt.ID, err)
+		return nil
+	}
+
+	return &MediaUpload{
+		Caption:       caption,
+		URL:           url,
+		MediaKey:      mediaKey,
+		FileEncSHA256: fileEncSHA256,
+		FileSHA256:    fileSHA256,
+		FileLength:    fileLength,
+		Thumbnail:     portal.downloadThumbnail(evt),
+	}
+}
+
+type MediaUpload struct {
+	Caption       string
+	URL           string
+	MediaKey      []byte
+	FileEncSHA256 []byte
+	FileSHA256    []byte
+	FileLength    uint64
+	Thumbnail     []byte
+}
+
+func (portal *Portal) GetMessage(jid types.WhatsAppMessageID) *waProto.WebMessageInfo {
+	node, err := portal.user.Conn.LoadMessagesBefore(portal.JID, jid, 1)
+	if err != nil {
+		return nil
+	}
+	msgs, ok := node.Content.([]interface{})
+	if !ok {
+		return nil
+	}
+	msg, ok := msgs[0].(*waProto.WebMessageInfo)
+	if !ok {
+		return nil
+	}
+	node, err = portal.user.Conn.LoadMessagesAfter(portal.JID, msg.GetKey().GetId(), 1)
+	if err != nil {
+		return nil
+	}
+	msgs, ok = node.Content.([]interface{})
+	if !ok {
+		return nil
+	}
+	msg, _ = msgs[0].(*waProto.WebMessageInfo)
+	return msg
 }
 }
 
 
 func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {
 func (portal *Portal) HandleMatrixMessage(evt *gomatrix.Event) {
-	info := whatsapp.MessageInfo{
-		Id:        makeMessageID(),
-		RemoteJid: portal.JID,
+	ts := uint64(evt.Timestamp / 1000)
+	status := waProto.WebMessageInfo_ERROR
+	fromMe := true
+	info := &waProto.WebMessageInfo{
+		Key: &waProto.MessageKey{
+			FromMe:    &fromMe,
+			Id:        makeMessageID(),
+			RemoteJid: &portal.JID,
+		},
+		MessageTimestamp: &ts,
+		Message:          &waProto.Message{},
+		Status:           &status,
+	}
+	ctxInfo := &waProto.ContextInfo{}
+	replyToID := evt.Content.GetReplyTo()
+	if len(replyToID) > 0 {
+		evt.Content.RemoveReplyFallback()
+		msg := portal.bridge.DB.Message.GetByMXID(replyToID)
+		if msg != nil {
+			origMsg := portal.GetMessage(msg.JID)
+			if origMsg != nil {
+				ctxInfo.StanzaId = &msg.JID
+				replyMsgSender := origMsg.GetParticipant()
+				if origMsg.GetKey().GetFromMe() {
+					replyMsgSender = portal.user.JID()
+				}
+				ctxInfo.Participant = &replyMsgSender
+				ctxInfo.QuotedMessage = []*waProto.Message{origMsg.Message}
+			}
+		}
 	}
 	}
 	var err error
 	var err error
 	switch evt.Content.MsgType {
 	switch evt.Content.MsgType {
 	case gomatrix.MsgText, gomatrix.MsgEmote:
 	case gomatrix.MsgText, gomatrix.MsgEmote:
 		text := evt.Content.Body
 		text := evt.Content.Body
 		if evt.Content.Format == gomatrix.FormatHTML {
 		if evt.Content.Format == gomatrix.FormatHTML {
-			text = htmlParser.Parse(evt.Content.FormattedBody)
+			text = portal.user.htmlParser.Parse(evt.Content.FormattedBody)
 		}
 		}
 		if evt.Content.MsgType == gomatrix.MsgEmote {
 		if evt.Content.MsgType == gomatrix.MsgEmote {
 			text = "/me " + text
 			text = "/me " + text
 		}
 		}
-		err = portal.user.Conn.Send(whatsapp.TextMessage{
-			Text: text,
-			Info: info,
-		})
+		ctxInfo.MentionedJid = mentionRegex.FindAllString(text, -1)
+		for index, mention := range ctxInfo.MentionedJid {
+			ctxInfo.MentionedJid[index] = mention[1:] + whatsapp_ext.NewUserSuffix
+		}
+		if ctxInfo.StanzaId != nil || ctxInfo.MentionedJid != nil {
+			info.Message.ExtendedTextMessage = &waProto.ExtendedTextMessage{
+				Text:        &text,
+				ContextInfo: ctxInfo,
+			}
+		} else {
+			info.Message.Conversation = &text
+		}
 	case gomatrix.MsgImage:
 	case gomatrix.MsgImage:
-		caption, content, thumbnail := portal.PreprocessMatrixMedia(evt)
-		if content == nil {
+		media := portal.preprocessMatrixMedia(evt, whatsapp.MediaImage)
+		if media == nil {
 			return
 			return
 		}
 		}
-		err = portal.user.Conn.Send(whatsapp.ImageMessage{
-			Caption:   caption,
-			Content:   content,
-			Thumbnail: thumbnail,
-			Type:      evt.Content.Info.MimeType,
-			Info:      info,
-		})
+		info.Message.ImageMessage = &waProto.ImageMessage{
+			Caption:       &media.Caption,
+			JpegThumbnail: media.Thumbnail,
+			Url:           &media.URL,
+			MediaKey:      media.MediaKey,
+			Mimetype:      &evt.Content.GetInfo().MimeType,
+			FileEncSha256: media.FileEncSHA256,
+			FileSha256:    media.FileSHA256,
+			FileLength:    &media.FileLength,
+		}
 	case gomatrix.MsgVideo:
 	case gomatrix.MsgVideo:
-		caption, content, thumbnail := portal.PreprocessMatrixMedia(evt)
-		if content == nil {
+		media := portal.preprocessMatrixMedia(evt, whatsapp.MediaVideo)
+		if media == nil {
 			return
 			return
 		}
 		}
-		err = portal.user.Conn.Send(whatsapp.VideoMessage{
-			Caption:   caption,
-			Content:   content,
-			Thumbnail: thumbnail,
-			Type:      evt.Content.Info.MimeType,
-			Info:      info,
-		})
+		duration := uint32(evt.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,
+			Seconds:       &duration,
+			FileEncSha256: media.FileEncSHA256,
+			FileSha256:    media.FileSHA256,
+			FileLength:    &media.FileLength,
+		}
 	case gomatrix.MsgAudio:
 	case gomatrix.MsgAudio:
-		_, content, _ := portal.PreprocessMatrixMedia(evt)
-		if content == nil {
+		media := portal.preprocessMatrixMedia(evt, whatsapp.MediaAudio)
+		if media == nil {
 			return
 			return
 		}
 		}
-		err = portal.user.Conn.Send(whatsapp.AudioMessage{
-			Content: content,
-			Type:    evt.Content.Info.MimeType,
-			Info:    info,
-		})
+		duration := uint32(evt.Content.GetInfo().Duration)
+		info.Message.AudioMessage = &waProto.AudioMessage{
+			Url:           &media.URL,
+			MediaKey:      media.MediaKey,
+			Mimetype:      &evt.Content.GetInfo().MimeType,
+			Seconds:       &duration,
+			FileEncSha256: media.FileEncSHA256,
+			FileSha256:    media.FileSHA256,
+			FileLength:    &media.FileLength,
+		}
 	case gomatrix.MsgFile:
 	case gomatrix.MsgFile:
-		_, content, thumbnail := portal.PreprocessMatrixMedia(evt)
-		if content == nil {
+		media := portal.preprocessMatrixMedia(evt, whatsapp.MediaDocument)
+		if media == nil {
 			return
 			return
 		}
 		}
-		err = portal.user.Conn.Send(whatsapp.DocumentMessage{
-			Content:   content,
-			Thumbnail: thumbnail,
-			Type:      evt.Content.Info.MimeType,
-			Info:      info,
-		})
+		info.Message.DocumentMessage = &waProto.DocumentMessage{
+			Url:           &media.URL,
+			MediaKey:      media.MediaKey,
+			Mimetype:      &evt.Content.GetInfo().MimeType,
+			FileEncSha256: media.FileEncSHA256,
+			FileSha256:    media.FileSHA256,
+			FileLength:    &media.FileLength,
+		}
 	default:
 	default:
 		portal.log.Debugln("Unhandled Matrix event:", evt)
 		portal.log.Debugln("Unhandled Matrix event:", evt)
 		return
 		return
 	}
 	}
-	portal.MarkHandled(info.Id, evt.ID)
+	err = portal.user.Conn.Send(info)
+	portal.MarkHandled(info.GetKey().GetId(), evt.ID)
 	if err != nil {
 	if err != nil {
 		portal.log.Errorfln("Error handling Matrix event %s: %v", evt.ID, err)
 		portal.log.Errorfln("Error handling Matrix event %s: %v", evt.ID, err)
 	} else {
 	} else {

+ 5 - 1
puppet.go

@@ -32,7 +32,7 @@ import (
 
 
 func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.MatrixUserID, types.WhatsAppID, bool) {
 func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.MatrixUserID, types.WhatsAppID, bool) {
 	userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$",
 	userIDRegex, err := regexp.Compile(fmt.Sprintf("^@%s:%s$",
-		bridge.Config.Bridge.FormatUsername("([0-9]+)", "([0-9]+)"),
+		bridge.Config.Bridge.FormatUsername("(.+)", "([0-9]+)"),
 		bridge.Config.Homeserver.Domain))
 		bridge.Config.Homeserver.Domain))
 	if err != nil {
 	if err != nil {
 		bridge.Log.Warnln("Failed to compile puppet user ID regex:", err)
 		bridge.Log.Warnln("Failed to compile puppet user ID regex:", err)
@@ -138,6 +138,10 @@ type Puppet struct {
 	MXID types.MatrixUserID
 	MXID types.MatrixUserID
 }
 }
 
 
+func (puppet *Puppet) PhoneNumber() string {
+	return strings.Replace(puppet.JID, whatsapp_ext.NewUserSuffix, "", 1)
+}
+
 func (puppet *Puppet) Intent() *appservice.IntentAPI {
 func (puppet *Puppet) Intent() *appservice.IntentAPI {
 	return puppet.bridge.AppService.Intent(puppet.MXID)
 	return puppet.bridge.AppService.Intent(puppet.MXID)
 }
 }

+ 17 - 3
user.go

@@ -17,12 +17,14 @@
 package main
 package main
 
 
 import (
 import (
+	"regexp"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 	"time"
 	"time"
 
 
 	"github.com/Rhymen/go-whatsapp"
 	"github.com/Rhymen/go-whatsapp"
 	"github.com/skip2/go-qrcode"
 	"github.com/skip2/go-qrcode"
+	"maunium.net/go/gomatrix/format"
 	log "maunium.net/go/maulogger"
 	log "maunium.net/go/maulogger"
 	"maunium.net/go/mautrix-whatsapp/database"
 	"maunium.net/go/mautrix-whatsapp/database"
 	"maunium.net/go/mautrix-whatsapp/types"
 	"maunium.net/go/mautrix-whatsapp/types"
@@ -41,6 +43,11 @@ type User struct {
 	portalsLock   sync.Mutex
 	portalsLock   sync.Mutex
 	puppets       map[types.WhatsAppID]*Puppet
 	puppets       map[types.WhatsAppID]*Puppet
 	puppetsLock   sync.Mutex
 	puppetsLock   sync.Mutex
+
+	htmlParser *format.HTMLParser
+
+	waReplString map[*regexp.Regexp]string
+	waReplFunc   map[*regexp.Regexp]func(string) string
 }
 }
 
 
 func (bridge *Bridge) GetUser(userID types.MatrixUserID) *User {
 func (bridge *Bridge) GetUser(userID types.MatrixUserID) *User {
@@ -79,7 +86,7 @@ func (bridge *Bridge) GetAllUsers() []*User {
 }
 }
 
 
 func (bridge *Bridge) NewUser(dbUser *database.User) *User {
 func (bridge *Bridge) NewUser(dbUser *database.User) *User {
-	return &User{
+	user := &User{
 		User:          dbUser,
 		User:          dbUser,
 		bridge:        bridge,
 		bridge:        bridge,
 		log:           bridge.Log.Sub("User").Sub(string(dbUser.ID)),
 		log:           bridge.Log.Sub("User").Sub(string(dbUser.ID)),
@@ -87,6 +94,9 @@ func (bridge *Bridge) NewUser(dbUser *database.User) *User {
 		portalsByJID:  make(map[types.WhatsAppID]*Portal),
 		portalsByJID:  make(map[types.WhatsAppID]*Portal),
 		puppets:       make(map[types.WhatsAppID]*Puppet),
 		puppets:       make(map[types.WhatsAppID]*Puppet),
 	}
 	}
+	user.htmlParser = user.newHTMLParser()
+	user.waReplString, user.waReplFunc = user.newWhatsAppFormatMaps()
+	return user
 }
 }
 
 
 func (user *User) SetManagementRoom(roomID types.MatrixRoomID) {
 func (user *User) SetManagementRoom(roomID types.MatrixRoomID) {
@@ -183,6 +193,10 @@ func (user *User) Login(roomID types.MatrixRoomID) {
 	go user.Sync()
 	go user.Sync()
 }
 }
 
 
+func (user *User) JID() string {
+	return strings.Replace(user.Conn.Info.Wid, whatsapp_ext.OldUserSuffix, whatsapp_ext.NewUserSuffix, 1)
+}
+
 func (user *User) Sync() {
 func (user *User) Sync() {
 	user.log.Debugln("Syncing...")
 	user.log.Debugln("Syncing...")
 	user.Conn.Contacts()
 	user.Conn.Contacts()
@@ -241,7 +255,7 @@ func (user *User) HandlePresence(info whatsapp_ext.Presence) {
 	case whatsapp_ext.PresenceUnavailable:
 	case whatsapp_ext.PresenceUnavailable:
 		puppet.Intent().SetPresence("offline")
 		puppet.Intent().SetPresence("offline")
 	case whatsapp_ext.PresenceAvailable:
 	case whatsapp_ext.PresenceAvailable:
-		if len(puppet.typingIn) > 0 && puppet.typingAt + 15 > time.Now().Unix() {
+		if len(puppet.typingIn) > 0 && puppet.typingAt+15 > time.Now().Unix() {
 			puppet.Intent().UserTyping(puppet.typingIn, false, 0)
 			puppet.Intent().UserTyping(puppet.typingIn, false, 0)
 			puppet.typingIn = ""
 			puppet.typingIn = ""
 			puppet.typingAt = 0
 			puppet.typingAt = 0
@@ -252,7 +266,7 @@ func (user *User) HandlePresence(info whatsapp_ext.Presence) {
 		portal := user.GetPortalByJID(info.JID)
 		portal := user.GetPortalByJID(info.JID)
 		puppet.typingIn = portal.MXID
 		puppet.typingIn = portal.MXID
 		puppet.typingAt = time.Now().Unix()
 		puppet.typingAt = time.Now().Unix()
-		puppet.Intent().UserTyping(portal.MXID, true, 15 * 1000)
+		puppet.Intent().UserTyping(portal.MXID, true, 15*1000)
 	}
 	}
 }
 }