mirror of
https://github.com/status-im/status-go.git
synced 2025-01-09 22:26:30 +00:00
4b9517c0e2
When we received a remove event from a private group chat out of order, the chat would not be created. This was causing some issues if later on we received the previous event. This commit changes the behavior so that a chat is created.
679 lines
18 KiB
Go
679 lines
18 KiB
Go
package protocol
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ecdsa"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/golang/protobuf/proto"
|
|
"github.com/google/uuid"
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/status-im/status-go/eth-node/crypto"
|
|
"github.com/status-im/status-go/eth-node/types"
|
|
"github.com/status-im/status-go/protocol/protobuf"
|
|
)
|
|
|
|
// 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
|
|
Events []MembershipUpdateEvent `json:"events"`
|
|
Message *protobuf.ChatMessage `json:"-"`
|
|
EmojiReaction *protobuf.EmojiReaction `json:"-"`
|
|
}
|
|
|
|
const signatureLength = 65
|
|
|
|
func MembershipUpdateEventFromProtobuf(chatID string, raw []byte) (*MembershipUpdateEvent, error) {
|
|
if len(raw) <= signatureLength {
|
|
return nil, errors.New("invalid payload length")
|
|
}
|
|
decodedEvent := protobuf.MembershipUpdateEvent{}
|
|
signature := raw[:signatureLength]
|
|
encodedEvent := raw[signatureLength:]
|
|
|
|
signatureMaterial := append([]byte(chatID), encodedEvent...)
|
|
publicKey, err := crypto.ExtractSignature(signatureMaterial, signature)
|
|
if err != nil {
|
|
return nil, errors.Wrap(err, "failed to extract signature")
|
|
}
|
|
|
|
from := publicKeyToString(publicKey)
|
|
|
|
err = proto.Unmarshal(encodedEvent, &decodedEvent)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &MembershipUpdateEvent{
|
|
ClockValue: decodedEvent.Clock,
|
|
ChatID: chatID,
|
|
Members: decodedEvent.Members,
|
|
Name: decodedEvent.Name,
|
|
Type: decodedEvent.Type,
|
|
Color: decodedEvent.Color,
|
|
Image: decodedEvent.Image,
|
|
Signature: signature,
|
|
RawPayload: encodedEvent,
|
|
From: from,
|
|
}, nil
|
|
}
|
|
|
|
func (m *MembershipUpdateMessage) ToProtobuf() (*protobuf.MembershipUpdateMessage, error) {
|
|
var rawEvents [][]byte
|
|
for _, e := range m.Events {
|
|
var encodedEvent []byte
|
|
encodedEvent = append(encodedEvent, e.Signature...)
|
|
encodedEvent = append(encodedEvent, e.RawPayload...)
|
|
rawEvents = append(rawEvents, encodedEvent)
|
|
}
|
|
|
|
mUM := &protobuf.MembershipUpdateMessage{
|
|
ChatId: m.ChatID,
|
|
Events: rawEvents,
|
|
}
|
|
|
|
// If message is not piggybacking anything, that's a valid case and we just return
|
|
switch {
|
|
case m.Message != nil:
|
|
mUM.ChatEntity = &protobuf.MembershipUpdateMessage_Message{Message: m.Message}
|
|
case m.EmojiReaction != nil:
|
|
mUM.ChatEntity = &protobuf.MembershipUpdateMessage_EmojiReaction{EmojiReaction: m.EmojiReaction}
|
|
}
|
|
|
|
return mUM, nil
|
|
}
|
|
|
|
func MembershipUpdateMessageFromProtobuf(raw *protobuf.MembershipUpdateMessage) (*MembershipUpdateMessage, error) {
|
|
var events []MembershipUpdateEvent
|
|
for _, e := range raw.Events {
|
|
verifiedEvent, err := MembershipUpdateEventFromProtobuf(raw.ChatId, e)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
events = append(events, *verifiedEvent)
|
|
}
|
|
return &MembershipUpdateMessage{
|
|
ChatID: raw.ChatId,
|
|
Events: events,
|
|
Message: raw.GetMessage(),
|
|
EmojiReaction: raw.GetEmojiReaction(),
|
|
}, nil
|
|
}
|
|
|
|
// EncodeMembershipUpdateMessage encodes a MembershipUpdateMessage using protobuf serialization.
|
|
func EncodeMembershipUpdateMessage(value MembershipUpdateMessage) ([]byte, error) {
|
|
pb, err := value.ToProtobuf()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return proto.Marshal(pb)
|
|
}
|
|
|
|
// MembershipUpdateEvent contains an event information.
|
|
// Member and Members are hex-encoded values with 0x prefix.
|
|
type MembershipUpdateEvent struct {
|
|
Type protobuf.MembershipUpdateEvent_EventType `json:"type"`
|
|
ClockValue uint64 `json:"clockValue"`
|
|
Members []string `json:"members,omitempty"` // in "members-added" and "admins-added" events
|
|
Name string `json:"name,omitempty"` // name of the group chat
|
|
Color string `json:"color,omitempty"` // color of the group chat
|
|
Image []byte `json:"image,omitempty"` // image of the group chat
|
|
From string `json:"from,omitempty"`
|
|
Signature []byte `json:"signature,omitempty"`
|
|
ChatID string `json:"chatId"`
|
|
RawPayload []byte `json:"rawPayload"`
|
|
}
|
|
|
|
func (u *MembershipUpdateEvent) Equal(update MembershipUpdateEvent) bool {
|
|
return bytes.Equal(u.Signature, update.Signature)
|
|
}
|
|
|
|
func (u *MembershipUpdateEvent) Sign(key *ecdsa.PrivateKey) error {
|
|
if len(u.ChatID) == 0 {
|
|
return errors.New("can't sign with empty chatID")
|
|
}
|
|
encodedEvent, err := proto.Marshal(u.ToProtobuf())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
u.RawPayload = encodedEvent
|
|
var signatureMaterial []byte
|
|
signatureMaterial = append(signatureMaterial, []byte(u.ChatID)...)
|
|
signatureMaterial = crypto.Keccak256(append(signatureMaterial, u.RawPayload...))
|
|
signature, err := crypto.Sign(signatureMaterial, key)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
u.Signature = signature
|
|
u.From = publicKeyToString(&key.PublicKey)
|
|
return nil
|
|
}
|
|
|
|
func (u *MembershipUpdateEvent) ToProtobuf() *protobuf.MembershipUpdateEvent {
|
|
return &protobuf.MembershipUpdateEvent{
|
|
Clock: u.ClockValue,
|
|
Name: u.Name,
|
|
Color: u.Color,
|
|
Image: u.Image,
|
|
Members: u.Members,
|
|
Type: u.Type,
|
|
}
|
|
}
|
|
|
|
func MergeMembershipUpdateEvents(dest []MembershipUpdateEvent, src []MembershipUpdateEvent) []MembershipUpdateEvent {
|
|
for _, update := range src {
|
|
var exists bool
|
|
for _, existing := range dest {
|
|
if existing.Equal(update) {
|
|
exists = true
|
|
break
|
|
}
|
|
}
|
|
if !exists {
|
|
dest = append(dest, update)
|
|
}
|
|
}
|
|
return dest
|
|
}
|
|
|
|
func NewChatCreatedEvent(name string, color string, clock uint64) MembershipUpdateEvent {
|
|
return MembershipUpdateEvent{
|
|
Type: protobuf.MembershipUpdateEvent_CHAT_CREATED,
|
|
Name: name,
|
|
ClockValue: clock,
|
|
Color: color,
|
|
}
|
|
}
|
|
|
|
func NewNameChangedEvent(name string, clock uint64) MembershipUpdateEvent {
|
|
return MembershipUpdateEvent{
|
|
Type: protobuf.MembershipUpdateEvent_NAME_CHANGED,
|
|
Name: name,
|
|
ClockValue: clock,
|
|
}
|
|
}
|
|
|
|
func NewColorChangedEvent(color string, clock uint64) MembershipUpdateEvent {
|
|
return MembershipUpdateEvent{
|
|
Type: protobuf.MembershipUpdateEvent_COLOR_CHANGED,
|
|
Color: color,
|
|
ClockValue: clock,
|
|
}
|
|
}
|
|
|
|
func NewImageChangedEvent(image []byte, clock uint64) MembershipUpdateEvent {
|
|
return MembershipUpdateEvent{
|
|
Type: protobuf.MembershipUpdateEvent_IMAGE_CHANGED,
|
|
Image: image,
|
|
ClockValue: clock,
|
|
}
|
|
}
|
|
|
|
func NewMembersAddedEvent(members []string, clock uint64) MembershipUpdateEvent {
|
|
return MembershipUpdateEvent{
|
|
Type: protobuf.MembershipUpdateEvent_MEMBERS_ADDED,
|
|
Members: members,
|
|
ClockValue: clock,
|
|
}
|
|
}
|
|
|
|
func NewMemberJoinedEvent(clock uint64) MembershipUpdateEvent {
|
|
return MembershipUpdateEvent{
|
|
Type: protobuf.MembershipUpdateEvent_MEMBER_JOINED,
|
|
ClockValue: clock,
|
|
}
|
|
}
|
|
|
|
func NewAdminsAddedEvent(admins []string, clock uint64) MembershipUpdateEvent {
|
|
return MembershipUpdateEvent{
|
|
Type: protobuf.MembershipUpdateEvent_ADMINS_ADDED,
|
|
Members: admins,
|
|
ClockValue: clock,
|
|
}
|
|
}
|
|
|
|
func NewMemberRemovedEvent(member string, clock uint64) MembershipUpdateEvent {
|
|
return MembershipUpdateEvent{
|
|
Type: protobuf.MembershipUpdateEvent_MEMBER_REMOVED,
|
|
Members: []string{member},
|
|
ClockValue: clock,
|
|
}
|
|
}
|
|
|
|
func NewAdminRemovedEvent(admin string, clock uint64) MembershipUpdateEvent {
|
|
return MembershipUpdateEvent{
|
|
Type: protobuf.MembershipUpdateEvent_ADMIN_REMOVED,
|
|
Members: []string{admin},
|
|
ClockValue: clock,
|
|
}
|
|
}
|
|
|
|
type Group struct {
|
|
chatID string
|
|
name string
|
|
color string
|
|
image []byte
|
|
events []MembershipUpdateEvent
|
|
admins *stringSet
|
|
members *stringSet
|
|
}
|
|
|
|
func groupChatID(creator *ecdsa.PublicKey) string {
|
|
return uuid.New().String() + "-" + publicKeyToString(creator)
|
|
}
|
|
|
|
func NewGroupWithEvents(chatID string, events []MembershipUpdateEvent) (*Group, error) {
|
|
return newGroup(chatID, events)
|
|
}
|
|
|
|
func NewGroupWithCreator(name string, color string, clock uint64, creator *ecdsa.PrivateKey) (*Group, error) {
|
|
chatID := groupChatID(&creator.PublicKey)
|
|
chatCreated := NewChatCreatedEvent(name, color, clock)
|
|
chatCreated.ChatID = chatID
|
|
err := chatCreated.Sign(creator)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return newGroup(chatID, []MembershipUpdateEvent{chatCreated})
|
|
}
|
|
|
|
func newGroup(chatID string, events []MembershipUpdateEvent) (*Group, error) {
|
|
g := Group{
|
|
chatID: chatID,
|
|
events: events,
|
|
admins: newStringSet(),
|
|
members: newStringSet(),
|
|
}
|
|
if err := g.init(); err != nil {
|
|
return nil, err
|
|
}
|
|
return &g, nil
|
|
}
|
|
|
|
func (g *Group) init() error {
|
|
g.sortEvents()
|
|
|
|
var chatID string
|
|
|
|
for _, event := range g.events {
|
|
if chatID == "" {
|
|
chatID = event.ChatID
|
|
} else if event.ChatID != chatID {
|
|
return errors.New("updates contain different chat IDs")
|
|
}
|
|
valid := g.validateEvent(event)
|
|
if !valid {
|
|
return fmt.Errorf("invalid event %#+v from %s", event, event.From)
|
|
}
|
|
g.processEvent(event)
|
|
}
|
|
|
|
valid := g.validateChatID(g.chatID)
|
|
if !valid {
|
|
return fmt.Errorf("invalid chat ID: %s", g.chatID)
|
|
}
|
|
if chatID != g.chatID {
|
|
return fmt.Errorf("expected chat ID equal %s, got %s", g.chatID, chatID)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (g Group) ChatID() string {
|
|
return g.chatID
|
|
}
|
|
|
|
func (g Group) Name() string {
|
|
return g.name
|
|
}
|
|
|
|
func (g Group) Color() string {
|
|
return g.color
|
|
}
|
|
|
|
func (g Group) Image() []byte {
|
|
return g.image
|
|
}
|
|
|
|
func (g Group) Events() []MembershipUpdateEvent {
|
|
return g.events
|
|
}
|
|
|
|
// AbridgedEvents returns the minimum set of events for a user to publish a post
|
|
// The events we want to keep:
|
|
// 1) Chat created
|
|
// 2) Latest color changed
|
|
// 3) Latest image changed
|
|
// 4) For each admin, the latest admins added event that contains them
|
|
// 5) For each member, the latest members added event that contains them
|
|
// 4 & 5, might bring removed admins or removed members, for those, we also need to
|
|
// keep the event that removes them
|
|
func (g Group) AbridgedEvents() []MembershipUpdateEvent {
|
|
var events []MembershipUpdateEvent
|
|
var nameChangedEventFound bool
|
|
var colorChangedEventFound bool
|
|
var imageChangedEventFound bool
|
|
removedMembers := make(map[string]*MembershipUpdateEvent)
|
|
addedMembers := make(map[string]bool)
|
|
extraMembers := make(map[string]bool)
|
|
admins := make(map[string]bool)
|
|
// Iterate in reverse
|
|
for i := len(g.events) - 1; i >= 0; i-- {
|
|
event := g.events[i]
|
|
switch event.Type {
|
|
case protobuf.MembershipUpdateEvent_CHAT_CREATED:
|
|
events = append(events, event)
|
|
case protobuf.MembershipUpdateEvent_NAME_CHANGED:
|
|
if nameChangedEventFound {
|
|
continue
|
|
}
|
|
events = append(events, event)
|
|
nameChangedEventFound = true
|
|
case protobuf.MembershipUpdateEvent_COLOR_CHANGED:
|
|
if colorChangedEventFound {
|
|
continue
|
|
}
|
|
events = append(events, event)
|
|
colorChangedEventFound = true
|
|
case protobuf.MembershipUpdateEvent_IMAGE_CHANGED:
|
|
if imageChangedEventFound {
|
|
continue
|
|
}
|
|
events = append(events, event)
|
|
imageChangedEventFound = true
|
|
|
|
case protobuf.MembershipUpdateEvent_MEMBERS_ADDED:
|
|
var shouldAddEvent bool
|
|
for _, m := range event.Members {
|
|
// If it's adding a current user, and we don't have a more
|
|
// recent event
|
|
// if it's an admin, we track it
|
|
if admins[m] || (g.members.Has(m) && !addedMembers[m]) {
|
|
addedMembers[m] = true
|
|
shouldAddEvent = true
|
|
}
|
|
}
|
|
if shouldAddEvent {
|
|
// Append the event and check the not current members that are also
|
|
// added
|
|
for _, m := range event.Members {
|
|
if !g.members.Has(m) && !admins[m] {
|
|
extraMembers[m] = true
|
|
}
|
|
}
|
|
events = append(events, event)
|
|
}
|
|
case protobuf.MembershipUpdateEvent_ADMIN_REMOVED:
|
|
// We add it always for now
|
|
events = append(events, event)
|
|
case protobuf.MembershipUpdateEvent_ADMINS_ADDED:
|
|
// We track admins in full
|
|
admins[event.Members[0]] = true
|
|
events = append(events, event)
|
|
case protobuf.MembershipUpdateEvent_MEMBER_REMOVED:
|
|
// Save member removed events, as we might need it
|
|
// to remove members who have been added but subsequently left
|
|
if removedMembers[event.Members[0]] == nil || removedMembers[event.Members[0]].ClockValue < event.ClockValue {
|
|
removedMembers[event.Members[0]] = &event
|
|
}
|
|
|
|
case protobuf.MembershipUpdateEvent_MEMBER_JOINED:
|
|
if g.members.Has(event.From) {
|
|
events = append(events, event)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
for m := range extraMembers {
|
|
if removedMembers[m] != nil {
|
|
events = append(events, *removedMembers[m])
|
|
}
|
|
}
|
|
|
|
sort.Slice(events, func(i, j int) bool {
|
|
return events[i].ClockValue < events[j].ClockValue
|
|
})
|
|
|
|
return events
|
|
}
|
|
|
|
func (g Group) Members() []string {
|
|
return g.members.List()
|
|
}
|
|
|
|
func (g Group) Admins() []string {
|
|
return g.admins.List()
|
|
}
|
|
|
|
func (g *Group) ProcessEvents(events []MembershipUpdateEvent) error {
|
|
for _, event := range events {
|
|
err := g.ProcessEvent(event)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (g *Group) ProcessEvent(event MembershipUpdateEvent) error {
|
|
if !g.validateEvent(event) {
|
|
return fmt.Errorf("invalid event %#+v", event)
|
|
}
|
|
// Check if exists
|
|
g.events = append(g.events, event)
|
|
g.processEvent(event)
|
|
return nil
|
|
}
|
|
|
|
func (g Group) LastClockValue() uint64 {
|
|
if len(g.events) == 0 {
|
|
return 0
|
|
}
|
|
return g.events[len(g.events)-1].ClockValue
|
|
}
|
|
|
|
func (g Group) Creator() (string, error) {
|
|
if len(g.events) == 0 {
|
|
return "", errors.New("no events in the group")
|
|
}
|
|
first := g.events[0]
|
|
if first.Type != protobuf.MembershipUpdateEvent_CHAT_CREATED {
|
|
return "", fmt.Errorf("expected first event to be 'chat-created', got %s", first.Type)
|
|
}
|
|
return first.From, nil
|
|
}
|
|
|
|
func (g Group) isCreator(id string) (bool, error) {
|
|
c, err := g.Creator()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return id == c, nil
|
|
}
|
|
|
|
func (g Group) validateChatID(chatID string) bool {
|
|
creator, err := g.Creator()
|
|
if err != nil || creator == "" {
|
|
return false
|
|
}
|
|
// TODO: It does not verify that the prefix is a valid UUID.
|
|
// Improve it so that the prefix follows UUIDv4 spec.
|
|
return strings.HasSuffix(chatID, creator) && chatID != creator
|
|
}
|
|
|
|
func (g Group) IsMember(id string) bool {
|
|
return g.members.Has(id)
|
|
}
|
|
|
|
func (g Group) WasEverMember(id string) (bool, error) {
|
|
isCreator, err := g.isCreator(id)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if isCreator {
|
|
return true, nil
|
|
}
|
|
|
|
for _, event := range g.events {
|
|
if event.Type == protobuf.MembershipUpdateEvent_MEMBERS_ADDED {
|
|
for _, member := range event.Members {
|
|
if member == id {
|
|
return true, nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
// validateEvent returns true if a given event is valid.
|
|
func (g Group) validateEvent(event MembershipUpdateEvent) bool {
|
|
if len(event.From) == 0 {
|
|
return false
|
|
}
|
|
switch event.Type {
|
|
case protobuf.MembershipUpdateEvent_CHAT_CREATED:
|
|
return g.admins.Empty() && g.members.Empty()
|
|
case protobuf.MembershipUpdateEvent_NAME_CHANGED:
|
|
return (g.admins.Has(event.From) || g.members.Has(event.From)) && len(event.Name) > 0
|
|
case protobuf.MembershipUpdateEvent_COLOR_CHANGED:
|
|
return (g.admins.Has(event.From) || g.members.Has(event.From)) && len(event.Color) > 0
|
|
case protobuf.MembershipUpdateEvent_IMAGE_CHANGED:
|
|
return (g.admins.Has(event.From) || g.members.Has(event.From)) && len(event.Image) > 0
|
|
case protobuf.MembershipUpdateEvent_MEMBERS_ADDED:
|
|
return g.admins.Has(event.From) || g.members.Has(event.From)
|
|
case protobuf.MembershipUpdateEvent_MEMBER_JOINED:
|
|
return g.members.Has(event.From)
|
|
case protobuf.MembershipUpdateEvent_MEMBER_REMOVED:
|
|
// Member can remove themselves or admin can remove a member.
|
|
return len(event.Members) == 1 && (event.From == event.Members[0] || (g.admins.Has(event.From) && !g.admins.Has(event.Members[0])))
|
|
case protobuf.MembershipUpdateEvent_ADMINS_ADDED:
|
|
return g.admins.Has(event.From) && stringSliceSubset(event.Members, g.members.List())
|
|
case protobuf.MembershipUpdateEvent_ADMIN_REMOVED:
|
|
return len(event.Members) == 1 && g.admins.Has(event.From) && event.From == event.Members[0]
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (g *Group) processEvent(event MembershipUpdateEvent) {
|
|
switch event.Type {
|
|
case protobuf.MembershipUpdateEvent_CHAT_CREATED:
|
|
g.name = event.Name
|
|
g.color = event.Color
|
|
g.members.Add(event.From)
|
|
g.admins.Add(event.From)
|
|
case protobuf.MembershipUpdateEvent_NAME_CHANGED:
|
|
g.name = event.Name
|
|
case protobuf.MembershipUpdateEvent_COLOR_CHANGED:
|
|
g.color = event.Color
|
|
case protobuf.MembershipUpdateEvent_IMAGE_CHANGED:
|
|
g.image = event.Image
|
|
case protobuf.MembershipUpdateEvent_ADMINS_ADDED:
|
|
g.admins.Add(event.Members...)
|
|
case protobuf.MembershipUpdateEvent_ADMIN_REMOVED:
|
|
g.admins.Remove(event.Members[0])
|
|
case protobuf.MembershipUpdateEvent_MEMBERS_ADDED:
|
|
g.members.Add(event.Members...)
|
|
case protobuf.MembershipUpdateEvent_MEMBER_REMOVED:
|
|
g.admins.Remove(event.Members[0])
|
|
g.members.Remove(event.Members[0])
|
|
}
|
|
}
|
|
|
|
func (g *Group) sortEvents() {
|
|
sort.Slice(g.events, func(i, j int) bool {
|
|
return g.events[i].ClockValue < g.events[j].ClockValue
|
|
})
|
|
}
|
|
|
|
func stringSliceSubset(subset []string, set []string) bool {
|
|
for _, item1 := range set {
|
|
var found bool
|
|
for _, item2 := range subset {
|
|
if item1 == item2 {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if found {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func publicKeyToString(publicKey *ecdsa.PublicKey) string {
|
|
return types.EncodeHex(crypto.FromECDSAPub(publicKey))
|
|
}
|
|
|
|
type stringSet struct {
|
|
m map[string]struct{}
|
|
items []string
|
|
}
|
|
|
|
func newStringSet() *stringSet {
|
|
return &stringSet{
|
|
m: make(map[string]struct{}),
|
|
}
|
|
}
|
|
|
|
func newStringSetFromSlice(s []string) *stringSet {
|
|
set := newStringSet()
|
|
if len(s) > 0 {
|
|
set.Add(s...)
|
|
}
|
|
return set
|
|
}
|
|
|
|
func (s *stringSet) Add(items ...string) {
|
|
for _, item := range items {
|
|
if _, ok := s.m[item]; !ok {
|
|
s.m[item] = struct{}{}
|
|
s.items = append(s.items, item)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *stringSet) Remove(items ...string) {
|
|
for _, item := range items {
|
|
if _, ok := s.m[item]; ok {
|
|
delete(s.m, item)
|
|
s.removeFromItems(item)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *stringSet) Has(item string) bool {
|
|
_, ok := s.m[item]
|
|
return ok
|
|
}
|
|
|
|
func (s *stringSet) Empty() bool {
|
|
return len(s.items) == 0
|
|
}
|
|
|
|
func (s *stringSet) List() []string {
|
|
return s.items
|
|
}
|
|
|
|
func (s *stringSet) removeFromItems(dropped string) {
|
|
n := 0
|
|
for _, item := range s.items {
|
|
if item != dropped {
|
|
s.items[n] = item
|
|
n++
|
|
}
|
|
}
|
|
s.items = s.items[:n]
|
|
}
|