Add an RPC endpoint listing all members

Currently an RPC endpoint which lists only the connected members is
available. A list of all members, even those who are offline, is
available only using the web dashboard. This pull request adds an RPC
endpoint which lists all members of the room.

This new endpoint can be used to augment SSB clients with extra
information about the rooms. For example friends who are room members
can be displayed in room-related settings or information about shared
rooms can be displayed in user profiles.

The new endpoint is a source endpoint called ["room", "members"]. Source
endpoint was selected to make it possible to return multiple smaller
responses instead of one large response - an async endpoint could
struggle to return the list of all members in case of larger rooms. Each
response carries a list of member objects. Currently the implementation
naively returns one member per response message but that can be adjusted
in the future.

Currently the request takes no arguments but extra options could be
added in the future.

Currently member objects only have one property: their id. This can be
extended in the future.
This commit is contained in:
boreq 2022-10-31 16:42:32 +01:00 committed by André Staltz
parent 158e5129ba
commit ea9b22cfa7
6 changed files with 163 additions and 8 deletions

View File

@ -43,7 +43,7 @@ func (h *Handler) attendants(ctx context.Context, req *muxrpc.Request, snk *muxr
}
if pm == roomdb.ModeCommunity || pm == roomdb.ModeRestricted {
_, err := h.members.GetByFeed(ctx, *peer)
_, err := h.membersdb.GetByFeed(ctx, *peer)
if err != nil {
return fmt.Errorf("external user are not allowed to enumerate members")
}

View File

@ -0,0 +1,57 @@
// SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021
//
// SPDX-License-Identifier: MIT
package server
import (
"context"
"encoding/json"
"fmt"
"github.com/ssb-ngi-pointer/go-ssb-room/v2/internal/network"
"github.com/ssb-ngi-pointer/go-ssb-room/v2/roomdb"
"go.cryptoscope.co/muxrpc/v2"
refs "go.mindeco.de/ssb-refs"
)
type Member struct {
ID refs.FeedRef `json:"id"`
}
func (h *Handler) members(ctx context.Context, req *muxrpc.Request, snk *muxrpc.ByteSink) error {
peer, err := network.GetFeedRefFromAddr(req.RemoteAddr())
if err != nil {
return err
}
pm, err := h.config.GetPrivacyMode(ctx)
if err != nil {
return fmt.Errorf("running with unknown privacy mode: %w", err)
}
if pm == roomdb.ModeCommunity || pm == roomdb.ModeRestricted {
_, err := h.membersdb.GetByFeed(ctx, *peer)
if err != nil {
return fmt.Errorf("external user are not allowed to list members: %w", err)
}
}
members, err := h.membersdb.List(ctx)
if err != nil {
return fmt.Errorf("error listing members: %w", err)
}
snk.SetEncoding(muxrpc.TypeJSON)
for _, member := range members {
if err = json.NewEncoder(snk).Encode([]Member{
{
ID: member.PubKey,
},
}); err != nil {
return fmt.Errorf("encoder error: %w", err)
}
}
return snk.Close()
}

View File

@ -30,7 +30,7 @@ func New(log kitlog.Logger, netInfo network.ServerEndpointDetails, m *roomstate.
h.netInfo = netInfo
h.logger = log
h.state = m
h.members = members
h.membersdb = members
h.config = config
return h
@ -59,6 +59,7 @@ func (h *Handler) RegisterRoom(mux typemux.HandlerMux) {
mux.RegisterAsync(append(namespace, "ping"), typemux.AsyncFunc(h.ping))
mux.RegisterSource(append(namespace, "attendants"), typemux.SourceFunc(h.attendants))
mux.RegisterSource(append(namespace, "members"), typemux.SourceFunc(h.members))
mux.RegisterDuplex(append(namespace, "connect"), connectHandler{
logger: h.logger,

View File

@ -23,10 +23,10 @@ import (
type Handler struct {
logger kitlog.Logger
netInfo network.ServerEndpointDetails
state *roomstate.Manager
members roomdb.MembersService
config roomdb.RoomConfig
netInfo network.ServerEndpointDetails
state *roomstate.Manager
membersdb roomdb.MembersService
config roomdb.RoomConfig
}
type MetadataReply struct {
@ -50,7 +50,7 @@ func (h *Handler) metadata(ctx context.Context, req *muxrpc.Request) (interface{
reply.Name = h.netInfo.Domain
// check if caller is a member
if _, err := h.members.GetByFeed(ctx, *ref); err != nil {
if _, err := h.membersdb.GetByFeed(ctx, *ref); err != nil {
if !errors.Is(err, roomdb.ErrNotFound) {
return nil, err
}
@ -123,7 +123,7 @@ func (h *Handler) endpoints(ctx context.Context, req *muxrpc.Request, snk *muxrp
case roomdb.ModeCommunity:
fallthrough
case roomdb.ModeRestricted:
_, err := h.members.GetByFeed(ctx, *peer)
_, err := h.membersdb.GetByFeed(ctx, *peer)
if err != nil {
return fmt.Errorf("external user are not allowed to enumerate members")
}

View File

@ -0,0 +1,96 @@
// SPDX-FileCopyrightText: 2021 The NGI Pointer Secure-Scuttlebutt Team of 2020/2021
//
// SPDX-License-Identifier: MIT
package go_test
import (
"context"
"encoding/json"
"github.com/ssb-ngi-pointer/go-ssb-room/v2/internal/maybemod/keys"
"github.com/ssb-ngi-pointer/go-ssb-room/v2/muxrpc/handlers/tunnel/server"
"github.com/ssb-ngi-pointer/go-ssb-room/v2/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/v2/roomsrv"
"math/rand"
"testing"
"time"
"github.com/stretchr/testify/require"
"go.cryptoscope.co/muxrpc/v2"
)
// this tests the new room.members call
func TestRoomMembers(t *testing.T) {
testInit(t)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
r := require.New(t)
appKey := make([]byte, 32)
rand.Read(appKey)
netOpts := []roomsrv.Option{
roomsrv.WithAppKey(appKey),
roomsrv.WithContext(ctx),
}
session := makeNamedTestBot(t, "srv", ctx, netOpts)
aliceKey, err := keys.NewKeyPair(nil)
r.NoError(err)
bobKey, err := keys.NewKeyPair(nil)
r.NoError(err)
bobSession := makeNamedTestBot(t, "bob", ctx, append(netOpts,
roomsrv.WithKeyPair(bobKey),
))
_, err = session.srv.Members.Add(ctx, aliceKey.Feed, roomdb.RoleMember)
r.NoError(err)
_, err = session.srv.Members.Add(ctx, bobKey.Feed, roomdb.RoleMember)
r.NoError(err)
// allow bots to dial the remote
// side-effect of re-using a room-server as the client
_, err = bobSession.srv.Members.Add(ctx, session.srv.Whoami(), roomdb.RoleMember)
r.NoError(err)
err = bobSession.srv.Network.Connect(ctx, session.srv.Network.GetListenAddr())
r.NoError(err, "connect A to the Server")
t.Log("letting handshaking settle..")
time.Sleep(1 * time.Second)
clientForServer, ok := bobSession.srv.Network.GetEndpointFor(session.srv.Whoami())
r.True(ok)
src, err := clientForServer.Source(ctx, muxrpc.TypeString, muxrpc.Method{"room", "members"})
r.NoError(err)
var responses []server.Member
for src.Next(ctx) {
bytes, err := src.Bytes()
r.NoError(err)
var members []server.Member
err = json.Unmarshal(bytes, &members)
r.NoError(err)
responses = append(responses, members...)
}
r.Equal(
[]server.Member{
{
ID: aliceKey.Feed,
},
{
ID: bobKey.Feed,
},
},
responses,
)
}

View File

@ -45,6 +45,7 @@ const manifest manifestHandler = `
"connect": "duplex",
"attendants": "source",
"members": "source",
"metadata": "async",
"ping": "sync"
},