github.com/decred/politeia@v1.4.0/politeiawww/legacy/comments/process.go (about)

     1  // Copyright (c) 2020-2021 The Decred developers
     2  // Use of this source code is governed by an ISC
     3  // license that can be found in the LICENSE file.
     4  
     5  package comments
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"fmt"
    11  
    12  	pdv2 "github.com/decred/politeia/politeiad/api/v2"
    13  	"github.com/decred/politeia/politeiad/plugins/comments"
    14  	v1 "github.com/decred/politeia/politeiawww/api/comments/v1"
    15  	"github.com/decred/politeia/politeiawww/config"
    16  	"github.com/decred/politeia/politeiawww/legacy/user"
    17  	"github.com/google/uuid"
    18  )
    19  
    20  func (c *Comments) processNew(ctx context.Context, n v1.New, u user.User) (*v1.NewReply, error) {
    21  	log.Tracef("processNew: %v %v %v", n.Token, u.Username)
    22  
    23  	// Verify state
    24  	state := convertStateToPlugin(n.State)
    25  	if state == comments.RecordStateInvalid {
    26  		return nil, v1.UserErrorReply{
    27  			ErrorCode: v1.ErrorCodeRecordStateInvalid,
    28  		}
    29  	}
    30  
    31  	// Verify user signed using active identity
    32  	if u.PublicKey() != n.PublicKey {
    33  		return nil, v1.UserErrorReply{
    34  			ErrorCode:    v1.ErrorCodePublicKeyInvalid,
    35  			ErrorContext: "not active identity",
    36  		}
    37  	}
    38  
    39  	// Execute pre plugin hooks. Checking the mode is a temporary
    40  	// measure until user plugins have been properly implemented.
    41  	switch c.cfg.Mode {
    42  	case config.PiWWWMode:
    43  		err := c.piHookNewPre(u)
    44  		if err != nil {
    45  			return nil, err
    46  		}
    47  	}
    48  
    49  	// Only admins and the record author are allowed to comment on
    50  	// unvetted records.
    51  	if n.State == v1.RecordStateUnvetted && !u.Admin {
    52  		// User is not an admin. Check if the user is the author.
    53  		authorID, err := c.politeiad.Author(ctx, n.Token)
    54  		if err != nil {
    55  			return nil, err
    56  		}
    57  		if u.ID.String() != authorID {
    58  			return nil, v1.UserErrorReply{
    59  				ErrorCode:    v1.ErrorCodeUnauthorized,
    60  				ErrorContext: "user is not author or admin",
    61  			}
    62  		}
    63  	}
    64  
    65  	// Send plugin command
    66  	cn := comments.New{
    67  		UserID:        u.ID.String(),
    68  		State:         state,
    69  		Token:         n.Token,
    70  		ParentID:      n.ParentID,
    71  		Comment:       n.Comment,
    72  		PublicKey:     n.PublicKey,
    73  		Signature:     n.Signature,
    74  		ExtraData:     n.ExtraData,
    75  		ExtraDataHint: n.ExtraDataHint,
    76  	}
    77  	pdc, err := c.politeiad.CommentNew(ctx, cn)
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  
    82  	// Prepare reply
    83  	cm := convertComment(*pdc)
    84  	commentPopulateUserData(&cm, u)
    85  
    86  	// Emit event
    87  	c.events.Emit(EventTypeNew,
    88  		EventNew{
    89  			State:   n.State,
    90  			Comment: cm,
    91  		})
    92  
    93  	return &v1.NewReply{
    94  		Comment: cm,
    95  	}, nil
    96  }
    97  
    98  func (c *Comments) processEdit(ctx context.Context, e v1.Edit, u user.User) (*v1.EditReply, error) {
    99  	log.Tracef("processEdit: %v %v", e.Token, e.CommentID)
   100  
   101  	// Verify state
   102  	state := convertStateToPlugin(e.State)
   103  	if state == comments.RecordStateInvalid {
   104  		return nil, v1.UserErrorReply{
   105  			ErrorCode: v1.ErrorCodeRecordStateInvalid,
   106  		}
   107  	}
   108  
   109  	// Verify user signed using active identity
   110  	if u.PublicKey() != e.PublicKey {
   111  		return nil, v1.UserErrorReply{
   112  			ErrorCode:    v1.ErrorCodePublicKeyInvalid,
   113  			ErrorContext: "not active identity",
   114  		}
   115  	}
   116  
   117  	// Ensure that session user ID is identical to the user ID included in the
   118  	// edit request payload.
   119  	if u.ID.String() != e.UserID {
   120  		return nil, v1.UserErrorReply{
   121  			ErrorCode:    v1.ErrorCodeUnauthorized,
   122  			ErrorContext: "user is not comment author",
   123  		}
   124  	}
   125  
   126  	// Execute pre plugin hooks. Checking the mode is a temporary
   127  	// measure until user plugins have been properly implemented.
   128  	switch c.cfg.Mode {
   129  	case config.PiWWWMode:
   130  		err := c.piHookEditPre(u)
   131  		if err != nil {
   132  			return nil, err
   133  		}
   134  	}
   135  
   136  	// Send plugin command
   137  	ce := comments.Edit{
   138  		UserID:        u.ID.String(),
   139  		State:         state,
   140  		Token:         e.Token,
   141  		ParentID:      e.ParentID,
   142  		CommentID:     e.CommentID,
   143  		Comment:       e.Comment,
   144  		PublicKey:     e.PublicKey,
   145  		Signature:     e.Signature,
   146  		ExtraData:     e.ExtraData,
   147  		ExtraDataHint: e.ExtraDataHint,
   148  	}
   149  	pdc, err := c.politeiad.CommentEdit(ctx, ce)
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	// Prepare reply
   155  	cm := convertComment(*pdc)
   156  	commentPopulateUserData(&cm, u)
   157  
   158  	return &v1.EditReply{
   159  		Comment: cm,
   160  	}, nil
   161  }
   162  
   163  func (c *Comments) processVote(ctx context.Context, v v1.Vote, u user.User) (*v1.VoteReply, error) {
   164  	log.Tracef("processVote: %v %v %v", v.Token, v.CommentID, v.Vote)
   165  
   166  	// Verify state
   167  	state := convertStateToPlugin(v.State)
   168  	if state == comments.RecordStateInvalid {
   169  		return nil, v1.UserErrorReply{
   170  			ErrorCode: v1.ErrorCodeRecordStateInvalid,
   171  		}
   172  	}
   173  
   174  	// Verify user signed using active identity
   175  	if u.PublicKey() != v.PublicKey {
   176  		return nil, v1.UserErrorReply{
   177  			ErrorCode:    v1.ErrorCodePublicKeyInvalid,
   178  			ErrorContext: "not active identity",
   179  		}
   180  	}
   181  
   182  	// Execute pre plugin hooks. Checking the mode is a temporary
   183  	// measure until user plugins have been properly implemented.
   184  	switch c.cfg.Mode {
   185  	case config.PiWWWMode:
   186  		err := c.piHookVotePre(u)
   187  		if err != nil {
   188  			return nil, err
   189  		}
   190  	}
   191  
   192  	// Votes are only allowed on vetted records
   193  	if v.State != v1.RecordStateVetted {
   194  		return nil, v1.UserErrorReply{
   195  			ErrorCode:    v1.ErrorCodeRecordStateInvalid,
   196  			ErrorContext: "comment voting is only allowed on vetted records",
   197  		}
   198  	}
   199  
   200  	// Send plugin command
   201  	cv := comments.Vote{
   202  		UserID:    u.ID.String(),
   203  		State:     state,
   204  		Token:     v.Token,
   205  		CommentID: v.CommentID,
   206  		Vote:      comments.VoteT(v.Vote),
   207  		PublicKey: v.PublicKey,
   208  		Signature: v.Signature,
   209  	}
   210  	vr, err := c.politeiad.CommentVote(ctx, cv)
   211  	if err != nil {
   212  		return nil, err
   213  	}
   214  
   215  	return &v1.VoteReply{
   216  		Downvotes: vr.Downvotes,
   217  		Upvotes:   vr.Upvotes,
   218  		Timestamp: vr.Timestamp,
   219  		Receipt:   vr.Receipt,
   220  	}, nil
   221  }
   222  
   223  func (c *Comments) processDel(ctx context.Context, d v1.Del, u user.User) (*v1.DelReply, error) {
   224  	log.Tracef("processDel: %v %v %v", d.Token, d.CommentID, d.Reason)
   225  
   226  	// Verify state
   227  	state := convertStateToPlugin(d.State)
   228  	if state == comments.RecordStateInvalid {
   229  		return nil, v1.UserErrorReply{
   230  			ErrorCode: v1.ErrorCodeRecordStateInvalid,
   231  		}
   232  	}
   233  
   234  	// Verify user signed with their active identity
   235  	if u.PublicKey() != d.PublicKey {
   236  		return nil, v1.UserErrorReply{
   237  			ErrorCode:    v1.ErrorCodePublicKeyInvalid,
   238  			ErrorContext: "not active identity",
   239  		}
   240  	}
   241  
   242  	// Send plugin command
   243  	cd := comments.Del{
   244  		State:     state,
   245  		Token:     d.Token,
   246  		CommentID: d.CommentID,
   247  		Reason:    d.Reason,
   248  		PublicKey: d.PublicKey,
   249  		Signature: d.Signature,
   250  	}
   251  	cdr, err := c.politeiad.CommentDel(ctx, cd)
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  
   256  	// Prepare reply
   257  	cm := convertComment(cdr.Comment)
   258  	commentPopulateUserData(&cm, u)
   259  
   260  	return &v1.DelReply{
   261  		Comment: cm,
   262  	}, nil
   263  }
   264  
   265  func (c *Comments) processCount(ctx context.Context, ct v1.Count) (*v1.CountReply, error) {
   266  	log.Tracef("processCount: %v", ct.Tokens)
   267  
   268  	// Verify size of request
   269  	switch {
   270  	case len(ct.Tokens) == 0:
   271  		// Nothing to do
   272  		return &v1.CountReply{
   273  			Counts: map[string]uint32{},
   274  		}, nil
   275  
   276  	case len(ct.Tokens) > int(c.policy.CountPageSize):
   277  		return nil, v1.UserErrorReply{
   278  			ErrorCode:    v1.ErrorCodePageSizeExceeded,
   279  			ErrorContext: fmt.Sprintf("max page size is %v", c.policy.CountPageSize),
   280  		}
   281  	}
   282  
   283  	// Get comment counts
   284  	counts, err := c.politeiad.CommentCount(ctx, ct.Tokens)
   285  	if err != nil {
   286  		return nil, err
   287  	}
   288  
   289  	return &v1.CountReply{
   290  		Counts: counts,
   291  	}, nil
   292  }
   293  
   294  func (c *Comments) processComments(ctx context.Context, cs v1.Comments, u *user.User) (*v1.CommentsReply, error) {
   295  	log.Tracef("processComments: %v", cs.Token)
   296  
   297  	// Send plugin command
   298  	pcomments, err := c.politeiad.CommentsGetAll(ctx, cs.Token)
   299  	if err != nil {
   300  		return nil, err
   301  	}
   302  	if len(pcomments) == 0 {
   303  		return &v1.CommentsReply{
   304  			Comments: []v1.Comment{},
   305  		}, nil
   306  	}
   307  
   308  	// Only admins and the record author are allowed to retrieve
   309  	// unvetted comments. This is a public route so a user might
   310  	// not exist.
   311  	if pcomments[0].State == comments.RecordStateUnvetted {
   312  		var isAllowed bool
   313  		switch {
   314  		case u == nil:
   315  			// No logged in user. Not allowed.
   316  			isAllowed = false
   317  		case u.Admin:
   318  			// User is an admin. Allowed.
   319  			isAllowed = true
   320  		default:
   321  			// User is not an admin. Get the record author.
   322  			authorID, err := c.politeiad.Author(ctx, cs.Token)
   323  			if err != nil {
   324  				return nil, err
   325  			}
   326  			if u.ID.String() == authorID {
   327  				// User is the author. Allowed.
   328  				isAllowed = true
   329  			}
   330  		}
   331  		if !isAllowed {
   332  			return nil, v1.UserErrorReply{
   333  				ErrorCode:    v1.ErrorCodeUnauthorized,
   334  				ErrorContext: "user is not author or admin",
   335  			}
   336  		}
   337  	}
   338  
   339  	// Prepare reply. Comment user data must be pulled from the
   340  	// userdb.
   341  	comments := make([]v1.Comment, 0, len(pcomments))
   342  	for _, v := range pcomments {
   343  		cm := convertComment(v)
   344  
   345  		// Get comment user data
   346  		uuid, err := uuid.Parse(cm.UserID)
   347  		if err != nil {
   348  			return nil, err
   349  		}
   350  		u, err := c.userdb.UserGetById(uuid)
   351  		if err != nil {
   352  			return nil, err
   353  		}
   354  		commentPopulateUserData(&cm, *u)
   355  
   356  		// Add comment
   357  		comments = append(comments, cm)
   358  	}
   359  
   360  	return &v1.CommentsReply{
   361  		Comments: comments,
   362  	}, nil
   363  }
   364  
   365  func (c *Comments) processVotes(ctx context.Context, v v1.Votes) (*v1.VotesReply, error) {
   366  	log.Tracef("processVotes: %v %v", v.Token, v.UserID)
   367  
   368  	// Get comment votes. Votes are only allowed on vetted comments so
   369  	// there is no need to check the user permissions since all vetted
   370  	// comments are public.
   371  	cm := comments.Votes{
   372  		UserID: v.UserID,
   373  		Page:   v.Page,
   374  	}
   375  	votes, err := c.politeiad.CommentVotes(ctx, v.Token, cm)
   376  	if err != nil {
   377  		return nil, err
   378  	}
   379  	cv := convertCommentVotes(votes)
   380  
   381  	// Populate comment votes with user data
   382  	err = c.commentVotesPopulateUserData(cv, v.UserID)
   383  	if err != nil {
   384  		return nil, err
   385  	}
   386  
   387  	return &v1.VotesReply{
   388  		Votes: cv,
   389  	}, nil
   390  }
   391  
   392  // usersBatchSize is the maximum number of users which can be fetched from
   393  // politeiawww and stored in memory while populating the comment votes structs
   394  // with the missing users data.
   395  var usersBatchSize = 10
   396  
   397  // commentVotePopulateUserData populates the comment votes with user data that
   398  // is not stored in politeiad. If all votes are associated with one user it
   399  // expects to get the user's ID as a parameter.
   400  func (c *Comments) commentVotesPopulateUserData(votes []v1.CommentVote, userID string) error {
   401  	// If given votes slice is emptry, nothing to do
   402  	if len(votes) == 0 {
   403  		return nil
   404  	}
   405  
   406  	// Collect the users public keys in a map to prevent duplicates and to
   407  	// retrieve the users in a batched db call.
   408  	var mPubkeys map[string]bool // map[pubkey]bool
   409  	if userID != "" {
   410  		// If user ID filter is applied, we have only one user
   411  		// to fetch.
   412  		mPubkeys = make(map[string]bool, 1)
   413  		mPubkeys[votes[0].PublicKey] = true
   414  	} else {
   415  		// If user ID filter is not applied, we need to collect all
   416  		// the user public keys from comment votes.
   417  		mPubkeys = make(map[string]bool, len(votes))
   418  		for _, vote := range votes {
   419  			if ok := mPubkeys[vote.UserID]; ok {
   420  				// If user uuid already known, skip
   421  				continue
   422  			}
   423  			mPubkeys[vote.PublicKey] = true
   424  		}
   425  	}
   426  
   427  	// Store public keys in a slice
   428  	pubkeys := make([]string, 0, len(mPubkeys))
   429  	for pubkey := range mPubkeys {
   430  		pubkeys = append(pubkeys, pubkey)
   431  	}
   432  
   433  	// Get users from db in batchs to avoid reading too many
   434  	// users into memory.
   435  	var batchStartIdx int
   436  	usernames := make(map[string]string, len(pubkeys))
   437  	for batchStartIdx < len(pubkeys) {
   438  		batchEndIdx := batchStartIdx + usersBatchSize
   439  		if batchEndIdx > len(pubkeys) {
   440  			// We've reached the end of the slice
   441  			batchEndIdx = len(pubkeys)
   442  		}
   443  
   444  		// batchStartIdx is included. batchEndIdx is excluded.
   445  		batch := pubkeys[batchStartIdx:batchEndIdx]
   446  
   447  		// Get batch of users
   448  		users, err := c.userdb.UsersGetByPubKey(batch)
   449  		if err != nil {
   450  			return err
   451  		}
   452  
   453  		// Map user IDs to usernames
   454  		for _, u := range users {
   455  			usernames[u.ID.String()] = u.Username
   456  		}
   457  
   458  		log.Debugf("Fetched a batch of %v users out of %v required users",
   459  			len(batch), len(pubkeys))
   460  
   461  		// Next batch start index
   462  		batchStartIdx = batchEndIdx
   463  	}
   464  
   465  	// Populate comment votes with usernames
   466  	for k := range votes {
   467  		username := usernames[votes[k].UserID]
   468  		votes[k].Username = username
   469  	}
   470  
   471  	return nil
   472  }
   473  
   474  func (c *Comments) processTimestamps(ctx context.Context, t v1.Timestamps, isAdmin bool) (*v1.TimestampsReply, error) {
   475  	log.Tracef("processTimestamps: %v %v", t.Token, t.CommentIDs)
   476  
   477  	// Verify size of request
   478  	switch {
   479  	case len(t.CommentIDs) == 0:
   480  		// Nothing to do
   481  		return &v1.TimestampsReply{
   482  			Comments: map[uint32]v1.CommentTimestamp{},
   483  		}, nil
   484  
   485  	case len(t.CommentIDs) > int(c.policy.TimestampsPageSize):
   486  		return nil, v1.UserErrorReply{
   487  			ErrorCode: v1.ErrorCodePageSizeExceeded,
   488  			ErrorContext: fmt.Sprintf("max page size is %v",
   489  				c.policy.TimestampsPageSize),
   490  		}
   491  	}
   492  
   493  	// Get record state
   494  	r, err := c.recordNoFiles(ctx, t.Token)
   495  	if err != nil {
   496  		if err == errRecordNotFound {
   497  			return nil, v1.UserErrorReply{
   498  				ErrorCode: v1.ErrorCodeRecordNotFound,
   499  			}
   500  		}
   501  		return nil, err
   502  	}
   503  
   504  	// Get timestamps
   505  	ct := comments.Timestamps{
   506  		CommentIDs: t.CommentIDs,
   507  	}
   508  	ctr, err := c.politeiad.CommentTimestamps(ctx, t.Token, ct)
   509  	if err != nil {
   510  		return nil, err
   511  	}
   512  
   513  	// Prepare reply
   514  	var (
   515  		comments = make(map[uint32]v1.CommentTimestamp, len(ctr.Comments))
   516  
   517  		// Unvetted data payloads are removed from the timestamp if the
   518  		// user is not an admin.
   519  		rmPayloads = (r.State == pdv2.RecordStateUnvetted) && !isAdmin
   520  	)
   521  	for commentID, ct := range ctr.Comments {
   522  		adds := make([]v1.Timestamp, 0, len(ct.Adds))
   523  		for _, ts := range ct.Adds {
   524  			if rmPayloads {
   525  				ts.Data = ""
   526  			}
   527  			adds = append(adds, convertTimestamp(ts))
   528  		}
   529  
   530  		var del *v1.Timestamp
   531  		if ct.Del != nil {
   532  			if rmPayloads {
   533  				ct.Del.Data = ""
   534  			}
   535  			d := convertTimestamp(*ct.Del)
   536  			del = &d
   537  		}
   538  
   539  		comments[commentID] = v1.CommentTimestamp{
   540  			Adds: adds,
   541  			Del:  del,
   542  		}
   543  	}
   544  
   545  	return &v1.TimestampsReply{
   546  		Comments: comments,
   547  	}, nil
   548  }
   549  
   550  var (
   551  	errRecordNotFound = errors.New("record not found")
   552  )
   553  
   554  // recordNoFiles returns a politeiad record without any of its files. This
   555  // allows the call to be light weight but still return metadata about the
   556  // record such as state and status.
   557  func (c *Comments) recordNoFiles(ctx context.Context, token string) (*pdv2.Record, error) {
   558  	req := []pdv2.RecordRequest{
   559  		{
   560  			Token:        token,
   561  			OmitAllFiles: true,
   562  		},
   563  	}
   564  	records, err := c.politeiad.Records(ctx, req)
   565  	if err != nil {
   566  		return nil, err
   567  	}
   568  	r, ok := records[token]
   569  	if !ok {
   570  		return nil, errRecordNotFound
   571  	}
   572  
   573  	return &r, nil
   574  }
   575  
   576  // commentPopulateUserData populates the comment with user data that is not
   577  // stored in politeiad.
   578  func commentPopulateUserData(c *v1.Comment, u user.User) {
   579  	c.Username = u.Username
   580  }
   581  
   582  func convertStateToPlugin(s v1.RecordStateT) comments.RecordStateT {
   583  	switch s {
   584  	case v1.RecordStateUnvetted:
   585  		return comments.RecordStateUnvetted
   586  	case v1.RecordStateVetted:
   587  		return comments.RecordStateVetted
   588  	}
   589  	return comments.RecordStateInvalid
   590  }
   591  
   592  func convertStateToV1(s comments.RecordStateT) v1.RecordStateT {
   593  	switch s {
   594  	case comments.RecordStateUnvetted:
   595  		return v1.RecordStateUnvetted
   596  	case comments.RecordStateVetted:
   597  		return v1.RecordStateVetted
   598  	}
   599  	return v1.RecordStateInvalid
   600  }
   601  
   602  func convertComment(c comments.Comment) v1.Comment {
   603  	// Fields that are intentionally omitted are not stored in
   604  	// politeiad. They need to be pulled from the userdb.
   605  	return v1.Comment{
   606  		UserID:        c.UserID,
   607  		Username:      "", // Intentionally omitted
   608  		State:         convertStateToV1(c.State),
   609  		Token:         c.Token,
   610  		ParentID:      c.ParentID,
   611  		Comment:       c.Comment,
   612  		PublicKey:     c.PublicKey,
   613  		Signature:     c.Signature,
   614  		CommentID:     c.CommentID,
   615  		Version:       c.Version,
   616  		CreatedAt:     c.CreatedAt,
   617  		Timestamp:     c.Timestamp,
   618  		Receipt:       c.Receipt,
   619  		Downvotes:     c.Downvotes,
   620  		Upvotes:       c.Upvotes,
   621  		Deleted:       c.Deleted,
   622  		Reason:        c.Reason,
   623  		ExtraData:     c.ExtraData,
   624  		ExtraDataHint: c.ExtraDataHint,
   625  	}
   626  }
   627  
   628  func convertCommentVotes(cv []comments.CommentVote) []v1.CommentVote {
   629  	c := make([]v1.CommentVote, 0, len(cv))
   630  	for _, v := range cv {
   631  		c = append(c, v1.CommentVote{
   632  			UserID:    v.UserID,
   633  			Token:     v.Token,
   634  			State:     convertStateToV1(v.State),
   635  			CommentID: v.CommentID,
   636  			Vote:      v1.VoteT(v.Vote),
   637  			PublicKey: v.PublicKey,
   638  			Signature: v.Signature,
   639  			Timestamp: v.Timestamp,
   640  			Receipt:   v.Receipt,
   641  		})
   642  	}
   643  	return c
   644  }
   645  
   646  func convertProof(p comments.Proof) v1.Proof {
   647  	return v1.Proof{
   648  		Type:       p.Type,
   649  		Digest:     p.Digest,
   650  		MerkleRoot: p.MerkleRoot,
   651  		MerklePath: p.MerklePath,
   652  		ExtraData:  p.ExtraData,
   653  	}
   654  }
   655  
   656  func convertTimestamp(t comments.Timestamp) v1.Timestamp {
   657  	proofs := make([]v1.Proof, 0, len(t.Proofs))
   658  	for _, v := range t.Proofs {
   659  		proofs = append(proofs, convertProof(v))
   660  	}
   661  	return v1.Timestamp{
   662  		Data:       t.Data,
   663  		Digest:     t.Digest,
   664  		TxID:       t.TxID,
   665  		MerkleRoot: t.MerkleRoot,
   666  		Proofs:     proofs,
   667  	}
   668  }