github.com/Ptt-official-app/go-bbs@v0.12.0/bbs.go (about)

     1  package bbs
     2  
     3  import (
     4  	"bufio"
     5  	"errors"
     6  	"fmt"
     7  	"log"
     8  	"strings"
     9  	"time"
    10  )
    11  
    12  // UserRecord mapping to `userec` in most system, it records uesr's
    13  // basical data
    14  type UserRecord interface {
    15  	// UserID return user's identification string, and it is userid in
    16  	// mostly bbs system
    17  	UserID() string
    18  	// HashedPassword return user hashed password, it only for debug,
    19  	// If you want to check is user password correct, please use
    20  	// VerifyPassword insteaded.
    21  	HashedPassword() string
    22  	// VerifyPassword will check user's password is OK. it will return null
    23  	// when OK and error when there are something wrong
    24  	VerifyPassword(password string) error
    25  	// Nickname return a string for user's nickname, this string may change
    26  	// depend on user's mood, return empty string if this bbs system do not support
    27  	Nickname() string
    28  	// RealName return a string for user's real name, this string may not be changed
    29  	// return empty string if this bbs system do not support
    30  	RealName() string
    31  	// NumLoginDays return how many days this have been login since account created.
    32  	NumLoginDays() int
    33  	// NumPosts return how many posts this user has posted.
    34  	NumPosts() int
    35  	// Money return the money this user have.
    36  	Money() int
    37  	// LastLogin return last login time of user
    38  	LastLogin() time.Time
    39  	// LastHost return last login host of user, it is IPv4 address usually, but it
    40  	// could be domain name or IPv6 address.
    41  	LastHost() string
    42  	// UserFlag return user setting.
    43  	// uint32, see https://github.com/ptt/pttbbs/blob/master/include/uflags.h
    44  	UserFlag() uint32
    45  }
    46  
    47  // BadPostUserRecord return UserRecord interface which support NumBadPosts
    48  type BadPostUserRecord interface {
    49  	// NumBadPosts return how many bad post this use have
    50  	NumBadPosts() int
    51  }
    52  
    53  // LastCountryUserRecord return UserRecord interface which support LastCountry
    54  type LastCountryUserRecord interface {
    55  	// LastLoginCountry will return the country with this user's last login IP
    56  	LastLoginCountry() string
    57  }
    58  
    59  // MailboxUserRecord return UserRecord interface which support MailboxDescription
    60  type MailboxUserRecord interface {
    61  	// MailboxDescription will return the mailbox description with this user
    62  	MailboxDescription() string
    63  }
    64  
    65  type FavoriteType int
    66  
    67  const (
    68  	FavoriteTypeBoard  FavoriteType = iota // 0
    69  	FavoriteTypeFolder                     // 1
    70  	FavoriteTypeLine                       // 2
    71  
    72  )
    73  
    74  type FavoriteRecord interface {
    75  	Title() string
    76  	Type() FavoriteType
    77  	BoardID() string
    78  
    79  	// Records is FavoriteTypeFolder only.
    80  	Records() []FavoriteRecord
    81  }
    82  
    83  type BoardRecord interface {
    84  	BoardID() string
    85  
    86  	Title() string
    87  
    88  	IsClass() bool
    89  	// ClassID should return the class id to which this board/class belongs.
    90  	ClassID() string
    91  
    92  	BM() []string
    93  }
    94  
    95  type BoardRecordSettings interface {
    96  	IsHide() bool
    97  	IsPostMask() bool
    98  	IsAnonymous() bool
    99  	IsDefaultAnonymous() bool
   100  	IsNoCredit() bool
   101  	IsVoteBoard() bool
   102  	IsWarnEL() bool
   103  	IsTop() bool
   104  	IsNoRecommend() bool
   105  	IsAngelAnonymous() bool
   106  	IsBMCount() bool
   107  	IsNoBoo() bool
   108  	IsRestrictedPost() bool
   109  	IsGuestPost() bool
   110  	IsCooldown() bool
   111  	IsCPLog() bool
   112  	IsNoFastRecommend() bool
   113  	IsIPLogRecommend() bool
   114  	IsOver18() bool
   115  	IsNoReply() bool
   116  	IsAlignedComment() bool
   117  	IsNoSelfDeletePost() bool
   118  	IsBMMaskContent() bool
   119  }
   120  
   121  type BoardRecordInfo interface {
   122  	GetPostLimitPosts() uint8
   123  	GetPostLimitLogins() uint8
   124  	GetPostLimitBadPost() uint8
   125  }
   126  
   127  type ArticleRecord interface {
   128  	Filename() string
   129  	Modified() time.Time
   130  	SetModified(newModified time.Time)
   131  	Recommend() int
   132  	Date() string
   133  	Title() string
   134  	Money() int
   135  	Owner() string
   136  }
   137  
   138  // DB is whole bbs filesystem, including where file store,
   139  // how to connect to local cache ( system V shared memory or etc.)
   140  // how to parse or store it's data to bianry
   141  type DB struct {
   142  	connector Connector
   143  }
   144  
   145  // Driver should implement Connector interface
   146  type Connector interface {
   147  	// Open provides the driver parameter settings, such as BBSHome parameter and SHM parameters.
   148  	Open(dataSourceName string) error
   149  	// GetUserRecordsPath should return user records file path, eg: BBSHome/.PASSWDS
   150  	GetUserRecordsPath() (string, error)
   151  	// ReadUserRecordsFile should return UserRecord list in the file called name
   152  	ReadUserRecordsFile(name string) ([]UserRecord, error)
   153  	// GetUserFavoriteRecordsPath should return the user favorite records file path
   154  	// for specific user, eg: BBSHOME/home/{{u}}/{{userID}}/.fav
   155  	GetUserFavoriteRecordsPath(userID string) (string, error)
   156  	// ReadUserFavoriteRecordsFile should return FavoriteRecord list in the file called name
   157  	ReadUserFavoriteRecordsFile(name string) ([]FavoriteRecord, error)
   158  	// GetBoardRecordsPath should return the board headers file path, eg: BBSHome/.BRD
   159  	GetBoardRecordsPath() (string, error)
   160  	// ReadBoardRecordsFile shoule return BoardRecord list in file, name is the file name
   161  	ReadBoardRecordsFile(name string) ([]BoardRecord, error)
   162  	// GetBoardArticleRecordsPath should return the article records file path, boardID is the board id,
   163  	// eg: BBSHome/boards/{{b}}/{{boardID}}/.DIR
   164  	GetBoardArticleRecordsPath(boardID string) (string, error)
   165  	// GetBoardArticleRecordsPath should return the treasure records file path, boardID is the board id,
   166  	// eg: BBSHome/man/boards/{{b}}/{{boardID}}/{{treasureID}}/.DIR
   167  	GetBoardTreasureRecordsPath(boardID string, treasureID []string) (string, error)
   168  	// ReadArticleRecordsFile returns ArticleRecord list in file, name is the file name
   169  	ReadArticleRecordsFile(name string) ([]ArticleRecord, error)
   170  	// GetBoardArticleFilePath return file path for specific boardID and filename
   171  	GetBoardArticleFilePath(boardID string, filename string) (string, error)
   172  	// GetBoardTreasureFilePath return file path for specific boardID, treasureID and filename
   173  	GetBoardTreasureFilePath(boardID string, treasureID []string, name string) (string, error)
   174  	// ReadBoardArticleFile should returns raw file of specific file name
   175  	ReadBoardArticleFile(name string) ([]byte, error)
   176  }
   177  
   178  // Driver which implement WriteBoardConnector supports modify board record file.
   179  type WriteBoardConnector interface {
   180  
   181  	// NewBoardRecord return BoardRecord object in this driver with arguments
   182  	NewBoardRecord(args map[string]interface{}) (BoardRecord, error)
   183  
   184  	// AddBoardRecordFileRecord given record file name and new record, should append
   185  	// file record in that file.
   186  	AddBoardRecordFileRecord(name string, brd BoardRecord) error
   187  
   188  	// UpdateBoardRecordFileRecord update boardRecord brd on index in record file,
   189  	// index is start with 0
   190  	UpdateBoardRecordFileRecord(name string, index uint, brd BoardRecord) error
   191  
   192  	// ReadBoardRecordFileRecord return boardRecord brd on index in record file.
   193  	ReadBoardRecordFileRecord(name string, index uint) (BoardRecord, error)
   194  
   195  	// RemoveBoardRecordFileRecord remove boardRecord brd on index in record file.
   196  	RemoveBoardRecordFileRecord(name string, index uint) error
   197  }
   198  
   199  // WriteArticleConnector is a connector for writing a article
   200  type WriteArticleConnector interface {
   201  
   202  	// CreateBoardArticleFilename returns available filename for board with boardID
   203  	CreateBoardArticleFilename(boardID string) (filename string, err error)
   204  
   205  	// NewArticleRecord return ArticleRecord object in this driver with arguments
   206  	NewArticleRecord(filename, owner, date, title string) (ArticleRecord, error)
   207  
   208  	// AddArticleRecordFileRecord given record file name and new record, should append
   209  	// file record in that file.
   210  	AddArticleRecordFileRecord(name string, article ArticleRecord) error
   211  
   212  	// UpdateArticleRecordFileRecord will write article in position index of name file
   213  	// position is start with 0.
   214  	UpdateArticleRecordFileRecord(name string, index uint, article ArticleRecord) error
   215  
   216  	// WriteBoardArticleFile will turncate name file and write content into that file.
   217  	WriteBoardArticleFile(name string, content []byte) error
   218  
   219  	// AppendNewLine append content into file
   220  	AppendBoardArticleFile(name string, content []byte) error
   221  }
   222  
   223  // UserArticleConnector is a connector for bbs who support cached user article records
   224  type UserArticleConnector interface {
   225  
   226  	// GetUserArticleRecordsPath should return the file path which user article record stores.
   227  	GetUserArticleRecordsPath(userID string) (string, error)
   228  
   229  	// ReadUserArticleRecordFile should return the article record in file.
   230  	ReadUserArticleRecordFile(name string) ([]UserArticleRecord, error)
   231  
   232  	// WriteUserArticleRecordFile write user article records into file.
   233  	WriteUserArticleRecordFile(name string, records []UserArticleRecord) error
   234  
   235  	// AppendUserArticleRecordFile append user article records into file.
   236  	AppendUserArticleRecordFile(name string, record UserArticleRecord) error
   237  }
   238  
   239  // UserCommentConnector is a connector for bbs to access the cached user
   240  // comment records.
   241  type UserCommentConnector interface {
   242  
   243  	// GetUserCommentRecordsPath should return the file path where storing the
   244  	//  user comment records.
   245  	GetUserCommentRecordsPath(userID string) (string, error)
   246  
   247  	// ReadUserCommentRecordFile should return the use comment records from the
   248  	//  file.
   249  	ReadUserCommentRecordFile(name string) ([]UserCommentRecord, error)
   250  }
   251  
   252  // UserDraftConnector is a connector for bbs which supports modify user
   253  // draft file.
   254  type UserDraftConnector interface {
   255  
   256  	// GetUserDraftPath should return the user's draft file path
   257  	// eg: BBSHome/home/{{u}}/{{userID}}/buf.{{draftID}}
   258  	GetUserDraftPath(userID, draftID string) (string, error)
   259  
   260  	// ReadUserDraft return the user draft from the named file.
   261  	ReadUserDraft(name string) ([]byte, error)
   262  
   263  	// DeleteUserDraft should remove the named file.
   264  	DeleteUserDraft(name string) error
   265  
   266  	// WriteUserDraft should replace user draft from named file and user draft data
   267  	WriteUserDraft(name string, draft []byte) error
   268  }
   269  
   270  var drivers = make(map[string]Connector)
   271  
   272  func Register(drivername string, connector Connector) {
   273  	// TODO: Mutex
   274  	drivers[drivername] = connector
   275  }
   276  
   277  // Open opan a
   278  func Open(drivername string, dataSourceName string) (*DB, error) {
   279  
   280  	c, ok := drivers[drivername]
   281  	if !ok {
   282  		return nil, fmt.Errorf("bbs: drivername: %v not found", drivername)
   283  	}
   284  
   285  	err := c.Open(dataSourceName)
   286  	if err != nil {
   287  		return nil, fmt.Errorf("bbs: drivername: %v open error: %v", drivername, err)
   288  	}
   289  
   290  	return &DB{
   291  		connector: c,
   292  	}, nil
   293  }
   294  
   295  // ReadUserRecords returns the UserRecords
   296  func (db *DB) ReadUserRecords() ([]UserRecord, error) {
   297  
   298  	path, err := db.connector.GetUserRecordsPath()
   299  	if err != nil {
   300  		log.Println("bbs: open file error:", err)
   301  		return nil, err
   302  	}
   303  	log.Println("path:", path)
   304  
   305  	userRecs, err := db.connector.ReadUserRecordsFile(path)
   306  	if err != nil {
   307  		log.Println("bbs: get user rec error:", err)
   308  		return nil, err
   309  	}
   310  	return userRecs, nil
   311  }
   312  
   313  // ReadUserFavoriteRecords returns the FavoriteRecord for specific userID
   314  func (db *DB) ReadUserFavoriteRecords(userID string) ([]FavoriteRecord, error) {
   315  
   316  	path, err := db.connector.GetUserFavoriteRecordsPath(userID)
   317  	if err != nil {
   318  		log.Println("bbs: get user favorite records path error:", err)
   319  		return nil, err
   320  	}
   321  	log.Println("path:", path)
   322  
   323  	recs, err := db.connector.ReadUserFavoriteRecordsFile(path)
   324  	if err != nil {
   325  		log.Println("bbs: read user favorite records error:", err)
   326  		return nil, err
   327  	}
   328  	return recs, nil
   329  
   330  }
   331  
   332  // ReadBoardRecords returns the UserRecords
   333  func (db *DB) ReadBoardRecords() ([]BoardRecord, error) {
   334  
   335  	path, err := db.connector.GetBoardRecordsPath()
   336  	if err != nil {
   337  		log.Println("bbs: open file error:", err)
   338  		return nil, err
   339  	}
   340  	log.Println("path:", path)
   341  
   342  	recs, err := db.connector.ReadBoardRecordsFile(path)
   343  	if err != nil {
   344  		log.Println("bbs: get user rec error:", err)
   345  		return nil, err
   346  	}
   347  	return recs, nil
   348  }
   349  
   350  func (db *DB) ReadBoardArticleRecordsFile(boardID string) ([]ArticleRecord, error) {
   351  
   352  	path, err := db.connector.GetBoardArticleRecordsPath(boardID)
   353  	if err != nil {
   354  		log.Println("bbs: open file error:", err)
   355  		return nil, err
   356  	}
   357  	log.Println("path:", path)
   358  
   359  	recs, err := db.connector.ReadArticleRecordsFile(path)
   360  	if err != nil {
   361  		if strings.Contains(err.Error(), "no such file or directory") {
   362  			return []ArticleRecord{}, nil
   363  		}
   364  		log.Println("bbs: ReadArticleRecordsFile error:", err)
   365  		return nil, err
   366  	}
   367  	return recs, nil
   368  
   369  }
   370  
   371  func (db *DB) ReadBoardTreasureRecordsFile(boardID string, treasureID []string) ([]ArticleRecord, error) {
   372  
   373  	path, err := db.connector.GetBoardTreasureRecordsPath(boardID, treasureID)
   374  	if err != nil {
   375  		log.Println("bbs: open file error:", err)
   376  		return nil, err
   377  	}
   378  	log.Println("path:", path)
   379  
   380  	recs, err := db.connector.ReadArticleRecordsFile(path)
   381  	if err != nil {
   382  		log.Println("bbs: get user rec error:", err)
   383  		return nil, err
   384  	}
   385  	return recs, nil
   386  }
   387  
   388  func (db *DB) ReadBoardArticleFile(boardID string, filename string) ([]byte, error) {
   389  
   390  	path, err := db.connector.GetBoardArticleFilePath(boardID, filename)
   391  	if err != nil {
   392  		log.Println("bbs: open file error:", err)
   393  		return nil, err
   394  	}
   395  	log.Println("path:", path)
   396  
   397  	recs, err := db.connector.ReadBoardArticleFile(path)
   398  	if err != nil {
   399  		log.Println("bbs: get user rec error:", err)
   400  		return nil, err
   401  	}
   402  	return recs, nil
   403  }
   404  
   405  func (db *DB) ReadBoardTreasureFile(boardID string, treasuresID []string, filename string) ([]byte, error) {
   406  
   407  	path, err := db.connector.GetBoardTreasureFilePath(boardID, treasuresID, filename)
   408  	if err != nil {
   409  		log.Println("bbs: open file error:", err)
   410  		return nil, err
   411  	}
   412  	log.Println("path:", path)
   413  
   414  	recs, err := db.connector.ReadBoardArticleFile(path)
   415  	if err != nil {
   416  		log.Println("bbs: get user rec error:", err)
   417  		return nil, err
   418  	}
   419  	return recs, nil
   420  }
   421  
   422  func (db *DB) NewBoardRecord(args map[string]interface{}) (BoardRecord, error) {
   423  	return db.connector.(WriteBoardConnector).NewBoardRecord(args)
   424  }
   425  
   426  func (db *DB) AddBoardRecord(brd BoardRecord) error {
   427  
   428  	path, err := db.connector.GetBoardRecordsPath()
   429  	if err != nil {
   430  		log.Println("bbs: open file error:", err)
   431  		return err
   432  	}
   433  	log.Println("path:", path)
   434  
   435  	err = db.connector.(WriteBoardConnector).AddBoardRecordFileRecord(path, brd)
   436  	if err != nil {
   437  		log.Println("bbs: AddBoardRecordFileRecord error:", err)
   438  		return err
   439  	}
   440  	return nil
   441  }
   442  
   443  // UpdateBoardRecordFileRecord update boardRecord brd on index in record file,
   444  // index is start with 0
   445  func (db *DB) UpdateBoardRecord(index uint, brd *BoardRecord) error {
   446  	return fmt.Errorf("not implement")
   447  }
   448  
   449  // ReadBoardRecordFileRecord return boardRecord brd on index in record file.
   450  func (db *DB) ReadBoardRecord(index uint) (*BoardRecord, error) {
   451  	return nil, fmt.Errorf("not implement")
   452  }
   453  
   454  // RemoveBoardRecordFileRecord remove boardRecord brd on index in record file.
   455  func (db *DB) RemoveBoardRecord(index uint) error {
   456  	return fmt.Errorf("not implement")
   457  }
   458  
   459  // CreateArticleRecord returns new ArticleRecord with new filename in boardID and owner, date and title.
   460  // This method will find a usable filename in board and occupy it.
   461  func (db *DB) CreateArticleRecord(boardID, owner, date, title string) (ArticleRecord, error) {
   462  	filename, err := db.connector.(WriteArticleConnector).CreateBoardArticleFilename(boardID)
   463  	if err != nil {
   464  		return nil, err
   465  	}
   466  	return db.connector.(WriteArticleConnector).NewArticleRecord(filename, owner, date, title)
   467  }
   468  
   469  // AddArticleRecordFileRecord append article ArticleRecord to boardID
   470  func (db *DB) AddArticleRecordFileRecord(boardID string, article ArticleRecord) error {
   471  
   472  	path, err := db.connector.GetBoardArticleRecordsPath(boardID)
   473  	if err != nil {
   474  		log.Println("bbs: open file error:", err)
   475  		return err
   476  	}
   477  	log.Println("path:", path)
   478  
   479  	return db.connector.(WriteArticleConnector).AddArticleRecordFileRecord(path, article)
   480  }
   481  
   482  // WriteBoardArticleFile writes content into filename in boardID
   483  func (db *DB) WriteBoardArticleFile(boardID, filename string, content []byte) error {
   484  
   485  	_, ok := db.connector.(WriteArticleConnector)
   486  	if !ok {
   487  		return fmt.Errorf("bbs: connector don't support WriteArticleConnector")
   488  	}
   489  
   490  	path, err := db.connector.GetBoardArticleFilePath(boardID, filename)
   491  	if err != nil {
   492  		log.Println("bbs: open file error:", err)
   493  		return err
   494  	}
   495  	log.Println("path:", path)
   496  
   497  	err = db.connector.(WriteArticleConnector).WriteBoardArticleFile(path, content)
   498  	if err != nil {
   499  		log.Println("bbs: write board article file error:", err)
   500  		return err
   501  	}
   502  	return nil
   503  }
   504  
   505  func (db *DB) AppendBoardArticleFile(boardID string, filename string, content []byte) error {
   506  	path, err := db.connector.GetBoardArticleFilePath(boardID, filename)
   507  	if err != nil {
   508  		return err
   509  	}
   510  	c, ok := db.connector.(WriteArticleConnector)
   511  	if !ok {
   512  		return fmt.Errorf("bbs: connector don't support WriteArticleConnector")
   513  	}
   514  	err = c.AppendBoardArticleFile(path, content)
   515  	return err
   516  }
   517  
   518  // GetUserArticleRecordFile returns aritcle file which user posted.
   519  func (db *DB) GetUserArticleRecordFile(userID string) ([]UserArticleRecord, error) {
   520  
   521  	recs := []UserArticleRecord{}
   522  	uac, ok := db.connector.(UserArticleConnector)
   523  	if ok {
   524  
   525  		path, err := uac.GetUserArticleRecordsPath(userID)
   526  		if err != nil {
   527  			log.Println("bbs: open file error:", err)
   528  			return nil, err
   529  		}
   530  		log.Println("path:", path)
   531  
   532  		recs, err = uac.ReadUserArticleRecordFile(path)
   533  		if err != nil {
   534  			log.Println("bbs: ReadUserArticleRecordFile error:", err)
   535  			return nil, err
   536  		}
   537  		if len(recs) != 0 {
   538  			return recs, nil
   539  		}
   540  
   541  	}
   542  
   543  	boardRecords, err := db.ReadBoardRecords()
   544  	if err != nil {
   545  		log.Println("bbs: ReadBoardRecords error:", err)
   546  		return nil, err
   547  	}
   548  
   549  	shouldSkip := func(boardID string) bool {
   550  		if boardID == "ALLPOST" {
   551  			return true
   552  		}
   553  		return false
   554  	}
   555  
   556  	for _, r := range boardRecords {
   557  		if shouldSkip(r.BoardID()) {
   558  			continue
   559  		}
   560  
   561  		ars, err := db.ReadBoardArticleRecordsFile(r.BoardID())
   562  		if err != nil {
   563  			log.Println("bbs: ReadBoardArticleRecordsFile error:", err)
   564  			return nil, err
   565  		}
   566  		for _, ar := range ars {
   567  			if ar.Owner() == userID {
   568  				log.Println("board: ", r.BoardID(), len(recs))
   569  				r := userArticleRecord{
   570  					"board_id":   r.BoardID(),
   571  					"title":      ar.Title(),
   572  					"owner":      ar.Owner(),
   573  					"article_id": ar.Filename(),
   574  				}
   575  				recs = append(recs, r)
   576  			}
   577  		}
   578  	}
   579  
   580  	return recs, nil
   581  }
   582  
   583  // GetUserCommentRecordFile returns the comment records of the specific user
   584  // from all boards and all articles.
   585  func (db *DB) GetUserCommentRecordFile(userID string) ([]UserCommentRecord, error) {
   586  
   587  	recs := []UserCommentRecord{}
   588  	ucc, ok := db.connector.(UserCommentConnector)
   589  	if ok {
   590  		path, err := ucc.GetUserCommentRecordsPath(userID)
   591  		if err != nil {
   592  			log.Println("bbs: open file error:", err)
   593  			return nil, err
   594  		}
   595  		log.Println("path:", path)
   596  
   597  		recs, err = ucc.ReadUserCommentRecordFile(path)
   598  		if err != nil {
   599  			log.Println("bbs: ReadUserCommentRecordFile error:", err)
   600  			return nil, err
   601  		}
   602  
   603  		if len(recs) != 0 {
   604  			return recs, nil
   605  		}
   606  	}
   607  
   608  	// TODO: Implement a method to get the board records with the filter.
   609  	// For example: db.ReadBoardRecordsFilter(skipBoardID []string)
   610  	boardRecords, err := db.ReadBoardRecords()
   611  	if err != nil {
   612  		log.Println("bbs: ReadBoardRecords error:", err)
   613  		return nil, err
   614  	}
   615  
   616  	shouldSkip := func(boardID string) bool {
   617  		if boardID == "ALLPOST" {
   618  			return true
   619  		}
   620  		return false
   621  	}
   622  
   623  	for _, r := range boardRecords {
   624  		if shouldSkip(r.BoardID()) {
   625  			continue
   626  		}
   627  
   628  		ucr, err := db.GetBoardUserCommentRecord(r.BoardID(), userID)
   629  		if err != nil {
   630  			log.Println("bbs: GetUserCommentRecordOfBoard error:", err)
   631  			return nil, err
   632  		}
   633  		recs = append(recs, ucr...)
   634  	}
   635  
   636  	return recs, nil
   637  }
   638  
   639  // GetBoardUserCommentRecord returns the comment records of the user from the
   640  // specific board.
   641  func (db *DB) GetBoardUserCommentRecord(boardID, userID string) (recs []UserCommentRecord, err error) {
   642  
   643  	ars, err := db.ReadBoardArticleRecordsFile(boardID)
   644  	if err != nil {
   645  		log.Println("bbs: ReadBoardArticleRecordsFile error:", err)
   646  		return nil, err
   647  	}
   648  
   649  	for _, ar := range ars {
   650  		crs, err := db.GetBoardArticleCommentRecords(boardID, ar)
   651  		if err != nil {
   652  			log.Println("bbs: GetBoardArticleCommentRecords error:", err)
   653  			return nil, err
   654  		}
   655  		for _, cr := range crs {
   656  			if userID != cr.Owner() {
   657  				continue
   658  			}
   659  			recs = append(recs, cr)
   660  		}
   661  	}
   662  
   663  	return recs, nil
   664  }
   665  
   666  // GetBoardArticleCommentRecords returns the comment records of the specific
   667  // article.
   668  func (db *DB) GetBoardArticleCommentRecords(boardID string, ar ArticleRecord) (crs []UserCommentRecord, err error) {
   669  
   670  	content, err := db.ReadBoardArticleFile(boardID, ar.Filename())
   671  	if err != nil {
   672  		log.Println("bbs: ReadBoardArticleFile error:", err)
   673  		return nil, err
   674  	}
   675  
   676  	floorCnt := uint32(1)
   677  	scanner := bufio.NewScanner(strings.NewReader(string(content)))
   678  	for scanner.Scan() {
   679  		l := FilterStringANSI(scanner.Text())
   680  		cr, err := NewUserCommentRecord(floorCnt, l, boardID, ar)
   681  		if err != nil {
   682  			// skip non-comment line
   683  			if errors.Is(err, ErrNotUserComment) {
   684  				continue
   685  			}
   686  			log.Println("bbs: NewUserCommentRecord error:", err)
   687  			return nil, err
   688  		}
   689  		crs = append(crs, cr)
   690  		floorCnt++
   691  	}
   692  
   693  	if len(crs) > 1 {
   694  		log.Println(content)
   695  	}
   696  
   697  	return crs, nil
   698  }
   699  
   700  func (db *DB) GetUserDrafts(userID, draftID string) (UserDraft, error) {
   701  
   702  	path, err := db.connector.(UserDraftConnector).GetUserDraftPath(userID, draftID)
   703  	if err != nil {
   704  		log.Println("bbs: GetUserDraftPath error:", err)
   705  		return nil, err
   706  	}
   707  	log.Println("path:", path)
   708  
   709  	raw, err := db.connector.(UserDraftConnector).ReadUserDraft(path)
   710  	if err != nil {
   711  		return nil, err
   712  	}
   713  
   714  	return NewUserDraft(raw), nil
   715  }
   716  
   717  func (db *DB) DeleteUserDraft(userID, draftID string) error {
   718  
   719  	path, err := db.connector.(UserDraftConnector).GetUserDraftPath(userID, draftID)
   720  	if err != nil {
   721  		log.Println("bbs: GetUserDraftPath error:", err)
   722  		return err
   723  	}
   724  	log.Println("path:", path)
   725  
   726  	return db.connector.(UserDraftConnector).DeleteUserDraft(path)
   727  }
   728  
   729  func (db *DB) WriteUserDraft(userID, draftID string, draftContent UserDraft) error {
   730  	path, err := db.connector.(UserDraftConnector).GetUserDraftPath(userID, draftID)
   731  	if err != nil {
   732  		log.Println("bbs: GetUserDraftPath error:", err)
   733  		return err
   734  	}
   735  
   736  	log.Println("path:", path)
   737  	return db.connector.(UserDraftConnector).WriteUserDraft(path, draftContent.Raw())
   738  }