Decode MembershipUpdate message (#76)

This change also adds CreatePrivateGroupTextMessage function to create a group message. Relates to #73
This commit is contained in:
Adam Babik 2019-09-23 10:52:59 +02:00 committed by GitHub
parent dbf4c4062e
commit 4492eb9779
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 320 additions and 24 deletions

View File

@ -1,6 +1,7 @@
package statusproto
import (
"container/list"
"errors"
"fmt"
"io"
@ -15,12 +16,14 @@ func NewMessageDecoder(r io.Reader) *transit.Decoder {
decoder := transit.NewDecoder(r)
decoder.AddHandler(messageTag, statusMessageHandler)
decoder.AddHandler(pairMessageTag, pairMessageHandler)
decoder.AddHandler(membershipUpdateTag, membershipUpdateMessageHandler)
return decoder
}
const (
messageTag = "c4"
pairMessageTag = "p2"
membershipUpdateTag = "g5"
)
func statusMessageHandler(d transit.Decoder, value interface{}) (interface{}, error) {
@ -123,3 +126,151 @@ func pairMessageHandler(d transit.Decoder, value interface{}) (interface{}, erro
}
return pm, nil
}
func membershipUpdateMessageHandler(d transit.Decoder, value interface{}) (interface{}, error) {
taggedValue, ok := value.(transit.TaggedValue)
if !ok {
return nil, errors.New("not a tagged value")
}
values, ok := taggedValue.Value.([]interface{})
if !ok {
return nil, errors.New("tagged value does not contain values")
}
m := MembershipUpdateMessage{}
for idx, v := range values {
var ok bool
switch idx {
case 0:
m.ChatID, ok = v.(string)
case 1:
var updates *list.List
updates, ok = v.(*list.List)
if !ok {
break
}
for e := updates.Front(); e != nil; e = e.Next() {
var value map[interface{}]interface{}
value, ok = e.Value.(map[interface{}]interface{})
if !ok {
break
}
update := MembershipUpdate{}
update.ChatID, ok = value[transit.Keyword("chat-id")].(string)
if !ok {
break
}
update.From, ok = value[transit.Keyword("from")].(string)
if !ok {
break
}
update.Signature, ok = value[transit.Keyword("signature")].(string)
if !ok {
break
}
// parse events
var events []interface{}
events, ok = value[transit.Keyword("events")].([]interface{})
if !ok {
break
}
for _, item := range events {
var event map[interface{}]interface{}
event, ok = item.(map[interface{}]interface{})
if !ok {
break
}
var updateEvent MembershipUpdateEvent
updateEvent, ok = parseEvent(event)
if !ok {
break
}
update.Events = append(update.Events, updateEvent)
}
m.Updates = append(m.Updates, update)
}
case 2:
if v == nil {
continue
}
messageI, err := statusMessageHandler(d, v)
if err != nil {
return nil, fmt.Errorf("failed to handle message in membership update: %v", err)
}
var message Message
message, ok = messageI.(Message)
if ok {
m.Message = &message
}
default:
// skip any other values
ok = true
}
if !ok {
return nil, fmt.Errorf("invalid value for index: %d", idx)
}
}
return m, nil
}
func setToString(set *transit.Set) ([]string, bool) {
result := make([]string, 0, len(set.Contents))
for _, item := range set.Contents {
val, ok := item.(string)
if !ok {
return nil, false
}
result = append(result, val)
}
return result, true
}
func parseEvent(event map[interface{}]interface{}) (result MembershipUpdateEvent, ok bool) {
// Type is required
result.Type, ok = event[transit.Keyword("type")].(string)
if !ok {
return
}
// ClockValue is required
result.ClockValue, ok = event[transit.Keyword("clock-value")].(int64)
if !ok {
return
}
// Name is optional
if val, exists := event[transit.Keyword("name")]; exists {
result.Name, ok = val.(string)
if !ok {
return
}
}
// Member is optional
if val, exists := event[transit.Keyword("member")]; exists {
result.Member, ok = val.(string)
if !ok {
return
}
}
// Members is optional
if val, exists := event[transit.Keyword("members")]; exists {
var members *transit.Set
members, ok = val.(*transit.Set)
if !ok {
return
}
result.Members, ok = setToString(members)
if !ok {
return
}
}
return
}

View File

@ -1,6 +1,7 @@
package statusproto
import (
"container/list"
"errors"
"io"
"reflect"
@ -8,6 +9,14 @@ import (
"github.com/russolsen/transit"
)
var (
messageType = reflect.TypeOf(Message{})
pairMessageType = reflect.TypeOf(PairMessage{})
membershipUpdateType = reflect.TypeOf(MembershipUpdateMessage{})
defaultMessageValueEncoder = &messageValueEncoder{}
)
// NewMessageEncoder returns a new Transit encoder
// that can encode Message values.
// More about Transit: https://github.com/cognitect/transit-format
@ -15,15 +24,10 @@ func NewMessageEncoder(w io.Writer) *transit.Encoder {
encoder := transit.NewEncoder(w, false)
encoder.AddHandler(messageType, defaultMessageValueEncoder)
encoder.AddHandler(pairMessageType, defaultMessageValueEncoder)
encoder.AddHandler(membershipUpdateType, defaultMessageValueEncoder)
return encoder
}
var (
messageType = reflect.TypeOf(Message{})
pairMessageType = reflect.TypeOf(PairMessage{})
defaultMessageValueEncoder = &messageValueEncoder{}
)
type messageValueEncoder struct{}
func (messageValueEncoder) IsStringable(reflect.Value) bool {
@ -33,20 +37,7 @@ func (messageValueEncoder) IsStringable(reflect.Value) bool {
func (messageValueEncoder) Encode(e transit.Encoder, value reflect.Value, asString bool) error {
switch message := value.Interface().(type) {
case Message:
taggedValue := transit.TaggedValue{
Tag: messageTag,
Value: []interface{}{
message.Text,
message.ContentT,
transit.Keyword(message.MessageT),
message.Clock,
message.Timestamp,
map[interface{}]interface{}{
transit.Keyword("chat-id"): message.Content.ChatID,
transit.Keyword("text"): message.Content.Text,
},
},
}
taggedValue := encodeMessageToTaggedValue(message)
return e.EncodeInterface(taggedValue, false)
case PairMessage:
taggedValue := transit.TaggedValue{
@ -59,7 +50,69 @@ func (messageValueEncoder) Encode(e transit.Encoder, value reflect.Value, asStri
},
}
return e.EncodeInterface(taggedValue, false)
case MembershipUpdateMessage:
updatesList := list.New()
for _, update := range message.Updates {
var events []interface{}
for _, event := range update.Events {
eventMap := map[interface{}]interface{}{
transit.Keyword("type"): event.Type,
transit.Keyword("clock-value"): event.ClockValue,
}
if event.Name != "" {
eventMap[transit.Keyword("name")] = event.Name
}
if event.Member != "" {
eventMap[transit.Keyword("member")] = event.Member
}
if len(event.Members) > 0 {
members := make([]interface{}, len(event.Members))
for idx, m := range event.Members {
members[idx] = m
}
eventMap[transit.Keyword("members")] = transit.NewSet(members)
}
events = append(events, eventMap)
}
element := map[interface{}]interface{}{
transit.Keyword("chat-id"): update.ChatID,
transit.Keyword("from"): update.From,
transit.Keyword("events"): events,
transit.Keyword("signature"): update.Signature,
}
updatesList.PushBack(element)
}
value := []interface{}{
message.ChatID,
updatesList,
}
if message.Message != nil {
value = append(value, encodeMessageToTaggedValue(*message.Message))
}
taggedValue := transit.TaggedValue{
Tag: membershipUpdateTag,
Value: value,
}
return e.EncodeInterface(taggedValue, false)
}
return errors.New("unknown message type to encode")
}
func encodeMessageToTaggedValue(m Message) transit.TaggedValue {
return transit.TaggedValue{
Tag: messageTag,
Value: []interface{}{
m.Text,
m.ContentT,
transit.Keyword(m.MessageT),
m.Clock,
m.Timestamp,
map[interface{}]interface{}{
transit.Keyword("chat-id"): m.Content.ChatID,
transit.Keyword("text"): m.Content.Text,
},
},
}
}

View File

@ -0,0 +1,37 @@
package statusproto
import "bytes"
// MembershipUpdateMessage is a message used to propagate information
// about group membership changes.
// For more information, see https://github.com/status-im/specs/blob/master/status-group-chats-spec.md.
type MembershipUpdateMessage struct {
ChatID string `json:"chatId"` // UUID concatenated with hex-encoded public key of the creator for the chat
Updates []MembershipUpdate `json:"updates"`
Message *Message `json:"message"` // optional message
}
type MembershipUpdate struct {
ChatID string `json:"chatId"`
From string `json:"from"`
Signature string `json:"signature"`
Events []MembershipUpdateEvent `json:"events"`
}
type MembershipUpdateEvent struct {
Type string `json:"type"`
ClockValue int64 `json:"clockValue"`
Member string `json:"member,omitempty"` // in "member-joined", "member-removed" and "admin-removed" events
Members []string `json:"members,omitempty"` // in "members-added" and "admins-added" events
Name string `json:"name,omitempty"` // name of the group chat
}
// EncodeMembershipUpdateMessage encodes a MembershipUpdateMessage using Transit serialization.
func EncodeMembershipUpdateMessage(value MembershipUpdateMessage) ([]byte, error) {
var buf bytes.Buffer
encoder := NewMessageEncoder(&buf)
if err := encoder.Encode(value); err != nil {
return nil, err
}
return buf.Bytes(), nil
}

View File

@ -0,0 +1,50 @@
package statusproto
import (
"testing"
"github.com/stretchr/testify/require"
)
var (
testMembershipUpdateMessageBytes = []byte(`["~#g5",["072ea460-84d3-53c5-9979-1ca36fb5d1020x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1",["~#list",[["^ ","~:chat-id","072ea460-84d3-53c5-9979-1ca36fb5d1020x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1","~:from","0x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1","~:events",[["^ ","~:type","chat-created","~:name","thathata","~:clock-value",156897373998501],["^ ","^5","members-added","^7",156897373998502,"~:members",["~#set",["0x04aebe2bb01a988abe7d978662f21de7760486119876c680e5a559e38e086a2df6dad41c4e4d9079c03db3bced6cb70fca76afc5650e50ea19b81572046a813534"]]]],"~:signature","7fca3d614cf55bc6cdf9c17fd1e65d1688673322bf1f004c58c78e0927edefea3d1053bf6a9d2e058ae88079f588105dccf2a2f9f330f6035cd47c715ee5950601"]]],null]]`)
testMembershipUpdateMessageStruct = MembershipUpdateMessage{
ChatID: "072ea460-84d3-53c5-9979-1ca36fb5d1020x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1",
Updates: []MembershipUpdate{
{
ChatID: "072ea460-84d3-53c5-9979-1ca36fb5d1020x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1",
Signature: "7fca3d614cf55bc6cdf9c17fd1e65d1688673322bf1f004c58c78e0927edefea3d1053bf6a9d2e058ae88079f588105dccf2a2f9f330f6035cd47c715ee5950601",
From: "0x0424a68f89ba5fcd5e0640c1e1f591d561fa4125ca4e2a43592bc4123eca10ce064e522c254bb83079ba404327f6eafc01ec90a1444331fe769d3f3a7f90b0dde1",
Events: []MembershipUpdateEvent{
{
Type: "chat-created",
Name: "thathata",
ClockValue: 156897373998501,
},
{
Type: "members-added",
Members: []string{"0x04aebe2bb01a988abe7d978662f21de7760486119876c680e5a559e38e086a2df6dad41c4e4d9079c03db3bced6cb70fca76afc5650e50ea19b81572046a813534"},
ClockValue: 156897373998502,
},
},
},
},
Message: nil,
}
)
func TestDecodeMembershipUpdateMessage(t *testing.T) {
val, err := decodeTransitMessage(testMembershipUpdateMessageBytes)
require.NoError(t, err)
require.EqualValues(t, testMembershipUpdateMessageStruct, val)
}
func TestEncodeMembershipUpdateMessage(t *testing.T) {
data, err := EncodeMembershipUpdateMessage(testMembershipUpdateMessageStruct)
require.NoError(t, err)
// Decode it back to a struct and compare. Comparing bytes is not an option because,
// for example, map encoding is non-deterministic.
val, err := decodeTransitMessage(data)
require.NoError(t, err)
require.EqualValues(t, testMembershipUpdateMessageStruct, val)
}

View File

@ -116,11 +116,16 @@ func CreatePublicTextMessage(data []byte, lastClock int64, chatID string) Messag
return m
}
// CreatePrivateTextMessage creates a public text Message.
// CreatePrivateTextMessage creates a one-to-one message.
func CreatePrivateTextMessage(data []byte, lastClock int64, chatID string) Message {
return createTextMessage(data, lastClock, chatID, MessageTypePrivate)
}
// CreatePrivateGroupTextMessage creates a group message.
func CreatePrivateGroupTextMessage(data []byte, lastClock int64, chatID string) Message {
return createTextMessage(data, lastClock, chatID, MessageTypePrivateGroup)
}
func decodeTransitMessage(originalPayload []byte) (interface{}, error) {
payload := make([]byte, len(originalPayload))
copy(payload, originalPayload)

View File

@ -16,7 +16,7 @@ var (
}
)
func TestDecodePairMessageMessage(t *testing.T) {
func TestDecodePairMessage(t *testing.T) {
val, err := decodeTransitMessage(testPairMessageBytes)
require.NoError(t, err)
require.EqualValues(t, testPairMessageStruct, val)