Re-break everything and fix Matrix->WhatsApp replies

This commit is contained in:
Tulir Asokan 2018-09-01 23:38:03 +03:00
parent e4a78832ad
commit ed27fa775e
13 changed files with 135 additions and 86 deletions

4
Gopkg.lock generated
View file

@ -101,7 +101,7 @@
branch = "master" branch = "master"
name = "golang.org/x/sys" name = "golang.org/x/sys"
packages = ["unix"] packages = ["unix"]
revision = "49385e6e15226593f68b26af201feec29d5bba22" revision = "fa5fdf94c78965f1aa8423f0cc50b8b8d728b05a"
[[projects]] [[projects]]
name = "gopkg.in/russross/blackfriday.v2" name = "gopkg.in/russross/blackfriday.v2"
@ -140,7 +140,7 @@
branch = "master" branch = "master"
name = "maunium.net/go/mautrix-appservice" name = "maunium.net/go/mautrix-appservice"
packages = ["."] packages = ["."]
revision = "fb756247f82716de7698b8200f28f16b4fd04a6b" revision = "4e24d1dd7bd9d89f946ec56cb4350ce777d17bfe"
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"

View file

@ -60,13 +60,24 @@ type UsernameTemplateArgs struct {
UserID string UserID string
} }
func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) string { func (bc BridgeConfig) FormatDisplayname(contact whatsapp.Contact) (string, int8) {
var buf bytes.Buffer var buf bytes.Buffer
if index := strings.IndexRune(contact.Jid, '@'); index > 0 { if index := strings.IndexRune(contact.Jid, '@'); index > 0 {
contact.Jid = "+" + contact.Jid[:index] contact.Jid = "+" + contact.Jid[:index]
} }
bc.displaynameTemplate.Execute(&buf, contact) bc.displaynameTemplate.Execute(&buf, contact)
return buf.String() var quality int8
switch {
case len(contact.Notify) > 0:
quality = 3
case len(contact.Name) > 0 || len(contact.Short) > 0:
quality = 2
case len(contact.Jid) > 0:
quality = 1
default:
quality = 0
}
return buf.String(), quality
} }
func (bc BridgeConfig) FormatUsername(userID types.WhatsAppID) string { func (bc BridgeConfig) FormatUsername(userID types.WhatsAppID) string {
@ -76,7 +87,7 @@ func (bc BridgeConfig) FormatUsername(userID types.WhatsAppID) string {
} }
func (bc BridgeConfig) MarshalYAML() (interface{}, error) { func (bc BridgeConfig) MarshalYAML() (interface{}, error) {
bc.DisplaynameTemplate = bc.FormatDisplayname(whatsapp.Contact{ bc.DisplaynameTemplate, _ = bc.FormatDisplayname(whatsapp.Contact{
Jid: "{{.Jid}}", Jid: "{{.Jid}}",
Notify: "{{.Notify}}", Notify: "{{.Notify}}",
Name: "{{.Name}}", Name: "{{.Name}}",

View file

@ -24,7 +24,7 @@ import (
) )
func (config *Config) NewRegistration() (*appservice.Registration, error) { func (config *Config) NewRegistration() (*appservice.Registration, error) {
registration := appservice.CreateRegistration("mautrix-whatsapp") registration := appservice.CreateRegistration()
err := config.copyToRegistration(registration) err := config.copyToRegistration(registration)
if err != nil { if err != nil {
@ -37,7 +37,7 @@ func (config *Config) NewRegistration() (*appservice.Registration, error) {
} }
func (config *Config) GetRegistration() (*appservice.Registration, error) { func (config *Config) GetRegistration() (*appservice.Registration, error) {
registration := appservice.CreateRegistration("mautrix-whatsapp") registration := appservice.CreateRegistration()
err := config.copyToRegistration(registration) err := config.copyToRegistration(registration)
if err != nil { if err != nil {

View file

@ -17,8 +17,11 @@
package database package database
import ( import (
"bytes"
"database/sql" "database/sql"
"encoding/json"
waProto "github.com/Rhymen/go-whatsapp/binary/proto"
log "maunium.net/go/maulogger" log "maunium.net/go/maulogger"
"maunium.net/go/mautrix-whatsapp/types" "maunium.net/go/mautrix-whatsapp/types"
) )
@ -32,8 +35,10 @@ func (mq *MessageQuery) CreateTable() error {
_, err := mq.db.Exec(`CREATE TABLE IF NOT EXISTS message ( _, err := mq.db.Exec(`CREATE TABLE IF NOT EXISTS message (
chat_jid VARCHAR(25), chat_jid VARCHAR(25),
chat_receiver VARCHAR(25), chat_receiver VARCHAR(25),
jid VARCHAR(255), jid VARCHAR(255),
mxid VARCHAR(255) NOT NULL UNIQUE, mxid VARCHAR(255) NOT NULL UNIQUE,
sender VARCHAR(25) NOT NULL,
content BLOB NOT NULL,
PRIMARY KEY (chat_jid, chat_receiver, jid), PRIMARY KEY (chat_jid, chat_receiver, jid),
FOREIGN KEY (chat_jid, chat_receiver) REFERENCES portal(jid, receiver) FOREIGN KEY (chat_jid, chat_receiver) REFERENCES portal(jid, receiver)
@ -80,34 +85,54 @@ type Message struct {
db *Database db *Database
log log.Logger log log.Logger
Chat PortalKey Chat PortalKey
JID types.WhatsAppMessageID JID types.WhatsAppMessageID
MXID types.MatrixEventID MXID types.MatrixEventID
Sender types.WhatsAppID
Content *waProto.Message
} }
func (msg *Message) Scan(row Scannable) *Message { func (msg *Message) Scan(row Scannable) *Message {
err := row.Scan(&msg.Chat.JID, &msg.Chat.Receiver, &msg.JID, &msg.MXID) var content []byte
err := row.Scan(&msg.Chat.JID, &msg.Chat.Receiver, &msg.JID, &msg.MXID, &msg.Sender, &content)
if err != nil { if err != nil {
if err != sql.ErrNoRows { if err != sql.ErrNoRows {
msg.log.Errorln("Database scan failed:", err) msg.log.Errorln("Database scan failed:", err)
} }
return nil return nil
} }
msg.parseBinaryContent(content)
return msg return msg
} }
func (msg *Message) Insert() error { func (msg *Message) parseBinaryContent(content []byte) {
_, err := msg.db.Exec("INSERT INTO message VALUES (?, ?, ?, ?)", msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID) msg.Content = &waProto.Message{}
reader := bytes.NewReader(content)
// dec := gob.NewDecoder(reader)
dec := json.NewDecoder(reader)
err := dec.Decode(msg.Content)
if err != nil { if err != nil {
msg.log.Warnfln("Failed to insert %s: %v", msg.Chat, msg.JID, err) msg.log.Warnln("Failed to decode message content:", err)
} }
return err
} }
func (msg *Message) Update() error { func (msg *Message) binaryContent() []byte {
_, err := msg.db.Exec("UPDATE portal SET mxid=? WHERE chat_jid=? AND chat_receiver=? AND jid=?", msg.MXID, msg.Chat.JID, msg.Chat.Receiver, msg.JID) var buf bytes.Buffer
//enc := gob.NewEncoder(&buf)
enc := json.NewEncoder(&buf)
err := enc.Encode(msg.Content)
if err != nil { if err != nil {
msg.log.Warnfln("Failed to update %s: %v", msg.Chat, msg.JID, err) msg.log.Warnln("Failed to encode message content:", err)
}
return buf.Bytes()
}
func (msg *Message) Insert() error {
_, err := msg.db.Exec("INSERT INTO message VALUES (?, ?, ?, ?, ?, ?)", msg.Chat.JID, msg.Chat.Receiver, msg.JID, msg.MXID, msg.Sender, msg.binaryContent())
if err != nil {
msg.log.Warnfln("Failed to insert %s@%s: %v", msg.Chat, msg.JID, err)
} }
return err return err
} }

View file

@ -30,9 +30,10 @@ type PuppetQuery struct {
func (pq *PuppetQuery) CreateTable() error { func (pq *PuppetQuery) CreateTable() error {
_, err := pq.db.Exec(`CREATE TABLE IF NOT EXISTS puppet ( _, err := pq.db.Exec(`CREATE TABLE IF NOT EXISTS puppet (
jid VARCHAR(25) PRIMARY KEY, jid VARCHAR(25) PRIMARY KEY,
displayname VARCHAR(255), avatar VARCHAR(255),
avatar VARCHAR(255) displayname VARCHAR(255),
name_quality TINYINT
)`) )`)
return err return err
} }
@ -69,8 +70,9 @@ type Puppet struct {
log log.Logger log log.Logger
JID types.WhatsAppID JID types.WhatsAppID
Displayname string
Avatar string Avatar string
Displayname string
NameQuality int8
} }
func (puppet *Puppet) Scan(row Scannable) *Puppet { func (puppet *Puppet) Scan(row Scannable) *Puppet {
@ -88,19 +90,19 @@ func (puppet *Puppet) Scan(row Scannable) *Puppet {
} }
func (puppet *Puppet) Insert() error { func (puppet *Puppet) Insert() error {
_, err := puppet.db.Exec("INSERT INTO puppet VALUES (?, ?, ?)", _, err := puppet.db.Exec("INSERT INTO puppet VALUES (?, ?, ?, ?)",
puppet.JID, puppet.Displayname, puppet.Avatar) puppet.JID, puppet.Avatar, puppet.Displayname, puppet.NameQuality)
if err != nil { if err != nil {
puppet.log.Errorfln("Failed to insert %s: %v", puppet.JID, err) puppet.log.Warnfln("Failed to insert %s: %v", puppet.JID, err)
} }
return err return err
} }
func (puppet *Puppet) Update() error { func (puppet *Puppet) Update() error {
_, err := puppet.db.Exec("UPDATE puppet SET displayname=?, avatar=? WHERE jid=?", _, err := puppet.db.Exec("UPDATE puppet SET displayname=?, name_quality=?, avatar=? WHERE jid=?",
puppet.Displayname, puppet.Avatar, puppet.JID) puppet.Displayname, puppet.NameQuality, puppet.Avatar, puppet.JID)
if err != nil { if err != nil {
puppet.log.Errorfln("Failed to update %s->%s: %v", puppet.JID, err) puppet.log.Warnfln("Failed to update %s->%s: %v", puppet.JID, err)
} }
return err return err
} }

View file

@ -131,6 +131,14 @@ func stripSuffix(jid types.WhatsAppID) string {
return jid[:index] return jid[:index]
} }
func (user *User) jidPtr() *string {
if len(user.JID) > 0 {
str := stripSuffix(user.JID)
return &str
}
return nil
}
func (user *User) sessionUnptr() (sess whatsapp.Session) { func (user *User) sessionUnptr() (sess whatsapp.Session) {
if user.Session != nil { if user.Session != nil {
sess = *user.Session sess = *user.Session
@ -140,17 +148,23 @@ func (user *User) sessionUnptr() (sess whatsapp.Session) {
func (user *User) Insert() error { func (user *User) Insert() error {
sess := user.sessionUnptr() sess := user.sessionUnptr()
_, err := user.db.Exec("INSERT INTO user VALUES (?, ?, ?, ?, ?, ?, ?, ?)", user.MXID, stripSuffix(user.JID), _, err := user.db.Exec("INSERT INTO user VALUES (?, ?, ?, ?, ?, ?, ?, ?)", user.MXID, user.jidPtr(),
user.ManagementRoom, user.ManagementRoom,
sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey) sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey)
if err != nil {
user.log.Warnfln("Failed to insert %s: %v", user.MXID, err)
}
return err return err
} }
func (user *User) Update() error { func (user *User) Update() error {
sess := user.sessionUnptr() sess := user.sessionUnptr()
_, err := user.db.Exec("UPDATE user SET jid=?, management_room=?, client_id=?, client_token=?, server_token=?, enc_key=?, mac_key=? WHERE mxid=?", _, err := user.db.Exec("UPDATE user SET jid=?, management_room=?, client_id=?, client_token=?, server_token=?, enc_key=?, mac_key=? WHERE mxid=?",
stripSuffix(user.JID), user.ManagementRoom, user.jidPtr(), user.ManagementRoom,
sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey, sess.ClientId, sess.ClientToken, sess.ServerToken, sess.EncKey, sess.MacKey,
user.MXID) user.MXID)
if err != nil {
user.log.Warnfln("Failed to update %s: %v", user.MXID, err)
}
return err return err
} }

View file

@ -139,6 +139,10 @@ func (mx *MatrixHandler) HandleRoomMetadata(evt *gomatrix.Event) {
} }
func (mx *MatrixHandler) HandleMessage(evt *gomatrix.Event) { func (mx *MatrixHandler) HandleMessage(evt *gomatrix.Event) {
if _, isPuppet := mx.bridge.ParsePuppetMXID(evt.Sender); evt.Sender == mx.bridge.Bot.UserID || isPuppet {
return
}
roomID := types.MatrixRoomID(evt.RoomID) roomID := types.MatrixRoomID(evt.RoomID)
user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender)) user := mx.bridge.GetUserByMXID(types.MatrixUserID(evt.Sender))

View file

@ -18,6 +18,7 @@ package main
import ( import (
"bytes" "bytes"
"encoding/gob"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"image" "image"
@ -121,6 +122,8 @@ type Portal struct {
recentlyHandled [20]types.WhatsAppMessageID recentlyHandled [20]types.WhatsAppMessageID
recentlyHandledLock sync.Mutex recentlyHandledLock sync.Mutex
recentlyHandledIndex uint8 recentlyHandledIndex uint8
isPrivate *bool
} }
func (portal *Portal) getMessageLock(messageID types.WhatsAppMessageID) sync.Mutex { func (portal *Portal) getMessageLock(messageID types.WhatsAppMessageID) sync.Mutex {
@ -157,18 +160,33 @@ func (portal *Portal) isDuplicate(id types.WhatsAppMessageID) bool {
return false return false
} }
func (portal *Portal) markHandled(jid types.WhatsAppMessageID, mxid types.MatrixEventID) { func init() {
gob.Register(&waProto.Message{})
}
func (portal *Portal) markHandled(source *User, message *waProto.WebMessageInfo, mxid types.MatrixEventID) {
msg := portal.bridge.DB.Message.New() msg := portal.bridge.DB.Message.New()
msg.Chat = portal.Key msg.Chat = portal.Key
msg.JID = jid msg.JID = message.GetKey().GetId()
msg.MXID = mxid msg.MXID = mxid
if message.GetKey().GetFromMe() {
msg.Sender = source.JID
} else if portal.IsPrivateChat() {
msg.Sender = portal.Key.JID
} else {
msg.Sender = message.GetKey().GetParticipant()
if len(msg.Sender) == 0 {
msg.Sender = message.GetParticipant()
}
}
msg.Content = message.Message
msg.Insert() msg.Insert()
portal.recentlyHandledLock.Lock() portal.recentlyHandledLock.Lock()
index := portal.recentlyHandledIndex index := portal.recentlyHandledIndex
portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % 20 portal.recentlyHandledIndex = (portal.recentlyHandledIndex + 1) % 20
portal.recentlyHandledLock.Unlock() portal.recentlyHandledLock.Unlock()
portal.recentlyHandled[index] = jid portal.recentlyHandled[index] = msg.JID
} }
func (portal *Portal) startHandling(id types.WhatsAppMessageID) (*sync.Mutex, bool) { func (portal *Portal) startHandling(id types.WhatsAppMessageID) (*sync.Mutex, bool) {
@ -184,8 +202,9 @@ func (portal *Portal) startHandling(id types.WhatsAppMessageID) (*sync.Mutex, bo
return &lock, true return &lock, true
} }
func (portal *Portal) finishHandling(id types.WhatsAppMessageID, mxid types.MatrixEventID) { func (portal *Portal) finishHandling(source *User, message *waProto.WebMessageInfo, mxid types.MatrixEventID) {
portal.markHandled(id, mxid) portal.markHandled(source, message, mxid)
id := message.GetKey().GetId()
portal.deleteMessageLock(id) portal.deleteMessageLock(id)
portal.log.Debugln("Handled message", id, "->", mxid) portal.log.Debugln("Handled message", id, "->", mxid)
} }
@ -450,7 +469,11 @@ func (portal *Portal) CreateMatrixRoom(invite []string) error {
} }
func (portal *Portal) IsPrivateChat() bool { func (portal *Portal) IsPrivateChat() bool {
return strings.HasSuffix(portal.Key.JID, whatsappExt.NewUserSuffix) if portal.isPrivate == nil {
val := strings.HasSuffix(portal.Key.JID, whatsappExt.NewUserSuffix)
portal.isPrivate = &val
}
return *portal.isPrivate
} }
func (portal *Portal) MainIntent() *appservice.IntentAPI { func (portal *Portal) MainIntent() *appservice.IntentAPI {
@ -527,7 +550,7 @@ func (portal *Portal) HandleTextMessage(source *User, message whatsapp.TextMessa
portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err) portal.log.Errorfln("Failed to handle message %s: %v", message.Info.Id, err)
return return
} }
portal.finishHandling(message.Info.Id, resp.EventID) portal.finishHandling(source, message.Info.Source, resp.EventID)
} }
func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte, error), thumbnail []byte, info whatsapp.MessageInfo, mimeType, caption string) { func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte, error), thumbnail []byte, info whatsapp.MessageInfo, mimeType, caption string) {
@ -628,7 +651,7 @@ func (portal *Portal) HandleMediaMessage(source *User, download func() ([]byte,
// TODO store caption mxid? // TODO store caption mxid?
} }
portal.finishHandling(info.Id, resp.EventID) portal.finishHandling(source, info.Source, resp.EventID)
} }
func makeMessageID() *string { func makeMessageID() *string {
@ -716,31 +739,6 @@ type MediaUpload struct {
Thumbnail []byte Thumbnail []byte
} }
func (portal *Portal) GetMessage(user *User, jid types.WhatsAppMessageID) *waProto.WebMessageInfo {
node, err := user.Conn.LoadMessagesBefore(portal.Key.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 = user.Conn.LoadMessagesAfter(portal.Key.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(sender *User, evt *gomatrix.Event) { func (portal *Portal) HandleMatrixMessage(sender *User, evt *gomatrix.Event) {
if portal.IsPrivateChat() && sender.JID != portal.Key.Receiver { if portal.IsPrivateChat() && sender.JID != portal.Key.Receiver {
return return
@ -764,17 +762,10 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *gomatrix.Event) {
if len(replyToID) > 0 { if len(replyToID) > 0 {
evt.Content.RemoveReplyFallback() evt.Content.RemoveReplyFallback()
msg := portal.bridge.DB.Message.GetByMXID(replyToID) msg := portal.bridge.DB.Message.GetByMXID(replyToID)
if msg != nil { if msg != nil && msg.Content != nil {
origMsg := portal.GetMessage(sender, msg.JID) ctxInfo.StanzaId = &msg.JID
if origMsg != nil { ctxInfo.Participant = &msg.Sender
ctxInfo.StanzaId = &msg.JID ctxInfo.QuotedMessage = []*waProto.Message{msg.Content}
replyMsgSender := origMsg.GetParticipant()
if origMsg.GetKey().GetFromMe() {
replyMsgSender = sender.JID
}
ctxInfo.Participant = &replyMsgSender
ctxInfo.QuotedMessage = []*waProto.Message{origMsg.Message}
}
} }
} }
var err error var err error
@ -863,7 +854,7 @@ func (portal *Portal) HandleMatrixMessage(sender *User, evt *gomatrix.Event) {
portal.log.Debugln("Unhandled Matrix event:", evt) portal.log.Debugln("Unhandled Matrix event:", evt)
return return
} }
portal.markHandled(info.GetKey().GetId(), evt.ID) portal.markHandled(sender, info, evt.ID)
err = sender.Conn.Send(info) err = sender.Conn.Send(info)
if err != nil { if err != nil {
portal.log.Errorfln("Error handling Matrix event %s: %v", evt.ID, err) portal.log.Errorfln("Error handling Matrix event %s: %v", evt.ID, err)

View file

@ -43,7 +43,7 @@ func (bridge *Bridge) ParsePuppetMXID(mxid types.MatrixUserID) (types.WhatsAppID
return "", false return "", false
} }
jid := types.WhatsAppID(match[2] + whatsappExt.NewUserSuffix) jid := types.WhatsAppID(match[1] + whatsappExt.NewUserSuffix)
return jid, true return jid, true
} }
@ -168,11 +168,12 @@ func (puppet *Puppet) Sync(source *User, contact whatsapp.Contact) {
if contact.Jid == source.JID { if contact.Jid == source.JID {
contact.Notify = source.Conn.Info.Pushname contact.Notify = source.Conn.Info.Pushname
} }
newName := puppet.bridge.Config.Bridge.FormatDisplayname(contact) newName, quality := puppet.bridge.Config.Bridge.FormatDisplayname(contact)
if puppet.Displayname != newName { if puppet.Displayname != newName && quality >= puppet.NameQuality {
err := puppet.Intent().SetDisplayName(newName) err := puppet.Intent().SetDisplayName(newName)
if err == nil { if err == nil {
puppet.Displayname = newName puppet.Displayname = newName
puppet.NameQuality = quality
puppet.Update() puppet.Update()
} else { } else {
puppet.log.Warnln("Failed to set display name:", err) puppet.log.Warnln("Failed to set display name:", err)

View file

@ -44,14 +44,16 @@ func GenerateRegistration(asName, botName string, reserveRooms, reserveUsers boo
boldCyan.Println("Generating appservice config and registration.") boldCyan.Println("Generating appservice config and registration.")
reader := bufio.NewReader(os.Stdin) reader := bufio.NewReader(os.Stdin)
registration := CreateRegistration()
config := Create()
registration.RateLimited = false
name, err := readString(reader, "Enter name for appservice", asName) name, err := readString(reader, "Enter name for appservice", asName)
if err != nil { if err != nil {
fmt.Println("Failed to read user Input:", err) fmt.Println("Failed to read user Input:", err)
return return
} }
registration := CreateRegistration(name) registration.ID = name
config := Create()
registration.RateLimited = false
registration.SenderLocalpart, err = readString(reader, "Enter bot username", botName) registration.SenderLocalpart, err = readString(reader, "Enter bot username", botName)
if err != nil { if err != nil {

View file

@ -1,11 +1,11 @@
package appservice package appservice
import ( import (
"context"
"encoding/json" "encoding/json"
"github.com/gorilla/mux"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"github.com/gorilla/mux"
"context"
"time" "time"
) )
@ -106,7 +106,6 @@ func (as *AppService) PutTransaction(w http.ResponseWriter, r *http.Request) {
} }
for _, event := range eventList.Events { for _, event := range eventList.Events {
as.Log.Debugln("Received event", event.ID)
as.UpdateState(event) as.UpdateState(event)
as.Events <- event as.Events <- event
} }

View file

@ -2,8 +2,8 @@ package appservice
import ( import (
"encoding/json" "encoding/json"
"net/http"
"maunium.net/go/gomatrix" "maunium.net/go/gomatrix"
"net/http"
) )
// EventList contains a list of events. // EventList contains a list of events.

View file

@ -21,7 +21,7 @@ type Registration struct {
} }
// CreateRegistration creates a Registration with random appservice and homeserver tokens. // CreateRegistration creates a Registration with random appservice and homeserver tokens.
func CreateRegistration(name string) *Registration { func CreateRegistration() *Registration {
return &Registration{ return &Registration{
AppToken: RandomString(64), AppToken: RandomString(64),
ServerToken: RandomString(64), ServerToken: RandomString(64),