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 }