github.com/qri-io/qri@v0.10.1-0.20220104210721-c771715036cb/lib/peers.go (about)

     1  package lib
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  
     8  	"github.com/qri-io/qri/base/params"
     9  	"github.com/qri-io/qri/config"
    10  	qhttp "github.com/qri-io/qri/lib/http"
    11  	"github.com/qri-io/qri/p2p"
    12  	"github.com/qri-io/qri/profile"
    13  	"github.com/qri-io/qri/repo"
    14  
    15  	peer "github.com/libp2p/go-libp2p-core/peer"
    16  	ma "github.com/multiformats/go-multiaddr"
    17  )
    18  
    19  // PeerMethods extends a lib.Instance with business logic for peer-to-peer
    20  // interaction
    21  type PeerMethods struct {
    22  	d dispatcher
    23  }
    24  
    25  // Name returns the name of this method group
    26  func (m PeerMethods) Name() string { return "peer" }
    27  
    28  // Attributes defines attributes for each method
    29  func (m PeerMethods) Attributes() map[string]AttributeSet {
    30  	return map[string]AttributeSet{
    31  		"list":                 {Endpoint: qhttp.AEPeers, HTTPVerb: "POST"},
    32  		"info":                 {Endpoint: qhttp.AEPeer, HTTPVerb: "POST"},
    33  		"connect":              {Endpoint: qhttp.AEConnect, HTTPVerb: "POST"},
    34  		"disconnect":           {Endpoint: qhttp.AEDisconnect, HTTPVerb: "POST"},
    35  		"connections":          {Endpoint: qhttp.AEConnections, HTTPVerb: "POST"},
    36  		"connectedqriprofiles": {Endpoint: qhttp.AEConnectedQriProfiles, HTTPVerb: "POST"},
    37  	}
    38  }
    39  
    40  // PeerListParams defines parameters for the List method
    41  type PeerListParams struct {
    42  	params.List
    43  	// Cached == true will return offline peers from the repo
    44  	// as well as online peers, default is to list connected peers only
    45  	Cached bool `json:"cached"`
    46  }
    47  
    48  // SetNonZeroDefaults sets a default limit and offset
    49  func (p *PeerListParams) SetNonZeroDefaults() {
    50  	if p.Offset < 0 {
    51  		p.Offset = 0
    52  	}
    53  	if p.Limit <= 0 {
    54  		p.Limit = params.DefaultListLimit
    55  	}
    56  }
    57  
    58  // List lists Peers on the qri network
    59  func (m PeerMethods) List(ctx context.Context, p *PeerListParams) ([]*config.ProfilePod, error) {
    60  	got, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "list"), p)
    61  	if res, ok := got.([]*config.ProfilePod); ok {
    62  		return res, err
    63  	}
    64  	return nil, dispatchReturnError(got, err)
    65  }
    66  
    67  // PeerInfoParams defines parameters for the Info method
    68  type PeerInfoParams struct {
    69  	Peername  string `json:"peername"`
    70  	ProfileID string `json:"profileID"`
    71  	// Verbose adds network details from the p2p Peerstore
    72  	Verbose bool `json:"verbose"`
    73  }
    74  
    75  // Info shows peer profile details
    76  func (m PeerMethods) Info(ctx context.Context, p *PeerInfoParams) (*config.ProfilePod, error) {
    77  	got, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "info"), p)
    78  	if res, ok := got.(*config.ProfilePod); ok {
    79  		return res, err
    80  	}
    81  	return nil, dispatchReturnError(got, err)
    82  }
    83  
    84  // Connect attempts to create a connection with a peer for a given peer.ID
    85  func (m PeerMethods) Connect(ctx context.Context, p *ConnectParamsPod) (*config.ProfilePod, error) {
    86  	got, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "connect"), p)
    87  	if res, ok := got.(*config.ProfilePod); ok {
    88  		return res, err
    89  	}
    90  	return nil, dispatchReturnError(got, err)
    91  }
    92  
    93  // Disconnect explicitly closes a peer connection
    94  func (m PeerMethods) Disconnect(ctx context.Context, p *ConnectParamsPod) error {
    95  	_, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "disconnect"), p)
    96  	return err
    97  }
    98  
    99  // ConnectionsParams defines parameters for the Connections method
   100  type ConnectionsParams struct {
   101  	params.List
   102  }
   103  
   104  // SetNonZeroDefaults sets a default limit and offset
   105  func (p *ConnectionsParams) SetNonZeroDefaults() {
   106  	if p.Offset < 0 {
   107  		p.Offset = 0
   108  	}
   109  	if p.Limit <= 0 {
   110  		p.Limit = params.DefaultListLimit
   111  	}
   112  }
   113  
   114  // Connections lists PeerID's we're currently connected to. If running
   115  // IPFS this will also return connected IPFS nodes
   116  func (m PeerMethods) Connections(ctx context.Context, p *ConnectionsParams) ([]string, error) {
   117  	got, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "connections"), p)
   118  	if res, ok := got.([]string); ok {
   119  		return res, err
   120  	}
   121  	return nil, dispatchReturnError(got, err)
   122  }
   123  
   124  // ConnectedQriProfiles lists profiles we're currently connected to
   125  func (m PeerMethods) ConnectedQriProfiles(ctx context.Context, p *ConnectionsParams) ([]*config.ProfilePod, error) {
   126  	got, _, err := m.d.Dispatch(ctx, dispatchMethodName(m, "connectedqriprofiles"), p)
   127  	if res, ok := got.([]*config.ProfilePod); ok {
   128  		return res, err
   129  	}
   130  	return nil, dispatchReturnError(got, err)
   131  }
   132  
   133  // ConnectParamsPod defines parameters for defining a connection
   134  // to a peer as plain-old-data
   135  type ConnectParamsPod struct {
   136  	Peername  string `json:"peername"`
   137  	ProfileID string `json:"profileID"`
   138  	NetworkID string `json:"networkID"`
   139  	Multiaddr string `json:"multiaddr"`
   140  }
   141  
   142  // NewConnectParamsPod attempts to turn a string into peer connection parameters
   143  func NewConnectParamsPod(s string) *ConnectParamsPod {
   144  	pcpod := &ConnectParamsPod{}
   145  
   146  	if strings.HasPrefix(s, "/ipfs/") {
   147  		pcpod.NetworkID = s
   148  	} else if maddr, err := ma.NewMultiaddr(s); err == nil {
   149  		pcpod.Multiaddr = maddr.String()
   150  	} else if len(s) == 46 && strings.HasPrefix(s, "Qm") {
   151  		pcpod.ProfileID = s
   152  	} else {
   153  		pcpod.Peername = s
   154  	}
   155  
   156  	return pcpod
   157  }
   158  
   159  func (p ConnectParamsPod) String() string {
   160  	if p.Peername != "" {
   161  		return p.Peername
   162  	}
   163  	if p.ProfileID != "" {
   164  		return p.ProfileID
   165  	}
   166  	if p.Multiaddr != "" {
   167  		return p.Multiaddr
   168  	}
   169  	if p.NetworkID != "" {
   170  		return p.NetworkID
   171  	}
   172  	return ""
   173  }
   174  
   175  // Decode turns plain-old-data into it's rich types
   176  func (p ConnectParamsPod) Decode() (cp p2p.PeerConnectionParams, err error) {
   177  	cp.Peername = p.Peername
   178  
   179  	if p.NetworkID != "" {
   180  		id := strings.TrimPrefix(p.NetworkID, "/ipfs/")
   181  		if len(id) == len(p.NetworkID) {
   182  			err = fmt.Errorf("network IDs must have a network prefix (eg. /ipfs/)")
   183  			return
   184  		}
   185  		if cp.PeerID, err = peer.IDB58Decode(id); err != nil {
   186  			err = fmt.Errorf("invalid networkID: %s", err.Error())
   187  			return
   188  		}
   189  	}
   190  
   191  	if p.ProfileID != "" {
   192  		if cp.ProfileID, err = profile.IDB58Decode(p.ProfileID); err != nil {
   193  			err = fmt.Errorf("invalid profileID: %s", err.Error())
   194  			return
   195  		}
   196  	}
   197  
   198  	if p.Multiaddr != "" {
   199  		if cp.Multiaddr, err = ma.NewMultiaddr(p.Multiaddr); err != nil {
   200  			err = fmt.Errorf("invalid multiaddr: %s", err.Error())
   201  		}
   202  	}
   203  
   204  	return
   205  }
   206  
   207  // peerImpl holds the method implementations for PeerMethods
   208  type peerImpl struct{}
   209  
   210  // List lists Peers on the qri network
   211  func (peerImpl) List(scope scope, p *PeerListParams) ([]*config.ProfilePod, error) {
   212  	res := []*config.ProfilePod{}
   213  
   214  	if scope.Node() == nil || !scope.Node().Online {
   215  		return nil, fmt.Errorf("error: not connected, run `qri connect` in another window")
   216  	}
   217  
   218  	var err error
   219  
   220  	// requesting user is hardcoded as node owner
   221  	u := scope.ActiveProfile()
   222  	res, err = p2p.ListPeers(scope.Context(), scope.Node(), u.ID, p.Offset, p.Limit, !p.Cached)
   223  	if err != nil {
   224  		return nil, err
   225  	}
   226  	return res, nil
   227  }
   228  
   229  // Info shows peer profile details
   230  func (peerImpl) Info(scope scope, p *PeerInfoParams) (*config.ProfilePod, error) {
   231  	res := config.ProfilePod{}
   232  	// TODO: Move most / all of this to p2p package, perhaps.
   233  	r := scope.Repo()
   234  
   235  	var pid profile.ID
   236  	var err error
   237  	if p.ProfileID != "" {
   238  		pid, err = profile.IDB58Decode(p.ProfileID)
   239  		if err != nil {
   240  			return nil, err
   241  		}
   242  	}
   243  
   244  	profiles, err := r.Profiles().List(scope.Context())
   245  	if err != nil {
   246  		log.Debug(err.Error())
   247  		return nil, err
   248  	}
   249  
   250  	for _, pro := range profiles {
   251  		if pro.ID == pid || pro.Peername == p.Peername {
   252  			if p.Verbose && len(pro.PeerIDs) > 0 {
   253  				// TODO - grab more than just the first peerID
   254  				pinfo := scope.Node().PeerInfo(pro.PeerIDs[0])
   255  				pro.NetworkAddrs = pinfo.Addrs
   256  			}
   257  
   258  			prof, err := pro.Encode()
   259  			if err != nil {
   260  				return nil, err
   261  			}
   262  			res = *prof
   263  
   264  			connected := scope.Node().ConnectedQriProfiles(scope.Context())
   265  
   266  			// If the requested profileID is in the list of connected peers, set Online flag.
   267  			if _, ok := connected[pro.ID]; ok {
   268  				res.Online = true
   269  			}
   270  			// If the requested profileID is myself and I'm Online, set Online flag.
   271  			if peer.ID(pro.ID) == scope.Node().ID && scope.Node().Online {
   272  				res.Online = true
   273  			}
   274  			return &res, nil
   275  		}
   276  	}
   277  
   278  	return nil, repo.ErrNotFound
   279  }
   280  
   281  // Connect attempts to create a connection with a peer for a given peer.ID
   282  func (peerImpl) Connect(scope scope, p *ConnectParamsPod) (*config.ProfilePod, error) {
   283  	pcp, err := p.Decode()
   284  	if err != nil {
   285  		return nil, err
   286  	}
   287  
   288  	prof, err := scope.Node().ConnectToPeer(scope.Context(), pcp)
   289  	if err != nil {
   290  		return nil, err
   291  	}
   292  
   293  	pro, err := prof.Encode()
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  
   298  	return pro, nil
   299  }
   300  
   301  // Disconnect explicitly closes a peer connection
   302  func (peerImpl) Disconnect(scope scope, p *ConnectParamsPod) error {
   303  	pcp, err := p.Decode()
   304  	if err != nil {
   305  		return err
   306  	}
   307  
   308  	return scope.Node().DisconnectFromPeer(scope.Context(), pcp)
   309  }
   310  
   311  // Connections lists PeerID's we're currently connected to. If running
   312  // IPFS this will also return connected IPFS nodes
   313  func (peerImpl) Connections(scope scope, p *ConnectionsParams) ([]string, error) {
   314  	// TODO (ramfox): limit and offset not currently used
   315  	return scope.Node().ConnectedPeers(), nil
   316  }
   317  
   318  // ConnectedQriProfiles lists profiles we're currently connected to
   319  func (peerImpl) ConnectedQriProfiles(scope scope, p *ConnectionsParams) ([]*config.ProfilePod, error) {
   320  	// TODO (ramfox): offset not currently used
   321  	connected := scope.Node().ConnectedQriProfiles(scope.Context())
   322  
   323  	build := make([]*config.ProfilePod, intMin(len(connected), p.Limit))
   324  	for _, pro := range connected {
   325  		build = append(build, pro)
   326  		if len(build) >= p.Limit {
   327  			break
   328  		}
   329  	}
   330  	return build, nil
   331  }
   332  
   333  func intMin(a, b int) int {
   334  	if a < b {
   335  		return a
   336  	}
   337  	return b
   338  }