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 }