Decode MembershipUpdate message (#76)
This change also adds CreatePrivateGroupTextMessage function to create a group message. Relates to #73
This commit is contained in:
parent
dbf4c4062e
commit
4492eb9779
155
v1/decoder.go
155
v1/decoder.go
|
@ -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"
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue