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  }