From d3ab3e1715f06a23ae32d36df0d3317fa621e241 Mon Sep 17 00:00:00 2001 From: MishkaRogachev Date: Tue, 25 Jul 2023 17:13:15 +0400 Subject: [PATCH] feat: proposal for collecting community metrics https://github.com/status-im/status-desktop/issues/11152 --- protocol/messenger_community_metrics.go | 76 +++++++++++++++++++ protocol/messenger_community_metrics_test.go | 30 ++++++++ protocol/persistence_metrics.go | 75 ++++++++++++++++++ protocol/persistence_metrics_test.go | 1 + .../requests/community_metrics_request.go | 42 ++++++++++ services/ext/api.go | 4 + 6 files changed, 228 insertions(+) create mode 100644 protocol/messenger_community_metrics.go create mode 100644 protocol/messenger_community_metrics_test.go create mode 100644 protocol/persistence_metrics.go create mode 100644 protocol/persistence_metrics_test.go create mode 100644 protocol/requests/community_metrics_request.go diff --git a/protocol/messenger_community_metrics.go b/protocol/messenger_community_metrics.go new file mode 100644 index 0000000000..2c4c8743a4 --- /dev/null +++ b/protocol/messenger_community_metrics.go @@ -0,0 +1,76 @@ +package protocol + +import ( + "errors" + + "github.com/status-im/status-go/eth-node/types" + "github.com/status-im/status-go/protocol/requests" +) + +type CommunityMetricsResponse struct { + Type requests.CommunityMetricsRequestType `json:"type"` + CommunityID types.HexBytes `json:"communityId"` + Entries map[uint64]int32 `json:"entries"` +} + +func initRangesMap(start uint64, end uint64, step uint64) map[uint64]int32 { + result := map[uint64]int32{} + for timestamp := start; timestamp <= end; timestamp += step { + result[timestamp] = 0 + } + return result +} + +func floorToRange(value uint64, start uint64, end uint64, step uint64) uint64 { + for timestamp := start; timestamp <= end; timestamp += step { + if value <= timestamp { + return timestamp + } + } + return 0 +} + +func (m *Messenger) collectCommunityMessagesMetrics(request *requests.CommunityMetricsRequest) (*CommunityMetricsResponse, error) { + community, err := m.GetCommunityByID(request.CommunityID) + if err != nil { + return nil, err + } + + if community == nil { + return nil, errors.New("no community found") + } + + // TODO: timestamp summary should be stored in special table, not calculated here + timestamps, err := m.persistence.FetchMessageTimestampsForChatsByPeriod(community.ChatIDs(), request.StartTimestamp, request.EndTimestamp) + if err != nil { + return nil, err + } + + timestampStep := (request.EndTimestamp - request.StartTimestamp) / uint64(request.MaxCount) + entries := initRangesMap(request.StartTimestamp, request.EndTimestamp, timestampStep) + + for _, timestamp := range timestamps { + entries[floorToRange(timestamp, request.StartTimestamp, request.EndTimestamp, timestampStep)] += 1 + } + + response := &CommunityMetricsResponse{ + Type: request.Type, + CommunityID: request.CommunityID, + Entries: entries, + } + + return response, nil +} + +func (m *Messenger) CollectCommunityMetrics(request *requests.CommunityMetricsRequest) (*CommunityMetricsResponse, error) { + if err := request.Validate(); err != nil { + return nil, err + } + + switch request.Type { + case requests.CommunityMetricsRequestMessages: + return m.collectCommunityMessagesMetrics(request) + default: + return nil, errors.New("metrics is not implemented yet") + } +} diff --git a/protocol/messenger_community_metrics_test.go b/protocol/messenger_community_metrics_test.go new file mode 100644 index 0000000000..fe40e76625 --- /dev/null +++ b/protocol/messenger_community_metrics_test.go @@ -0,0 +1,30 @@ +package protocol + +import ( + "testing" + + "github.com/status-im/status-go/protocol/requests" + "github.com/stretchr/testify/suite" +) + +func TestMessengerCommunityMetricsSuite(t *testing.T) { + suite.Run(t, new(MessengerCommunityMetricsSuite)) +} + +type MessengerCommunityMetricsSuite struct { + MessengerBaseTestSuite +} + +func (s *MessengerCommunityMetricsSuite) TestCollectCommunityMessageMetrics() { + request := &requests.CommunityMetricsRequest{ + CommunityID: []byte("0x654321"), + Type: requests.CommunityMetricsRequestMessages, + StartTimestamp: 1690279200, + EndTimestamp: 1690282800, // one hour + MaxCount: 10, + } + // Send contact request + resp, err := s.m.CollectCommunityMetrics(request) + s.Require().NoError(err) + s.Require().NotNil(resp) +} diff --git a/protocol/persistence_metrics.go b/protocol/persistence_metrics.go new file mode 100644 index 0000000000..1e4bce660e --- /dev/null +++ b/protocol/persistence_metrics.go @@ -0,0 +1,75 @@ +package protocol + +import ( + "context" + "database/sql" +) + +func (db sqlitePersistence) fetchMessagesTimestampsForPeriod(tx *sql.Tx, chatID string, startTimestamp uint64, endTimestamp uint64) ([]uint64, error) { + rows, err := tx.Query(` + SELECT timestamp FROM user_messages + WHERE local_chat_id = ? AND + timestamp >= ? AND + timestamp <= ?`, + chatID, + startTimestamp, + endTimestamp) + if err != nil { + return []uint64{}, err + } + defer rows.Close() + + var timestamps []uint64 + for rows.Next() { + var timestamp uint64 + err := rows.Scan(×tamp) + if err != nil { + return nil, err + } + timestamps = append(timestamps, timestamp) + } + + return timestamps, nil +} + +func (db sqlitePersistence) FetchMessageTimestampsForChatByPeriod(chatID string, startTimestamp uint64, endTimestamp uint64) ([]uint64, error) { + tx, err := db.db.BeginTx(context.Background(), &sql.TxOptions{}) + if err != nil { + return []uint64{}, err + } + defer func() { + if err == nil { + err = tx.Commit() + return + } + // don't shadow original error + _ = tx.Rollback() + }() + + return db.fetchMessagesTimestampsForPeriod(tx, chatID, startTimestamp, endTimestamp) +} + +func (db sqlitePersistence) FetchMessageTimestampsForChatsByPeriod(chatIDs []string, startTimestamp uint64, endTimestamp uint64) ([]uint64, error) { + tx, err := db.db.BeginTx(context.Background(), &sql.TxOptions{}) + if err != nil { + return []uint64{}, err + } + defer func() { + if err == nil { + err = tx.Commit() + return + } + // don't shadow original error + _ = tx.Rollback() + }() + + var timestamps []uint64 + for _, chatID := range chatIDs { + chatTimestamps, err := db.fetchMessagesTimestampsForPeriod(tx, chatID, startTimestamp, endTimestamp) + if err != nil { + return []uint64{}, err + } + timestamps = append(timestamps, chatTimestamps...) + } + return timestamps, nil +} diff --git a/protocol/persistence_metrics_test.go b/protocol/persistence_metrics_test.go new file mode 100644 index 0000000000..a8caf75edc --- /dev/null +++ b/protocol/persistence_metrics_test.go @@ -0,0 +1 @@ +package protocol \ No newline at end of file diff --git a/protocol/requests/community_metrics_request.go b/protocol/requests/community_metrics_request.go new file mode 100644 index 0000000000..9b169f1eb4 --- /dev/null +++ b/protocol/requests/community_metrics_request.go @@ -0,0 +1,42 @@ +package requests + +import ( + "errors" + + "github.com/status-im/status-go/eth-node/types" +) + +var ErrNoCommunityId = errors.New("community metrics request has no community id") +var ErrInvalidTimeInterval = errors.New("community metrics request invalid time interval") +var ErrInvalidMaxCount = errors.New("community metrics request max count should be gratear than zero") + +type CommunityMetricsRequestType uint + +const ( + CommunityMetricsRequestMessages CommunityMetricsRequestType = iota + 1 + CommunityMetricsRequestMembers + CommunityMetricsRequestControlNodeUptime +) + +type CommunityMetricsRequest struct { + CommunityID types.HexBytes `json:"communityId"` + Type CommunityMetricsRequestType `json:"type"` + StartTimestamp uint64 `json:"startTimestamp"` + EndTimestamp uint64 `json:"endTimestamp"` + MaxCount uint `json:"maxCount"` +} + +func (r *CommunityMetricsRequest) Validate() error { + if len(r.CommunityID) == 0 { + return ErrNoCommunityId + } + + if r.StartTimestamp == 0 || r.EndTimestamp == 0 || r.StartTimestamp >= r.EndTimestamp { + return ErrInvalidTimeInterval + } + + if r.MaxCount < 1 { + return ErrInvalidMaxCount + } + return nil +} diff --git a/services/ext/api.go b/services/ext/api.go index 248154c3ff..449ca57aaa 100644 --- a/services/ext/api.go +++ b/services/ext/api.go @@ -1357,6 +1357,10 @@ func (api *PublicAPI) CheckAllCommunityChannelsPermissions(request *requests.Che return api.service.messenger.CheckAllCommunityChannelsPermissions(request) } +func (api *PublicAPI) CollectCommunityMetrics(request *requests.CommunityMetricsRequest) (*protocol.CommunityMetricsResponse, error) { + return api.service.messenger.CollectCommunityMetrics(request) +} + func (api *PublicAPI) ShareCommunityURLWithChatKey(communityID types.HexBytes) (string, error) { return api.service.messenger.ShareCommunityURLWithChatKey(communityID) }