forked from Ptt-official-app/go-bbs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathbbs.go
461 lines (385 loc) · 13.3 KB
/
bbs.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
package bbs
import (
"fmt"
"log"
"strings"
"time"
)
// UserRecord mapping to `userec` in most system, it records uesr's
// basical data
type UserRecord interface {
// UserID return user's identification string, and it is userid in
// mostly bbs system
UserID() string
// HashedPassword return user hashed password, it only for debug,
// If you want to check is user password correct, please use
// VerifyPassword insteaded.
HashedPassword() string
// VerifyPassword will check user's password is OK. it will return null
// when OK and error when there are something wrong
VerifyPassword(password string) error
// Nickname return a string for user's nickname, this string may change
// depend on user's mood, return empty string if this bbs system do not support
Nickname() string
// RealName return a string for user's real name, this string may not be changed
// return empty string if this bbs system do not support
RealName() string
// NumLoginDays return how many days this have been login since account created.
NumLoginDays() int
// NumPosts return how many posts this user has posted.
NumPosts() int
// Money return the money this user have.
Money() int
// LastLogin return last login time of user
LastLogin() time.Time
// LastHost return last login host of user, it is IPv4 address usually, but it
// could be domain name or IPv6 address.
LastHost() string
}
// BadPostUserRecord return UserRecord interface which support NumBadPosts
type BadPostUserRecord interface {
// NumBadPosts return how many bad post this use have
NumBadPosts() int
}
// LastCountryUserRecord return UserRecord interface which support LastCountry
type LastCountryUserRecord interface {
// LastLoginCountry will return the country with this user's last login IP
LastLoginCountry() string
}
// MailboxUserRecord return UserRecord interface which support MailboxDescription
type MailboxUserRecord interface {
// MailboxDescription will return the mailbox description with this user
MailboxDescription() string
}
type FavoriteType int
const (
FavoriteTypeBoard FavoriteType = iota // 0
FavoriteTypeFolder // 1
FavoriteTypeLine // 2
)
type FavoriteRecord interface {
Title() string
Type() FavoriteType
BoardID() string
// Records is FavoriteTypeFolder only.
Records() []FavoriteRecord
}
type BoardRecord interface {
BoardID() string
Title() string
IsClass() bool
// ClassID should return the class id to which this board/class belongs.
ClassID() string
BM() []string
}
type BoardRecordSettings interface {
IsHide() bool
IsPostMask() bool
IsAnonymous() bool
IsDefaultAnonymous() bool
IsNoCredit() bool
IsVoteBoard() bool
IsWarnEL() bool
IsTop() bool
IsNoRecommend() bool
IsAngelAnonymous() bool
IsBMCount() bool
IsNoBoo() bool
IsRestrictedPost() bool
IsGuestPost() bool
IsCooldown() bool
IsCPLog() bool
IsNoFastRecommend() bool
IsIPLogRecommend() bool
IsOver18() bool
IsNoReply() bool
IsAlignedComment() bool
IsNoSelfDeletePost() bool
IsBMMaskContent() bool
}
type BoardRecordInfo interface {
GetPostLimitPosts() uint8
GetPostLimitLogins() uint8
GetPostLimitBadPost() uint8
}
type ArticleRecord interface {
Filename() string
Modified() time.Time
Recommend() int
Date() string
Title() string
Money() int
Owner() string
}
// DB is whole bbs filesystem, including where file store,
// how to connect to local cache ( system V shared memory or etc.)
// how to parse or store it's data to bianry
type DB struct {
connector Connector
}
// Driver should implement Connector interface
type Connector interface {
// Open provides the driver parameter settings, such as BBSHome parameter and SHM parameters.
Open(dataSourceName string) error
// GetUserRecordsPath should return user records file path, eg: BBSHome/.PASSWDS
GetUserRecordsPath() (string, error)
// ReadUserRecordsFile should return UserRecord list in the file called name
ReadUserRecordsFile(name string) ([]UserRecord, error)
// GetUserFavoriteRecordsPath should return the user favorite records file path
// for specific user, eg: BBSHOME/home/{{u}}/{{userID}}/.fav
GetUserFavoriteRecordsPath(userID string) (string, error)
// ReadUserFavoriteRecordsFile should return FavoriteRecord list in the file called name
ReadUserFavoriteRecordsFile(name string) ([]FavoriteRecord, error)
// GetBoardRecordsPath should return the board headers file path, eg: BBSHome/.BRD
GetBoardRecordsPath() (string, error)
// ReadBoardRecordsFile shoule return BoardRecord list in file, name is the file name
ReadBoardRecordsFile(name string) ([]BoardRecord, error)
// GetBoardArticleRecordsPath should return the article records file path, boardID is the board id,
// eg: BBSHome/boards/{{b}}/{{boardID}}/.DIR
GetBoardArticleRecordsPath(boardID string) (string, error)
// GetBoardArticleRecordsPath should return the treasure records file path, boardID is the board id,
// eg: BBSHome/man/boards/{{b}}/{{boardID}}/{{treasureID}}/.DIR
GetBoardTreasureRecordsPath(boardID string, treasureID []string) (string, error)
// ReadArticleRecordsFile returns ArticleRecord list in file, name is the file name
ReadArticleRecordsFile(name string) ([]ArticleRecord, error)
// GetBoardArticleFilePath return file path for specific boardID and filename
GetBoardArticleFilePath(boardID string, filename string) (string, error)
// GetBoardTreasureFilePath return file path for specific boardID, treasureID and filename
GetBoardTreasureFilePath(boardID string, treasureID []string, name string) (string, error)
// ReadBoardArticleFile should returns raw file of specific file name
ReadBoardArticleFile(name string) ([]byte, error)
}
// Driver which implement WriteBoardConnector supports modify board record file.
type WriteBoardConnector interface {
// NewBoardRecord return BoardRecord object in this driver with arugments
NewBoardRecord(args map[string]interface{}) (BoardRecord, error)
// AddBoardRecordFileRecord given record file name and new record, should append
// file record in that file.
AddBoardRecordFileRecord(name string, brd BoardRecord) error
// UpdateBoardRecordFileRecord update boardRecord brd on index in record file,
// index is start with 0
UpdateBoardRecordFileRecord(name string, index uint, brd BoardRecord) error
// ReadBoardRecordFileRecord return boardRecord brd on index in record file.
ReadBoardRecordFileRecord(name string, index uint) (BoardRecord, error)
// RemoveBoardRecordFileRecord remove boardRecord brd on index in record file.
RemoveBoardRecordFileRecord(name string, index uint) error
}
// UserArticleConnector is a connector for bbs who support cached user article records
type UserArticleConnector interface {
// GetUserArticleRecordsPath should return the file path which user article record stores.
GetUserArticleRecordsPath(userID string) (string, error)
// ReadUserArticleRecordFile should return the article record in file.
ReadUserArticleRecordFile(name string) ([]UserArticleRecord, error)
// WriteUserArticleRecordFile write user article records into file.
WriteUserArticleRecordFile(name string, records []UserArticleRecord) error
// AppendUserArticleRecordFile append user article records into file.
AppendUserArticleRecordFile(name string, record UserArticleRecord) error
}
var drivers = make(map[string]Connector)
func Register(drivername string, connector Connector) {
// TODO: Mutex
drivers[drivername] = connector
}
// Open opan a
func Open(drivername string, dataSourceName string) (*DB, error) {
c, ok := drivers[drivername]
if !ok {
return nil, fmt.Errorf("bbs: drivername: %v not found", drivername)
}
err := c.Open(dataSourceName)
if err != nil {
return nil, fmt.Errorf("bbs: drivername: %v open error: %v", drivername, err)
}
return &DB{
connector: c,
}, nil
}
// ReadUserRecords returns the UserRecords
func (db *DB) ReadUserRecords() ([]UserRecord, error) {
path, err := db.connector.GetUserRecordsPath()
if err != nil {
log.Println("bbs: open file error:", err)
return nil, err
}
log.Println("path:", path)
userRecs, err := db.connector.ReadUserRecordsFile(path)
if err != nil {
log.Println("bbs: get user rec error:", err)
return nil, err
}
return userRecs, nil
}
// ReadUserFavoriteRecords returns the FavoriteRecord for specific userID
func (db *DB) ReadUserFavoriteRecords(userID string) ([]FavoriteRecord, error) {
path, err := db.connector.GetUserFavoriteRecordsPath(userID)
if err != nil {
log.Println("bbs: get user favorite records path error:", err)
return nil, err
}
log.Println("path:", path)
recs, err := db.connector.ReadUserFavoriteRecordsFile(path)
if err != nil {
log.Println("bbs: read user favorite records error:", err)
return nil, err
}
return recs, nil
}
// ReadBoardRecords returns the UserRecords
func (db *DB) ReadBoardRecords() ([]BoardRecord, error) {
path, err := db.connector.GetBoardRecordsPath()
if err != nil {
log.Println("bbs: open file error:", err)
return nil, err
}
log.Println("path:", path)
recs, err := db.connector.ReadBoardRecordsFile(path)
if err != nil {
log.Println("bbs: get user rec error:", err)
return nil, err
}
return recs, nil
}
func (db *DB) ReadBoardArticleRecordsFile(boardID string) ([]ArticleRecord, error) {
path, err := db.connector.GetBoardArticleRecordsPath(boardID)
if err != nil {
log.Println("bbs: open file error:", err)
return nil, err
}
log.Println("path:", path)
recs, err := db.connector.ReadArticleRecordsFile(path)
if err != nil {
if strings.Contains(err.Error(), "no such file or directory") {
return []ArticleRecord{}, nil
}
log.Println("bbs: ReadArticleRecordsFile error:", err)
return nil, err
}
return recs, nil
}
func (db *DB) ReadBoardTreasureRecordsFile(boardID string, treasureID []string) ([]ArticleRecord, error) {
path, err := db.connector.GetBoardTreasureRecordsPath(boardID, treasureID)
if err != nil {
log.Println("bbs: open file error:", err)
return nil, err
}
log.Println("path:", path)
recs, err := db.connector.ReadArticleRecordsFile(path)
if err != nil {
log.Println("bbs: get user rec error:", err)
return nil, err
}
return recs, nil
}
func (db *DB) ReadBoardArticleFile(boardID string, filename string) ([]byte, error) {
path, err := db.connector.GetBoardArticleFilePath(boardID, filename)
if err != nil {
log.Println("bbs: open file error:", err)
return nil, err
}
log.Println("path:", path)
recs, err := db.connector.ReadBoardArticleFile(path)
if err != nil {
log.Println("bbs: get user rec error:", err)
return nil, err
}
return recs, nil
}
func (db *DB) ReadBoardTreasureFile(boardID string, treasuresID []string, filename string) ([]byte, error) {
path, err := db.connector.GetBoardTreasureFilePath(boardID, treasuresID, filename)
if err != nil {
log.Println("bbs: open file error:", err)
return nil, err
}
log.Println("path:", path)
recs, err := db.connector.ReadBoardArticleFile(path)
if err != nil {
log.Println("bbs: get user rec error:", err)
return nil, err
}
return recs, nil
}
func (db *DB) NewBoardRecord(args map[string]interface{}) (BoardRecord, error) {
return db.connector.(WriteBoardConnector).NewBoardRecord(args)
}
func (db *DB) AddBoardRecord(brd BoardRecord) error {
path, err := db.connector.GetBoardRecordsPath()
if err != nil {
log.Println("bbs: open file error:", err)
return err
}
log.Println("path:", path)
err = db.connector.(WriteBoardConnector).AddBoardRecordFileRecord(path, brd)
if err != nil {
log.Println("bbs: AddBoardRecordFileRecord error:", err)
return err
}
return nil
}
// UpdateBoardRecordFileRecord update boardRecord brd on index in record file,
// index is start with 0
func (db *DB) UpdateBoardRecord(index uint, brd *BoardRecord) error {
return fmt.Errorf("not implement")
}
// ReadBoardRecordFileRecord return boardRecord brd on index in record file.
func (db *DB) ReadBoardRecord(index uint) (*BoardRecord, error) {
return nil, fmt.Errorf("not implement")
}
// RemoveBoardRecordFileRecord remove boardRecord brd on index in record file.
func (db *DB) RemoveBoardRecord(index uint) error {
return fmt.Errorf("not implement")
}
// GetUserArticleRecordFile returns aritcle file which user posted.
func (db *DB) GetUserArticleRecordFile(userID string) ([]UserArticleRecord, error) {
recs := []UserArticleRecord{}
uac, ok := db.connector.(UserArticleConnector)
if ok {
path, err := uac.GetUserArticleRecordsPath(userID)
if err != nil {
log.Println("bbs: open file error:", err)
return nil, err
}
log.Println("path:", path)
recs, err = uac.ReadUserArticleRecordFile(path)
if err != nil {
log.Println("bbs: ReadUserArticleRecordFile error:", err)
return nil, err
}
if len(recs) != 0 {
return recs, nil
}
}
boardRecords, err := db.ReadBoardRecords()
if err != nil {
log.Println("bbs: ReadBoardRecords error:", err)
return nil, err
}
shouldSkip := func(boardID string) bool {
if boardID == "ALLPOST" {
return true
}
return false
}
for _, r := range boardRecords {
if shouldSkip(r.BoardID()) {
continue
}
ars, err := db.ReadBoardArticleRecordsFile(r.BoardID())
if err != nil {
log.Println("bbs: ReadBoardArticleRecordsFile error:", err)
return nil, err
}
for _, ar := range ars {
if ar.Owner() == userID {
log.Println("board: ", r.BoardID(), len(recs))
r := userArticleRecord{
"board_id": r.BoardID(),
"title": ar.Title(),
"owner": ar.Owner(),
"article_id": ar.Filename(),
}
recs = append(recs, r)
}
}
}
return recs, nil
}