diff --git a/cmd/event-service/di/inject_application.go b/cmd/event-service/di/inject_application.go index 2904a9f..cdcf649 100644 --- a/cmd/event-service/di/inject_application.go +++ b/cmd/event-service/di/inject_application.go @@ -19,4 +19,5 @@ var applicationSet = wire.NewSet( app.NewUpdateMetricsHandler, app.NewAddPublicKeyToMonitorHandler, app.NewGetEventHandler, + app.NewGetPublicKeyInfoHandler, ) diff --git a/cmd/event-service/di/wire_gen.go b/cmd/event-service/di/wire_gen.go index c7c062d..3410dd4 100644 --- a/cmd/event-service/di/wire_gen.go +++ b/cmd/event-service/di/wire_gen.go @@ -70,12 +70,14 @@ func BuildService(contextContext context.Context, configConfig config.Config) (S updateMetricsHandler := app.NewUpdateMetricsHandler(genericTransactionProvider, subscriber, logger, prometheusPrometheus) addPublicKeyToMonitorHandler := app.NewAddPublicKeyToMonitorHandler(genericTransactionProvider, logger, prometheusPrometheus) getEventHandler := app.NewGetEventHandler(genericTransactionProvider, logger, prometheusPrometheus) + getPublicKeyInfoHandler := app.NewGetPublicKeyInfoHandler(genericTransactionProvider, logger, prometheusPrometheus) application := app.Application{ SaveReceivedEvent: saveReceivedEventHandler, ProcessSavedEvent: processSavedEventHandler, UpdateMetrics: updateMetricsHandler, AddPublicKeyToMonitor: addPublicKeyToMonitorHandler, GetEvent: getEventHandler, + GetPublicKeyInfo: getPublicKeyInfoHandler, } server := http.NewServer(configConfig, logger, application, prometheusPrometheus) bootstrapRelaySource := relays.NewBootstrapRelaySource() diff --git a/service/adapters/sqlite/contact_repository.go b/service/adapters/sqlite/contact_repository.go index 5c9ab5a..e75a92a 100644 --- a/service/adapters/sqlite/contact_repository.go +++ b/service/adapters/sqlite/contact_repository.go @@ -181,3 +181,37 @@ func (r *ContactRepository) IsFolloweeOfMonitoredPublicKey(ctx context.Context, return true, nil } + +func (r *ContactRepository) CountFollowers(ctx context.Context, publicKey domain.PublicKey) (int, error) { + row := r.tx.QueryRow(` + SELECT COUNT(*) + FROM contacts_followees CF + INNER JOIN public_keys PK ON PK.id=CF.followee_id + WHERE PK.public_key=$1`, + publicKey.Hex(), + ) + + var count int + if err := row.Scan(&count); err != nil { + return 0, errors.Wrap(err, "scan err") + } + + return count, nil +} + +func (r *ContactRepository) CountFollowees(ctx context.Context, publicKey domain.PublicKey) (int, error) { + row := r.tx.QueryRow(` + SELECT COUNT(*) + FROM contacts_followees CF + INNER JOIN public_keys PK ON PK.id=CF.follower_id + WHERE PK.public_key=$1`, + publicKey.Hex(), + ) + + var count int + if err := row.Scan(&count); err != nil { + return 0, errors.Wrap(err, "scan err") + } + + return count, nil +} diff --git a/service/adapters/sqlite/contact_repository_test.go b/service/adapters/sqlite/contact_repository_test.go index 7e3078b..fc214c7 100644 --- a/service/adapters/sqlite/contact_repository_test.go +++ b/service/adapters/sqlite/contact_repository_test.go @@ -324,3 +324,122 @@ func BenchmarkContactRepository_GetCurrentContactsEvent(b *testing.B) { require.NoError(b, err) } } + +func TestContactRepository_CountFolloweesReturnsNumberOfFollowees(t *testing.T) { + ctx := fixtures.TestContext(t) + adapters := NewTestAdapters(ctx, t) + + pk1, sk1 := fixtures.SomeKeyPair() + event1 := fixtures.SomeEventWithAuthor(sk1) + followee11 := fixtures.SomePublicKey() + followee12 := fixtures.SomePublicKey() + + pk2, sk2 := fixtures.SomeKeyPair() + event2 := fixtures.SomeEventWithAuthor(sk2) + + err := adapters.TransactionProvider.Transact(ctx, func(ctx context.Context, adapters sqlite.TestAdapters) error { + n, err := adapters.ContactRepository.CountFollowees(ctx, pk1) + require.NoError(t, err) + require.Equal(t, 0, n) + + n, err = adapters.ContactRepository.CountFollowees(ctx, pk2) + require.NoError(t, err) + require.Equal(t, 0, n) + + return nil + }) + require.NoError(t, err) + + err = adapters.TransactionProvider.Transact(ctx, func(ctx context.Context, adapters sqlite.TestAdapters) error { + err := adapters.EventRepository.Save(ctx, event1) + require.NoError(t, err) + + err = adapters.EventRepository.Save(ctx, event2) + require.NoError(t, err) + + err = adapters.ContactRepository.SetContacts(ctx, event1, []domain.PublicKey{followee11, followee12}) + require.NoError(t, err) + + return nil + }) + require.NoError(t, err) + + err = adapters.TransactionProvider.Transact(ctx, func(ctx context.Context, adapters sqlite.TestAdapters) error { + n, err := adapters.ContactRepository.CountFollowees(ctx, pk1) + require.NoError(t, err) + require.Equal(t, 2, n) + + n, err = adapters.ContactRepository.CountFollowees(ctx, pk2) + require.NoError(t, err) + require.Equal(t, 0, n) + + return nil + }) + require.NoError(t, err) +} + +func TestContactRepository_CountFollowersReturnsNumberOfFollowers(t *testing.T) { + ctx := fixtures.TestContext(t) + adapters := NewTestAdapters(ctx, t) + + _, sk1 := fixtures.SomeKeyPair() + event1 := fixtures.SomeEventWithAuthor(sk1) + + _, sk2 := fixtures.SomeKeyPair() + event2 := fixtures.SomeEventWithAuthor(sk2) + + followee1 := fixtures.SomePublicKey() + followee2 := fixtures.SomePublicKey() + followee3 := fixtures.SomePublicKey() + + err := adapters.TransactionProvider.Transact(ctx, func(ctx context.Context, adapters sqlite.TestAdapters) error { + n, err := adapters.ContactRepository.CountFollowers(ctx, followee1) + require.NoError(t, err) + require.Equal(t, 0, n) + + n, err = adapters.ContactRepository.CountFollowers(ctx, followee2) + require.NoError(t, err) + require.Equal(t, 0, n) + + n, err = adapters.ContactRepository.CountFollowers(ctx, followee3) + require.NoError(t, err) + require.Equal(t, 0, n) + + return nil + }) + require.NoError(t, err) + + err = adapters.TransactionProvider.Transact(ctx, func(ctx context.Context, adapters sqlite.TestAdapters) error { + err := adapters.EventRepository.Save(ctx, event1) + require.NoError(t, err) + + err = adapters.EventRepository.Save(ctx, event2) + require.NoError(t, err) + + err = adapters.ContactRepository.SetContacts(ctx, event1, []domain.PublicKey{followee1, followee2}) + require.NoError(t, err) + + err = adapters.ContactRepository.SetContacts(ctx, event2, []domain.PublicKey{followee2}) + require.NoError(t, err) + + return nil + }) + require.NoError(t, err) + + err = adapters.TransactionProvider.Transact(ctx, func(ctx context.Context, adapters sqlite.TestAdapters) error { + n, err := adapters.ContactRepository.CountFollowers(ctx, followee1) + require.NoError(t, err) + require.Equal(t, 1, n) + + n, err = adapters.ContactRepository.CountFollowers(ctx, followee2) + require.NoError(t, err) + require.Equal(t, 2, n) + + n, err = adapters.ContactRepository.CountFollowers(ctx, followee3) + require.NoError(t, err) + require.Equal(t, 0, n) + + return nil + }) + require.NoError(t, err) +} diff --git a/service/app/app.go b/service/app/app.go index f1bf9dc..f22de6e 100644 --- a/service/app/app.go +++ b/service/app/app.go @@ -51,6 +51,8 @@ type ContactRepository interface { SetContacts(ctx context.Context, event domain.Event, contacts []domain.PublicKey) error GetFollowees(ctx context.Context, publicKey domain.PublicKey) ([]domain.PublicKey, error) IsFolloweeOfMonitoredPublicKey(ctx context.Context, publicKey domain.PublicKey) (bool, error) + CountFollowers(ctx context.Context, publicKey domain.PublicKey) (int, error) + CountFollowees(ctx context.Context, publicKey domain.PublicKey) (int, error) } type PublicKeysToMonitorRepository interface { @@ -73,6 +75,7 @@ type Application struct { UpdateMetrics *UpdateMetricsHandler AddPublicKeyToMonitor *AddPublicKeyToMonitorHandler GetEvent *GetEventHandler + GetPublicKeyInfo *GetPublicKeyInfoHandler } type ReceivedEvent struct { diff --git a/service/app/handler_get_public_key_info.go b/service/app/handler_get_public_key_info.go new file mode 100644 index 0000000..8c3e893 --- /dev/null +++ b/service/app/handler_get_public_key_info.go @@ -0,0 +1,78 @@ +package app + +import ( + "context" + + "github.com/boreq/errors" + "github.com/planetary-social/nos-event-service/internal/logging" + "github.com/planetary-social/nos-event-service/service/domain" +) + +type GetPublicKeyInfo struct { + publicKey domain.PublicKey +} + +func NewGetPublicKeyInfo(publicKey domain.PublicKey) GetPublicKeyInfo { + return GetPublicKeyInfo{publicKey: publicKey} +} + +type PublicKeyInfo struct { + numberOfFollowees int + numberOfFollowers int +} + +func NewPublicKeyInfo(numberOfFollowees int, numberOfFollowers int) PublicKeyInfo { + return PublicKeyInfo{numberOfFollowees: numberOfFollowees, numberOfFollowers: numberOfFollowers} +} + +func (p PublicKeyInfo) NumberOfFollowees() int { + return p.numberOfFollowees +} + +func (p PublicKeyInfo) NumberOfFollowers() int { + return p.numberOfFollowers +} + +type GetPublicKeyInfoHandler struct { + transactionProvider TransactionProvider + logger logging.Logger + metrics Metrics +} + +func NewGetPublicKeyInfoHandler( + transactionProvider TransactionProvider, + logger logging.Logger, + metrics Metrics, +) *GetPublicKeyInfoHandler { + return &GetPublicKeyInfoHandler{ + transactionProvider: transactionProvider, + logger: logger.New("getPublicKeyInfoHandler"), + metrics: metrics, + } +} + +func (h *GetPublicKeyInfoHandler) Handle(ctx context.Context, cmd GetPublicKeyInfo) (publicKeyInfo PublicKeyInfo, err error) { + defer h.metrics.StartApplicationCall("getPublicKeyInfo").End(&err) + + var followeesCount, followersCount int + + if err := h.transactionProvider.Transact(ctx, func(ctx context.Context, adapters Adapters) error { + tmp, err := adapters.Contacts.CountFollowees(ctx, cmd.publicKey) + if err != nil { + return errors.Wrap(err, "error counting followees") + } + followeesCount = tmp + + tmp, err = adapters.Contacts.CountFollowers(ctx, cmd.publicKey) + if err != nil { + return errors.Wrap(err, "error counting followers") + } + followersCount = tmp + + return nil + }); err != nil { + return PublicKeyInfo{}, errors.Wrap(err, "transaction error") + } + + return NewPublicKeyInfo(followeesCount, followersCount), nil +} diff --git a/service/ports/http/http.go b/service/ports/http/http.go index 58f93d0..f04cefa 100644 --- a/service/ports/http/http.go +++ b/service/ports/http/http.go @@ -63,6 +63,7 @@ func (s *Server) createMux() http.Handler { r := mux.NewRouter() r.Handle("/metrics", promhttp.HandlerFor(s.prometheus.Registry(), promhttp.HandlerOpts{})) r.HandleFunc("/events/{id}", rest.Wrap(s.serveEvents)) + r.HandleFunc("/public-keys/{hex}", rest.Wrap(s.servePublicKey)) r.HandleFunc("/", s.serveWs) return r } @@ -116,18 +117,48 @@ func (s *Server) serveEvents(r *http.Request) rest.RestResponse { return rest.ErrInternalServerError } - return rest.NewResponse(newGetEventTransport(event)) + return rest.NewResponse(newGetEventResponse(event)) default: return rest.ErrMethodNotAllowed } } -type getEventTransport struct { +type getEventResponse struct { Event json.RawMessage `json:"event"` } -func newGetEventTransport(event domain.Event) getEventTransport { - return getEventTransport{Event: event.Raw()} +func newGetEventResponse(event domain.Event) getEventResponse { + return getEventResponse{Event: event.Raw()} +} + +func (s *Server) servePublicKey(r *http.Request) rest.RestResponse { + vars := mux.Vars(r) + hexPublicKeyString := vars["hex"] + + publicKey, err := domain.NewPublicKeyFromHex(hexPublicKeyString) + if err != nil { + return rest.ErrBadRequest.WithMessage("invalid hex public key") + } + + publicKeyInfo, err := s.app.GetPublicKeyInfo.Handle(r.Context(), app.NewGetPublicKeyInfo(publicKey)) + if err != nil { + s.logger.Error().WithError(err).Message("error getting public key info") + return rest.ErrInternalServerError + } + + return rest.NewResponse(newGetPublicKeyResponse(publicKeyInfo)) +} + +type getPublicKeyResponse struct { + Followers int `json:"followers"` + Followees int `json:"followees"` +} + +func newGetPublicKeyResponse(info app.PublicKeyInfo) *getPublicKeyResponse { + return &getPublicKeyResponse{ + Followers: info.NumberOfFollowers(), + Followees: info.NumberOfFollowees(), + } } func (s *Server) handleConnection(ctx context.Context, conn *websocket.Conn) error {