status-im/status-go

View on GitHub
protocol/communities/community_categories.go

Summary

Maintainability
A
0 mins
Test Coverage
B
85%
package communities

import (
    "sort"

    "github.com/status-im/status-go/protocol/protobuf"
)

func (o *Community) ChatsByCategoryID(categoryID string) []string {
    o.mutex.Lock()
    defer o.mutex.Unlock()
    var chatIDs []string
    if o.config == nil || o.config.CommunityDescription == nil {
        return chatIDs
    }

    for chatID, chat := range o.config.CommunityDescription.Chats {
        if chat.CategoryId == categoryID {
            chatIDs = append(chatIDs, chatID)
        }
    }
    return chatIDs
}

func (o *Community) CommunityChatsIDs() []string {
    o.mutex.Lock()
    defer o.mutex.Unlock()
    var chatIDs []string
    if o.config == nil || o.config.CommunityDescription == nil {
        return chatIDs
    }

    for chatID := range o.config.CommunityDescription.Chats {
        chatIDs = append(chatIDs, chatID)
    }
    return chatIDs
}

func (o *Community) CreateCategory(categoryID string, categoryName string, chatIDs []string) (*CommunityChanges, error) {
    o.mutex.Lock()
    defer o.mutex.Unlock()

    if !(o.IsControlNode() || o.hasPermissionToSendCommunityEvent(protobuf.CommunityEvent_COMMUNITY_CATEGORY_CREATE)) {
        return nil, ErrNotAuthorized
    }

    changes, err := o.createCategory(categoryID, categoryName, chatIDs)
    if err != nil {
        return nil, err
    }

    changes.CategoriesAdded[categoryID] = o.config.CommunityDescription.Categories[categoryID]
    for i, cid := range chatIDs {
        changes.ChatsModified[cid] = &CommunityChatChanges{
            MembersAdded:     make(map[string]*protobuf.CommunityMember),
            MembersRemoved:   make(map[string]*protobuf.CommunityMember),
            CategoryModified: categoryID,
            PositionModified: i,
        }
    }

    if o.IsControlNode() {
        o.increaseClock()
    } else {
        err := o.addNewCommunityEvent(o.ToCreateCategoryCommunityEvent(categoryID, categoryName, chatIDs))
        if err != nil {
            return nil, err
        }
    }

    return changes, nil
}

func (o *Community) EditCategory(categoryID string, categoryName string, chatIDs []string) (*CommunityChanges, error) {
    o.mutex.Lock()
    defer o.mutex.Unlock()

    if !(o.IsControlNode() || o.hasPermissionToSendCommunityEvent(protobuf.CommunityEvent_COMMUNITY_CATEGORY_EDIT)) {
        return nil, ErrNotAuthorized
    }

    changes, err := o.editCategory(categoryID, categoryName, chatIDs)
    if err != nil {
        return nil, err
    }

    changes.CategoriesModified[categoryID] = o.config.CommunityDescription.Categories[categoryID]
    for i, cid := range chatIDs {
        changes.ChatsModified[cid] = &CommunityChatChanges{
            MembersAdded:     make(map[string]*protobuf.CommunityMember),
            MembersRemoved:   make(map[string]*protobuf.CommunityMember),
            CategoryModified: categoryID,
            PositionModified: i,
        }
    }

    if o.IsControlNode() {
        o.increaseClock()
    } else {
        err := o.addNewCommunityEvent(o.ToEditCategoryCommunityEvent(categoryID, categoryName, chatIDs))
        if err != nil {
            return nil, err
        }
    }

    return changes, nil
}

func (o *Community) ReorderCategories(categoryID string, newPosition int) (*CommunityChanges, error) {
    o.mutex.Lock()
    defer o.mutex.Unlock()

    if !(o.IsControlNode() || o.hasPermissionToSendCommunityEvent(protobuf.CommunityEvent_COMMUNITY_CATEGORY_REORDER)) {
        return nil, ErrNotAuthorized
    }

    changes, err := o.reorderCategories(categoryID, newPosition)
    if err != nil {
        return nil, err
    }

    if o.IsControlNode() {
        o.increaseClock()
    } else {
        err := o.addNewCommunityEvent(o.ToReorderCategoryCommunityEvent(categoryID, newPosition))
        if err != nil {
            return nil, err
        }
    }

    return changes, nil
}

func (o *Community) setModifiedCategories(changes *CommunityChanges, s sortSlice) {
    sort.Sort(s)
    for i, catSortHelper := range s {
        if o.config.CommunityDescription.Categories[catSortHelper.catID].Position != int32(i) {
            o.config.CommunityDescription.Categories[catSortHelper.catID].Position = int32(i)
            changes.CategoriesModified[catSortHelper.catID] = o.config.CommunityDescription.Categories[catSortHelper.catID]
        }
    }
}

func (o *Community) ReorderChat(categoryID string, chatID string, newPosition int) (*CommunityChanges, error) {
    o.mutex.Lock()
    defer o.mutex.Unlock()

    if !(o.IsControlNode() || o.hasPermissionToSendCommunityEvent(protobuf.CommunityEvent_COMMUNITY_CHANNEL_REORDER)) {
        return nil, ErrNotAuthorized
    }

    changes, err := o.reorderChat(categoryID, chatID, newPosition)
    if err != nil {
        return nil, err
    }

    if o.IsControlNode() {
        o.increaseClock()
    } else {
        err := o.addNewCommunityEvent(o.ToReorderChannelCommunityEvent(categoryID, chatID, newPosition))
        if err != nil {
            return nil, err
        }
    }

    return changes, nil
}

func (o *Community) SortCategoryChats(changes *CommunityChanges, categoryID string) {
    var catChats []string
    for k, c := range o.config.CommunityDescription.Chats {
        if c.CategoryId == categoryID {
            catChats = append(catChats, k)
        }
    }

    sortedChats := make(sortSlice, 0, len(catChats))
    for _, k := range catChats {
        sortedChats = append(sortedChats, sorterHelperIdx{
            pos:    o.config.CommunityDescription.Chats[k].Position,
            chatID: k,
        })
    }

    sort.Sort(sortedChats)

    for i, chatSortHelper := range sortedChats {
        if o.config.CommunityDescription.Chats[chatSortHelper.chatID].Position != int32(i) {
            o.config.CommunityDescription.Chats[chatSortHelper.chatID].Position = int32(i)
            if changes.ChatsModified[chatSortHelper.chatID] != nil {
                changes.ChatsModified[chatSortHelper.chatID].PositionModified = i
            } else {
                changes.ChatsModified[chatSortHelper.chatID] = &CommunityChatChanges{
                    PositionModified: i,
                    MembersAdded:     make(map[string]*protobuf.CommunityMember),
                    MembersRemoved:   make(map[string]*protobuf.CommunityMember),
                }
            }
        }
    }
}

func (o *Community) insertAndSort(changes *CommunityChanges, oldCategoryID string, categoryID string, chatID string, chat *protobuf.CommunityChat, newPosition int) {
    // We sort the chats here because maps are not guaranteed to keep order
    var catChats []string
    sortedChats := make(sortSlice, 0, len(o.config.CommunityDescription.Chats))
    for k, v := range o.config.CommunityDescription.Chats {
        sortedChats = append(sortedChats, sorterHelperIdx{
            pos:    v.Position,
            chatID: k,
        })
    }
    sort.Sort(sortedChats)
    for _, k := range sortedChats {
        if o.config.CommunityDescription.Chats[k.chatID].CategoryId == categoryID {
            catChats = append(catChats, k.chatID)
        }
    }

    if newPosition > 0 && newPosition >= len(catChats) {
        newPosition = len(catChats) - 1
    } else if newPosition < 0 {
        newPosition = 0
    }

    decrease := false
    if chat.Position > int32(newPosition) {
        decrease = true
    }

    for k, v := range o.config.CommunityDescription.Chats {
        if k != chatID && newPosition == int(v.Position) && v.CategoryId == categoryID {
            if oldCategoryID == categoryID {
                if decrease {
                    v.Position++
                } else {
                    v.Position--
                }
            } else {
                v.Position++
            }
        }
    }

    idx := -1
    currChatID := ""
    var sortedChatIDs []string
    for i, k := range catChats {
        if o.config.CommunityDescription.Chats[k] != chat && ((decrease && o.config.CommunityDescription.Chats[k].Position < int32(newPosition)) || (!decrease && o.config.CommunityDescription.Chats[k].Position <= int32(newPosition))) {
            sortedChatIDs = append(sortedChatIDs, k)
        } else {
            if o.config.CommunityDescription.Chats[k] == chat {
                idx = i
                currChatID = k
            }
        }
    }

    sortedChatIDs = append(sortedChatIDs, currChatID)

    for i, k := range catChats {
        if i == idx || (decrease && o.config.CommunityDescription.Chats[k].Position < int32(newPosition)) || (!decrease && o.config.CommunityDescription.Chats[k].Position <= int32(newPosition)) {
            continue
        }
        sortedChatIDs = append(sortedChatIDs, k)
    }

    for i, sortedChatID := range sortedChatIDs {
        if o.config.CommunityDescription.Chats[sortedChatID].Position != int32(i) {
            o.config.CommunityDescription.Chats[sortedChatID].Position = int32(i)
            if changes.ChatsModified[sortedChatID] != nil {
                changes.ChatsModified[sortedChatID].PositionModified = i
            } else {
                changes.ChatsModified[sortedChatID] = &CommunityChatChanges{
                    MembersAdded:     make(map[string]*protobuf.CommunityMember),
                    MembersRemoved:   make(map[string]*protobuf.CommunityMember),
                    PositionModified: i,
                }
            }
        }
    }
}

func (o *Community) getCategoryChatCount(categoryID string) int {
    result := 0
    for _, chat := range o.config.CommunityDescription.Chats {
        if chat.CategoryId == categoryID {
            result = result + 1
        }
    }
    return result
}

func (o *Community) DeleteCategory(categoryID string) (*CommunityChanges, error) {
    o.mutex.Lock()
    defer o.mutex.Unlock()

    if !(o.IsControlNode() || o.hasPermissionToSendCommunityEvent(protobuf.CommunityEvent_COMMUNITY_CATEGORY_DELETE)) {
        return nil, ErrNotAuthorized
    }

    changes, err := o.deleteCategory(categoryID)
    if err != nil {
        return nil, err
    }

    if o.IsControlNode() {
        o.increaseClock()
    } else {
        err := o.addNewCommunityEvent(o.ToDeleteCategoryCommunityEvent(categoryID))
        if err != nil {
            return nil, err
        }
    }

    return changes, nil
}

func (o *Community) createCategory(categoryID string, categoryName string, chatIDs []string) (*CommunityChanges, error) {
    if o.config.CommunityDescription.Categories == nil {
        o.config.CommunityDescription.Categories = make(map[string]*protobuf.CommunityCategory)
    }
    if _, ok := o.config.CommunityDescription.Categories[categoryID]; ok {
        return nil, ErrCategoryAlreadyExists
    }

    for _, cid := range chatIDs {
        c, exists := o.config.CommunityDescription.Chats[cid]
        if !exists {
            return nil, ErrChatNotFound
        }

        if exists && c.CategoryId != categoryID && c.CategoryId != "" {
            return nil, ErrChatAlreadyAssigned
        }
    }

    changes := o.emptyCommunityChanges()

    o.config.CommunityDescription.Categories[categoryID] = &protobuf.CommunityCategory{
        CategoryId: categoryID,
        Name:       categoryName,
        Position:   int32(len(o.config.CommunityDescription.Categories)),
    }

    for i, cid := range chatIDs {
        o.config.CommunityDescription.Chats[cid].CategoryId = categoryID
        o.config.CommunityDescription.Chats[cid].Position = int32(i)
    }

    o.SortCategoryChats(changes, "")

    return changes, nil
}

func (o *Community) editCategory(categoryID string, categoryName string, chatIDs []string) (*CommunityChanges, error) {
    if o.config.CommunityDescription.Categories == nil {
        o.config.CommunityDescription.Categories = make(map[string]*protobuf.CommunityCategory)
    }
    if _, ok := o.config.CommunityDescription.Categories[categoryID]; !ok {
        return nil, ErrCategoryNotFound
    }

    for _, cid := range chatIDs {
        c, exists := o.config.CommunityDescription.Chats[cid]
        if !exists {
            return nil, ErrChatNotFound
        }

        if exists && c.CategoryId != categoryID && c.CategoryId != "" {
            return nil, ErrChatAlreadyAssigned
        }
    }

    changes := o.emptyCommunityChanges()

    emptyCatLen := o.getCategoryChatCount("")

    // remove any chat that might have been assigned before and now it's not part of the category
    var chatsToRemove []string
    for k, chat := range o.config.CommunityDescription.Chats {
        if chat.CategoryId == categoryID {
            found := false
            for _, c := range chatIDs {
                if k == c {
                    found = true
                }
            }
            if !found {
                chat.CategoryId = ""
                chatsToRemove = append(chatsToRemove, k)
            }
        }
    }

    o.config.CommunityDescription.Categories[categoryID].Name = categoryName

    for i, cid := range chatIDs {
        o.config.CommunityDescription.Chats[cid].CategoryId = categoryID
        o.config.CommunityDescription.Chats[cid].Position = int32(i)
    }

    for i, cid := range chatsToRemove {
        o.config.CommunityDescription.Chats[cid].Position = int32(emptyCatLen + i)
        changes.ChatsModified[cid] = &CommunityChatChanges{
            MembersAdded:     make(map[string]*protobuf.CommunityMember),
            MembersRemoved:   make(map[string]*protobuf.CommunityMember),
            CategoryModified: "",
            PositionModified: int(o.config.CommunityDescription.Chats[cid].Position),
        }
    }

    o.SortCategoryChats(changes, "")

    return changes, nil
}

func (o *Community) deleteCategory(categoryID string) (*CommunityChanges, error) {
    if _, exists := o.config.CommunityDescription.Categories[categoryID]; !exists {
        return nil, ErrCategoryNotFound
    }

    changes := o.emptyCommunityChanges()

    emptyCategoryChatCount := o.getCategoryChatCount("")
    i := 0
    for _, chat := range o.config.CommunityDescription.Chats {
        if chat.CategoryId == categoryID {
            i++
            chat.CategoryId = ""
            chat.Position = int32(emptyCategoryChatCount + i)
        }
    }

    o.SortCategoryChats(changes, "")

    delete(o.config.CommunityDescription.Categories, categoryID)

    changes.CategoriesRemoved = append(changes.CategoriesRemoved, categoryID)

    // Reorder
    s := make(sortSlice, 0, len(o.config.CommunityDescription.Categories))
    for _, cat := range o.config.CommunityDescription.Categories {
        s = append(s, sorterHelperIdx{
            pos:   cat.Position,
            catID: cat.CategoryId,
        })
    }

    o.setModifiedCategories(changes, s)

    return changes, nil
}

func (o *Community) reorderCategories(categoryID string, newPosition int) (*CommunityChanges, error) {
    if _, exists := o.config.CommunityDescription.Categories[categoryID]; !exists {
        return nil, ErrCategoryNotFound
    }

    if newPosition > 0 && newPosition >= len(o.config.CommunityDescription.Categories) {
        newPosition = len(o.config.CommunityDescription.Categories) - 1
    } else if newPosition < 0 {
        newPosition = 0
    }

    category := o.config.CommunityDescription.Categories[categoryID]
    if category.Position == int32(newPosition) {
        return nil, ErrNoChangeInPosition
    }

    decrease := false
    if category.Position > int32(newPosition) {
        decrease = true
    }

    // Sorting the categories because maps are not guaranteed to keep order
    s := make(sortSlice, 0, len(o.config.CommunityDescription.Categories))
    for k, v := range o.config.CommunityDescription.Categories {
        s = append(s, sorterHelperIdx{
            pos:   v.Position,
            catID: k,
        })
    }
    sort.Sort(s)
    var communityCategories []*protobuf.CommunityCategory
    for _, currCat := range s {
        communityCategories = append(communityCategories, o.config.CommunityDescription.Categories[currCat.catID])
    }

    var sortedCategoryIDs []string
    for _, v := range communityCategories {
        if v != category && ((decrease && v.Position < int32(newPosition)) || (!decrease && v.Position <= int32(newPosition))) {
            sortedCategoryIDs = append(sortedCategoryIDs, v.CategoryId)
        }
    }

    sortedCategoryIDs = append(sortedCategoryIDs, categoryID)

    for _, v := range communityCategories {
        if v.CategoryId == categoryID || (decrease && v.Position < int32(newPosition)) || (!decrease && v.Position <= int32(newPosition)) {
            continue
        }
        sortedCategoryIDs = append(sortedCategoryIDs, v.CategoryId)
    }

    s = make(sortSlice, 0, len(o.config.CommunityDescription.Categories))
    for i, k := range sortedCategoryIDs {
        s = append(s, sorterHelperIdx{
            pos:   int32(i),
            catID: k,
        })
    }

    changes := o.emptyCommunityChanges()

    o.setModifiedCategories(changes, s)

    return changes, nil
}

func (o *Community) reorderChat(categoryID string, chatID string, newPosition int) (*CommunityChanges, error) {
    if categoryID != "" {
        if _, exists := o.config.CommunityDescription.Categories[categoryID]; !exists {
            return nil, ErrCategoryNotFound
        }
    }

    var chat *protobuf.CommunityChat
    var exists bool
    if chat, exists = o.config.CommunityDescription.Chats[chatID]; !exists {
        return nil, ErrChatNotFound
    }

    oldCategoryID := chat.CategoryId
    chat.CategoryId = categoryID

    changes := o.emptyCommunityChanges()

    o.SortCategoryChats(changes, oldCategoryID)
    o.insertAndSort(changes, oldCategoryID, categoryID, chatID, chat, newPosition)

    return changes, nil
}