github.com/decred/politeia@v1.4.0/politeiawww/legacy/pi/events.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 pi 6 7 import ( 8 "context" 9 "fmt" 10 11 pdv2 "github.com/decred/politeia/politeiad/api/v2" 12 cmplugin "github.com/decred/politeia/politeiad/plugins/comments" 13 piplugin "github.com/decred/politeia/politeiad/plugins/pi" 14 tkplugin "github.com/decred/politeia/politeiad/plugins/ticketvote" 15 cmv1 "github.com/decred/politeia/politeiawww/api/comments/v1" 16 rcv1 "github.com/decred/politeia/politeiawww/api/records/v1" 17 v1 "github.com/decred/politeia/politeiawww/api/records/v1" 18 tkv1 "github.com/decred/politeia/politeiawww/api/ticketvote/v1" 19 www "github.com/decred/politeia/politeiawww/api/www/v1" 20 "github.com/decred/politeia/politeiawww/client" 21 "github.com/decred/politeia/politeiawww/legacy/comments" 22 "github.com/decred/politeia/politeiawww/legacy/records" 23 "github.com/decred/politeia/politeiawww/legacy/ticketvote" 24 "github.com/decred/politeia/politeiawww/legacy/user" 25 "github.com/google/uuid" 26 ) 27 28 func (p *Pi) setupEventListeners() { 29 // Setup process for each event: 30 // 1. Create a channel for the event. 31 // 2. Register the channel with the event manager. 32 // 3. Launch an event handler to listen for events emitted into the 33 // channel by the event manager. 34 35 log.Debugf("Setting up pi event listeners") 36 37 // Record new 38 ch := make(chan interface{}) 39 p.events.Register(records.EventTypeNew, ch) 40 go p.handleEventRecordNew(ch) 41 42 // Record edit 43 ch = make(chan interface{}) 44 p.events.Register(records.EventTypeEdit, ch) 45 go p.handleEventRecordEdit(ch) 46 47 // Record set status 48 ch = make(chan interface{}) 49 p.events.Register(records.EventTypeSetStatus, ch) 50 go p.handleEventRecordSetStatus(ch) 51 52 // Comment new 53 ch = make(chan interface{}) 54 p.events.Register(comments.EventTypeNew, ch) 55 go p.handleEventCommentNew(ch) 56 57 // Ticket vote authorized 58 ch = make(chan interface{}) 59 p.events.Register(ticketvote.EventTypeAuthorize, ch) 60 go p.handleEventVoteAuthorized(ch) 61 62 // Ticket vote started 63 ch = make(chan interface{}) 64 p.events.Register(ticketvote.EventTypeStart, ch) 65 go p.handleEventVoteStarted(ch) 66 } 67 68 func (p *Pi) handleEventRecordNew(ch chan interface{}) { 69 for msg := range ch { 70 e, ok := msg.(records.EventNew) 71 if !ok { 72 log.Errorf("handleEventRecordNew invalid msg: %v", msg) 73 continue 74 } 75 76 // Compile notification email list 77 var ( 78 recipients = make(map[uuid.UUID]string, 1024) 79 ntfnBit = uint64(www.NotificationEmailAdminProposalNew) 80 ) 81 err := p.userdb.AllUsers(func(u *user.User) { 82 switch { 83 case !u.Admin: 84 // Only admins get this notification 85 return 86 case !u.NotificationIsEnabled(ntfnBit): 87 // Admin doesn't have notification bit set 88 return 89 default: 90 // User is an admin and has the notification bit set. Add 91 // them to the email list. 92 recipients[u.ID] = u.Email 93 } 94 }) 95 if err != nil { 96 log.Errorf("handleEventRecordNew: AllUsers: %v", err) 97 return 98 } 99 100 // Send notfication email 101 var ( 102 token = e.Record.CensorshipRecord.Token 103 name = proposalNameFromFiles(e.Record.Files) 104 ) 105 err = p.mailNtfnProposalNew(token, name, e.User.Username, recipients) 106 if err != nil { 107 log.Errorf("mailNtfnProposalNew: %v", err) 108 } 109 110 log.Debugf("Proposal new ntfn sent %v", token) 111 } 112 } 113 114 func (p *Pi) handleEventRecordEdit(ch chan interface{}) { 115 for msg := range ch { 116 e, ok := msg.(records.EventEdit) 117 if !ok { 118 log.Errorf("handleEventRecordEdit invalid msg: %v", msg) 119 continue 120 } 121 122 // Only send edit notifications for public proposals 123 if e.Record.State == rcv1.RecordStateUnvetted { 124 log.Debugf("Proposal is unvetted no edit ntfn %v", 125 e.Record.CensorshipRecord.Token) 126 continue 127 } 128 129 // Compile notification email list 130 var ( 131 recipients = make(map[uuid.UUID]string, 1024) 132 authorID = e.User.ID.String() 133 ntfnBit = uint64(www.NotificationEmailRegularProposalEdited) 134 ) 135 err := p.userdb.AllUsers(func(u *user.User) { 136 switch { 137 case u.ID.String() == authorID: 138 // User is the author. No need to send the notification to 139 // the author. 140 return 141 case u.NotificationIsEnabled(ntfnBit): 142 // User doesn't have notification bit set 143 return 144 default: 145 // User has the notification bit set. Add them to the email 146 // list. 147 recipients[u.ID] = u.Email 148 } 149 }) 150 if err != nil { 151 log.Errorf("handleEventRecordEdit: AllUsers: %v", err) 152 continue 153 } 154 155 // Send notification email 156 var ( 157 token = e.Record.CensorshipRecord.Token 158 version = e.Record.Version 159 name = proposalNameFromFiles(e.Record.Files) 160 username = e.User.Username 161 ) 162 err = p.mailNtfnProposalEdit(token, version, name, username, recipients) 163 if err != nil { 164 log.Errorf("mailNtfnProposaledit: %v", err) 165 continue 166 } 167 168 log.Debugf("Proposal edit ntfn sent %v", token) 169 } 170 } 171 172 func (p *Pi) ntfnRecordSetStatusToAuthor(r rcv1.Record) error { 173 // Unpack args 174 var ( 175 token = r.CensorshipRecord.Token 176 status = r.Status 177 name = proposalNameFromFiles(r.Files) 178 authorID = userIDFromMetadata(r.Metadata) 179 ) 180 181 // Parse the status change reason 182 sc, err := client.StatusChangesDecode(r.Metadata) 183 if err != nil { 184 return fmt.Errorf("decode status changes: %v", err) 185 } 186 if len(sc) == 0 { 187 return fmt.Errorf("not status changes found %v", token) 188 } 189 reason := sc[len(sc)-1].Reason 190 191 // Get author 192 uid, err := uuid.Parse(authorID) 193 if err != nil { 194 return err 195 } 196 author, err := p.userdb.UserGetById(uid) 197 if err != nil { 198 return fmt.Errorf("UserGetById %v: %v", uid, err) 199 } 200 201 // Send notification to author 202 ntfnBit := uint64(www.NotificationEmailRegularProposalVetted) 203 if !author.NotificationIsEnabled(ntfnBit) { 204 // Author does not have notification enabled 205 log.Debugf("Record set status ntfn to author not enabled %v", token) 206 return nil 207 } 208 209 // Author has notification enabled 210 recipient := map[uuid.UUID]string{ 211 uid: author.Email, 212 } 213 err = p.mailNtfnProposalSetStatusToAuthor(token, name, 214 status, reason, recipient) 215 if err != nil { 216 return fmt.Errorf("mailNtfnProposalSetStatusToAuthor: %v", err) 217 } 218 219 log.Debugf("Record set status ntfn to author sent %v", token) 220 221 return nil 222 } 223 224 func (p *Pi) ntfnRecordSetStatus(r rcv1.Record) error { 225 // Unpack args 226 var ( 227 token = r.CensorshipRecord.Token 228 status = r.Status 229 name = proposalNameFromFiles(r.Files) 230 authorID = userIDFromMetadata(r.Metadata) 231 ) 232 233 // Compile user notification email list 234 var ( 235 recipients = make(map[uuid.UUID]string, 1024) 236 ntfnBit = uint64(www.NotificationEmailRegularProposalVetted) 237 ) 238 err := p.userdb.AllUsers(func(u *user.User) { 239 switch { 240 case u.ID.String() == authorID: 241 // User is the author. The author is sent a different 242 // notification. Don't include them in the users list. 243 return 244 case !u.NotificationIsEnabled(ntfnBit): 245 // User does not have notification bit set 246 return 247 default: 248 // Add user to notification list 249 recipients[u.ID] = u.Email 250 } 251 }) 252 if err != nil { 253 return fmt.Errorf("AllUsers: %v", err) 254 } 255 256 // Send user notifications 257 err = p.mailNtfnProposalSetStatus(token, name, status, recipients) 258 if err != nil { 259 return fmt.Errorf("mailNtfnProposalSetStatus: %v", err) 260 } 261 262 log.Debugf("Record set status ntfn to users sent %v", token) 263 264 return nil 265 } 266 267 func (p *Pi) handleEventRecordSetStatus(ch chan interface{}) { 268 for msg := range ch { 269 e, ok := msg.(records.EventSetStatus) 270 if !ok { 271 log.Errorf("handleRecordSetStatus invalid msg: %v", msg) 272 continue 273 } 274 275 // Unpack args 276 var ( 277 token = e.Record.CensorshipRecord.Token 278 status = e.Record.Status 279 ) 280 281 // Verify a notification should be sent 282 switch status { 283 case rcv1.RecordStatusPublic, rcv1.RecordStatusCensored: 284 // Status requires a notification be sent 285 default: 286 // Status does not require a notification be sent 287 log.Debugf("Record set status ntfn not needed for %v status %v", 288 rcv1.RecordStatuses[status], token) 289 continue 290 } 291 292 // Send notification to the author 293 err := p.ntfnRecordSetStatusToAuthor(e.Record) 294 if err != nil { 295 // Log the error and continue. This error should not prevent 296 // the other notifications from attempting to be sent. 297 log.Errorf("ntfnRecordSetStatusToAuthor: %v", err) 298 } 299 300 // Only send a notification to non-author users if the proposal 301 // is being made public. 302 if status != rcv1.RecordStatusPublic { 303 log.Debugf("Record set status ntfn to users not needed for %v status %v", 304 rcv1.RecordStatuses[status], token) 305 continue 306 } 307 308 // Send notification to the users 309 err = p.ntfnRecordSetStatus(e.Record) 310 if err != nil { 311 log.Errorf("ntfnRecordSetStatus: %v", err) 312 continue 313 } 314 315 // Notifications sent! 316 continue 317 } 318 } 319 320 func (p *Pi) ntfnCommentNewProposalAuthor(c cmv1.Comment, proposalAuthorID, proposalName string) error { 321 // Get the proposal author 322 uid, err := uuid.Parse(proposalAuthorID) 323 if err != nil { 324 return err 325 } 326 pauthor, err := p.userdb.UserGetById(uid) 327 if err != nil { 328 return fmt.Errorf("UserGetByID %v: %v", uid.String(), err) 329 } 330 331 // Check if notification should be sent 332 ntfnBit := uint64(www.NotificationEmailCommentOnMyProposal) 333 switch { 334 case c.Username == pauthor.Username: 335 // Author commented on their own proposal 336 log.Debugf("Comment ntfn to proposal author not needed %v", c.Token) 337 return nil 338 case !pauthor.NotificationIsEnabled(ntfnBit): 339 // Author does not have notification bit set on 340 log.Debugf("Comment ntfn to proposal author not enabled %v", c.Token) 341 return nil 342 } 343 344 // Send notification email 345 recipient := map[uuid.UUID]string{ 346 pauthor.ID: pauthor.Email, 347 } 348 err = p.mailNtfnCommentNewToProposalAuthor(c.Token, c.CommentID, 349 c.Username, proposalName, recipient) 350 if err != nil { 351 return err 352 } 353 354 log.Debugf("Comment new ntfn to proposal author sent %v", c.Token) 355 356 return nil 357 } 358 359 func (p *Pi) ntfnCommentReply(c cmv1.Comment, proposalName string) error { 360 // Verify there is work to do. This notification only applies to 361 // reply comments. 362 if c.ParentID == 0 { 363 log.Debugf("Comment reply ntfn not needed %v", c.Token) 364 return nil 365 } 366 367 // Get the parent comment author 368 g := cmplugin.Get{ 369 CommentIDs: []uint32{c.ParentID}, 370 } 371 cs, err := p.politeiad.CommentsGet(context.Background(), c.Token, g) 372 if err != nil { 373 return err 374 } 375 parent, ok := cs[c.ParentID] 376 if !ok { 377 return fmt.Errorf("parent comment %v not found", c.ParentID) 378 } 379 userID, err := uuid.Parse(parent.UserID) 380 if err != nil { 381 return err 382 } 383 pauthor, err := p.userdb.UserGetById(userID) 384 if err != nil { 385 return err 386 } 387 388 // Check if notification should be sent 389 ntfnBit := uint64(www.NotificationEmailCommentOnMyComment) 390 switch { 391 case c.UserID == pauthor.ID.String(): 392 // Author replied to their own comment 393 log.Debugf("Comment reply ntfn to parent author not needed %v", c.Token) 394 return nil 395 case !pauthor.NotificationIsEnabled(ntfnBit): 396 // Author does not have notification bit set 397 log.Debugf("Comment reply ntfn to parent author not enabled %v", c.Token) 398 return nil 399 } 400 401 // Send notification email 402 recipient := map[uuid.UUID]string{ 403 pauthor.ID: pauthor.Email, 404 } 405 err = p.mailNtfnCommentReply(c.Token, c.CommentID, 406 c.Username, proposalName, recipient) 407 if err != nil { 408 return err 409 } 410 411 log.Debugf("Comment reply ntfn to parent author sent %v", c.Token) 412 413 return nil 414 } 415 416 func (p *Pi) handleEventCommentNew(ch chan interface{}) { 417 for msg := range ch { 418 e, ok := msg.(comments.EventNew) 419 if !ok { 420 log.Errorf("handleEventCommentNew invalid msg: %v", msg) 421 continue 422 } 423 424 // Get the record author and record name 425 var ( 426 pdr *pdv2.Record 427 r rcv1.Record 428 proposalAuthorID string 429 proposalName string 430 err error 431 ) 432 pdr, err = p.recordAbridged(e.Comment.Token) 433 if err != nil { 434 goto failed 435 } 436 r = convertRecordToV1(*pdr) 437 proposalAuthorID = userIDFromMetadata(r.Metadata) 438 proposalName = proposalNameFromFiles(r.Files) 439 440 // Notify the proposal author 441 err = p.ntfnCommentNewProposalAuthor(e.Comment, 442 proposalAuthorID, proposalName) 443 if err != nil { 444 // Log error and continue. This error should not prevent the 445 // other notifications from attempting to be sent. 446 log.Errorf("ntfnCommentNewProposalAuthor: %v", err) 447 } 448 449 // Notify the parent comment author 450 err = p.ntfnCommentReply(e.Comment, proposalName) 451 if err != nil { 452 err = fmt.Errorf("ntfnCommentReply: %v", err) 453 goto failed 454 } 455 456 // Notifications sent! 457 continue 458 459 failed: 460 log.Errorf("handleEventCommentNew: %v", err) 461 continue 462 } 463 } 464 465 func (p *Pi) handleEventVoteAuthorized(ch chan interface{}) { 466 for msg := range ch { 467 e, ok := msg.(ticketvote.EventAuthorize) 468 if !ok { 469 log.Errorf("handleEventVoteAuthorized invalid msg: %v", msg) 470 continue 471 } 472 473 // Verify there is work to do. We don't need to send a 474 // notification on revocations. 475 if e.Auth.Action != tkv1.AuthActionAuthorize { 476 log.Debugf("Vote authorize ntfn to admin not needed %v", e.Auth.Token) 477 continue 478 } 479 480 // Setup args to prevent goto errors 481 var ( 482 token = e.Auth.Token 483 proposalName string 484 r rcv1.Record 485 recipients = make(map[uuid.UUID]string, 1024) 486 ntfnBit = uint64(www.NotificationEmailAdminProposalVoteAuthorized) 487 err error 488 ) 489 490 // Get record 491 pdr, err := p.recordAbridged(token) 492 if err != nil { 493 goto failed 494 } 495 r = convertRecordToV1(*pdr) 496 proposalName = proposalNameFromFiles(r.Files) 497 498 // Compile notification email list 499 err = p.userdb.AllUsers(func(u *user.User) { 500 switch { 501 case !u.Admin: 502 // Only notify admin users 503 return 504 case !u.NotificationIsEnabled(ntfnBit): 505 // Admin does not have notfication enabled 506 return 507 default: 508 // Admin has notification enabled 509 recipients[u.ID] = u.Email 510 } 511 }) 512 if err != nil { 513 err = fmt.Errorf("AllUsers: %v", err) 514 goto failed 515 } 516 517 // Send notification email 518 err = p.mailNtfnVoteAuthorized(token, proposalName, recipients) 519 if err != nil { 520 err = fmt.Errorf("mailNtfnVoteAuthorized: %v", err) 521 goto failed 522 } 523 524 log.Debugf("Vote authorized ntfn to admin sent %v", e.Auth.Token) 525 continue 526 527 failed: 528 log.Errorf("handleEventVoteAuthorized: %v", err) 529 continue 530 } 531 } 532 533 func (p *Pi) ntfnVoteStartedToAuthor(sd tkv1.StartDetails, authorID, proposalName string) error { 534 var ( 535 token = sd.Params.Token 536 ntfnBit = uint64(www.NotificationEmailRegularProposalVoteStarted) 537 ) 538 539 // Get record author 540 uid, err := uuid.Parse(authorID) 541 if err != nil { 542 return err 543 } 544 author, err := p.userdb.UserGetById(uid) 545 if err != nil { 546 return fmt.Errorf("UserGetByID %v: %v", authorID, err) 547 } 548 549 // Verify author notification settings 550 if !author.NotificationIsEnabled(ntfnBit) { 551 log.Debugf("Vote started ntfn to author not enabled %v", token) 552 return nil 553 } 554 555 // Send notification to author 556 recipient := map[uuid.UUID]string{ 557 author.ID: author.Email, 558 } 559 err = p.mailNtfnVoteStartedToAuthor(token, proposalName, recipient) 560 if err != nil { 561 return err 562 } 563 564 log.Debugf("Vote started ntfn to author sent %v", token) 565 566 return nil 567 } 568 569 func (p *Pi) ntfnVoteStarted(sd tkv1.StartDetails, eventUser user.User, authorID, proposalName string) error { 570 var ( 571 token = sd.Params.Token 572 ntfnBit = uint64(www.NotificationEmailRegularProposalVoteStarted) 573 ) 574 575 // Compile user notification list 576 recipients := make(map[uuid.UUID]string, 1024) 577 err := p.userdb.AllUsers(func(u *user.User) { 578 switch { 579 case u.ID.String() == eventUser.ID.String(): 580 // Don't send a notification to the user that sent the request 581 // to start the vote. 582 return 583 case u.ID.String() == authorID: 584 // Don't send the notification to the author. They are sent a 585 // separate notification. 586 return 587 case !u.NotificationIsEnabled(ntfnBit): 588 // User does not have notification bit set 589 return 590 default: 591 // User has notification bit set 592 recipients[u.ID] = u.Email 593 } 594 }) 595 if err != nil { 596 return fmt.Errorf("AllUsers: %v", err) 597 } 598 599 // Email users 600 err = p.mailNtfnVoteStarted(token, proposalName, recipients) 601 if err != nil { 602 return fmt.Errorf("mailNtfnVoteStarted: %v", err) 603 } 604 605 log.Debugf("Vote started ntfn to users sent %v", token) 606 607 return nil 608 } 609 610 func (p *Pi) handleEventVoteStarted(ch chan interface{}) { 611 for msg := range ch { 612 e, ok := msg.(ticketvote.EventStart) 613 if !ok { 614 log.Errorf("handleEventVoteStarted invalid msg: %v", msg) 615 continue 616 } 617 618 for _, v := range e.Starts { 619 // Setup args to prevent goto errors 620 var ( 621 token = v.Params.Token 622 623 pdr *pdv2.Record 624 r rcv1.Record 625 err error 626 627 authorID string 628 proposalName string 629 ) 630 pdr, err = p.recordAbridged(token) 631 if err != nil { 632 goto failed 633 } 634 r = convertRecordToV1(*pdr) 635 authorID = userIDFromMetadata(r.Metadata) 636 proposalName = proposalNameFromFiles(r.Files) 637 638 // Send notification to record author 639 err = p.ntfnVoteStartedToAuthor(v, authorID, proposalName) 640 if err != nil { 641 // Log the error and continue. This error should not prevent 642 // the other notifications from attempting to be sent. 643 log.Errorf("ntfnVoteStartedToAuthor: %v", err) 644 } 645 646 // Send notification to users 647 err = p.ntfnVoteStarted(v, e.User, authorID, proposalName) 648 if err != nil { 649 err = fmt.Errorf("ntfnVoteStarted: %v", err) 650 goto failed 651 } 652 653 // Notifications sent! 654 continue 655 656 failed: 657 log.Errorf("handleVoteStarted %v: %v", token, err) 658 continue 659 } 660 } 661 } 662 663 // recordAbridged returns a proposal record without its index file or any 664 // attachment files. This allows the request to be light weight. 665 func (p *Pi) recordAbridged(token string) (*pdv2.Record, error) { 666 reqs := []pdv2.RecordRequest{ 667 { 668 Token: token, 669 Filenames: []string{ 670 piplugin.FileNameProposalMetadata, 671 tkplugin.FileNameVoteMetadata, 672 }, 673 }, 674 } 675 rs, err := p.politeiad.Records(context.Background(), reqs) 676 if err != nil { 677 return nil, fmt.Errorf("politeiad records: %v", err) 678 } 679 r, ok := rs[token] 680 if !ok { 681 return nil, fmt.Errorf("record not found %v", token) 682 } 683 return &r, nil 684 } 685 686 // proposalNameFromFiles parses the proposal name from the ProposalMetadata file and 687 // returns it. An empty string is returned if a proposal name is not found. 688 func proposalNameFromFiles(files []rcv1.File) string { 689 pm, err := client.ProposalMetadataDecode(files) 690 if err != nil { 691 return "" 692 } 693 return pm.Name 694 } 695 696 // userIDFromMetadata searches for a UserMetadata and parses the user ID from 697 // it if found. An empty string is returned if no UserMetadata is found. 698 func userIDFromMetadata(ms []v1.MetadataStream) string { 699 um, err := client.UserMetadataDecode(ms) 700 if err != nil { 701 return "" 702 } 703 if um == nil { 704 return "" 705 } 706 return um.UserID 707 } 708 709 func convertStateToV1(s pdv2.RecordStateT) rcv1.RecordStateT { 710 switch s { 711 case pdv2.RecordStateUnvetted: 712 return rcv1.RecordStateUnvetted 713 case pdv2.RecordStateVetted: 714 return rcv1.RecordStateVetted 715 } 716 return rcv1.RecordStateInvalid 717 } 718 719 func convertStatusToV1(s pdv2.RecordStatusT) rcv1.RecordStatusT { 720 switch s { 721 case pdv2.RecordStatusUnreviewed: 722 return rcv1.RecordStatusUnreviewed 723 case pdv2.RecordStatusPublic: 724 return rcv1.RecordStatusPublic 725 case pdv2.RecordStatusCensored: 726 return rcv1.RecordStatusCensored 727 case pdv2.RecordStatusArchived: 728 return rcv1.RecordStatusArchived 729 } 730 return rcv1.RecordStatusInvalid 731 } 732 733 func convertFilesToV1(f []pdv2.File) []rcv1.File { 734 files := make([]rcv1.File, 0, len(f)) 735 for _, v := range f { 736 files = append(files, rcv1.File{ 737 Name: v.Name, 738 MIME: v.MIME, 739 Digest: v.Digest, 740 Payload: v.Payload, 741 }) 742 } 743 return files 744 } 745 746 func convertMetadataStreamsToV1(ms []pdv2.MetadataStream) []rcv1.MetadataStream { 747 metadata := make([]rcv1.MetadataStream, 0, len(ms)) 748 for _, v := range ms { 749 metadata = append(metadata, rcv1.MetadataStream{ 750 PluginID: v.PluginID, 751 StreamID: v.StreamID, 752 Payload: v.Payload, 753 }) 754 } 755 return metadata 756 } 757 758 func convertRecordToV1(r pdv2.Record) rcv1.Record { 759 // User fields that are not part of the politeiad record have 760 // been intentionally left blank. These fields must be pulled 761 // from the user database. 762 return rcv1.Record{ 763 State: convertStateToV1(r.State), 764 Status: convertStatusToV1(r.Status), 765 Version: r.Version, 766 Timestamp: r.Timestamp, 767 Username: "", // Intentionally left blank 768 Metadata: convertMetadataStreamsToV1(r.Metadata), 769 Files: convertFilesToV1(r.Files), 770 CensorshipRecord: rcv1.CensorshipRecord{ 771 Token: r.CensorshipRecord.Token, 772 Merkle: r.CensorshipRecord.Merkle, 773 Signature: r.CensorshipRecord.Signature, 774 }, 775 } 776 }