github.com/Ptt-official-app/go-bbs@v0.12.0/pttbbs/board.go (about) 1 // Copyright 2020 Pichu Chen, The PTT APP Authors 2 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 7 // http://www.apache.org/licenses/LICENSE-2.0 8 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package pttbbs 16 17 import ( 18 "bytes" 19 "encoding/binary" 20 "fmt" 21 "io" 22 "log" 23 "os" 24 "strings" 25 "time" 26 27 "github.com/Ptt-official-app/go-bbs/filelock" 28 ) 29 30 // 31 // For Current PTT 32 // Please see https://github.com/ptt/pttbbs/blob/master/include/pttstruct.h 33 // boardheader_t 34 // 35 36 type BoardHeader struct { 37 BrdName string 38 title string 39 bm string 40 Brdattr uint32 // uid[.] 41 ChessCountry string 42 VoteLimitPosts uint8 43 VoteLimitLogins uint8 44 BUpdate time.Time 45 PostLimitPosts uint8 46 PostLimitLogins uint8 47 BVote uint8 48 VTime time.Time 49 Level uint32 50 PermReload time.Time 51 52 // Parent class id, first item is start from 1. 53 Gid int32 54 Next []int32 55 FirstChild []int32 56 Parent int32 57 ChildCount int32 58 Nuser int32 59 PostExpire int32 60 EndGamble time.Time 61 PostType string 62 PostTypeF string 63 FastRecommendPause uint8 64 VoteLimitBadPost uint8 65 PostLimitBadPost uint8 66 SRexpire time.Time 67 } 68 69 func (b *BoardHeader) BoardID() string { return b.BrdName } 70 func (b *BoardHeader) SetBoardID(newValue string) { b.BrdName = newValue } 71 72 func (b *BoardHeader) Title() string { return b.title } 73 func (b *BoardHeader) SetTitle(newValue string) { b.title = newValue } 74 75 func (b *BoardHeader) IsClass() bool { return b.IsGroupBoard() } 76 func (b *BoardHeader) ClassID() string { return fmt.Sprintf("%v", b.Gid) } 77 78 func (b *BoardHeader) IsNoCount() bool { return b.Brdattr&0x00000002 != 0 } 79 func (b *BoardHeader) IsGroupBoard() bool { return b.Brdattr&0x00000008 != 0 } // Class 80 func (b *BoardHeader) IsHide() bool { return b.Brdattr&0x00000010 != 0 } // BoardHide board or friend only 81 func (b *BoardHeader) IsPostMask() bool { return b.Brdattr&0x00000020 != 0 } // Has Post or Reading Limition 82 func (b *BoardHeader) IsAnonymous() bool { return b.Brdattr&0x00000040 != 0 } 83 func (b *BoardHeader) IsDefaultAnonymous() bool { return b.Brdattr&0x00000080 != 0 } 84 func (b *BoardHeader) IsNoCredit() bool { return b.Brdattr&0x00000100 != 0 } 85 func (b *BoardHeader) IsVoteBoard() bool { return b.Brdattr&0x00000200 != 0 } 86 func (b *BoardHeader) IsWarnEL() bool { return b.Brdattr&0x00000400 != 0 } // Warning for Remove Board 87 func (b *BoardHeader) IsTop() bool { return b.Brdattr&0x00000800 != 0 } 88 func (b *BoardHeader) IsNoRecommend() bool { return b.Brdattr&0x00001000 != 0 } // Forbiddent Recommend (Push) 89 func (b *BoardHeader) IsAngelAnonymous() bool { return b.Brdattr&0x00002000 != 0 } 90 func (b *BoardHeader) IsBMCount() bool { return b.Brdattr&0x00004000 != 0 } 91 func (b *BoardHeader) IsIsSymbolic() bool { return b.Brdattr&0x00008000 != 0 } // symbolic link to board 92 func (b *BoardHeader) IsNoBoo() bool { return b.Brdattr&0x00010000 != 0 } 93 func (b *BoardHeader) IsRestrictedPost() bool { return b.Brdattr&0x00040000 != 0 } // Board Friend only 94 func (b *BoardHeader) IsGuestPost() bool { return b.Brdattr&0x00080000 != 0 } 95 func (b *BoardHeader) IsCooldown() bool { return b.Brdattr&0x00100000 != 0 } 96 func (b *BoardHeader) IsCPLog() bool { return b.Brdattr&0x00200000 != 0 } 97 func (b *BoardHeader) IsNoFastRecommend() bool { return b.Brdattr&0x00400000 != 0 } 98 func (b *BoardHeader) IsIPLogRecommend() bool { return b.Brdattr&0x00800000 != 0 } 99 func (b *BoardHeader) IsOver18() bool { return b.Brdattr&0x01000000 != 0 } 100 func (b *BoardHeader) IsNoReply() bool { return b.Brdattr&0x02000000 != 0 } 101 func (b *BoardHeader) IsAlignedComment() bool { return b.Brdattr&0x04000000 != 0 } 102 func (b *BoardHeader) IsNoSelfDeletePost() bool { return b.Brdattr&0x08000000 != 0 } 103 func (b *BoardHeader) IsBMMaskContent() bool { return b.Brdattr&0x10000000 != 0 } 104 105 func (b *BoardHeader) GetPostLimitPosts() uint8 { return b.PostLimitPosts } 106 func (b *BoardHeader) GetPostLimitLogins() uint8 { return b.PostLimitLogins } 107 func (b *BoardHeader) GetPostLimitBadPost() uint8 { return b.PostLimitBadPost } 108 109 func (b *BoardHeader) BM() []string { 110 if b.bm == "" { 111 return []string{} 112 } 113 return strings.Split(b.bm, "/") 114 } 115 116 const ( 117 // BoardTitleLength https://github.com/ptt/pttbbs/blob/master/include/pttstruct.h#L165 118 BoardTitleLength = 48 119 ) 120 121 const ( 122 PosOfBoardName = 0 123 PosOfBoardTitle = PosOfBoardName + IDLength + 1 124 PosOfBM = PosOfBoardTitle + BoardTitleLength + 1 125 PosOfBrdAttr = 3 + PosOfBM + IDLength*3 + 3 126 PosOfChessCountry = PosOfBrdAttr + 4 127 PosOfVoteLimitPosts = PosOfChessCountry + 1 128 PosOfVoteLimitLogins = PosOfVoteLimitPosts + 1 129 PosOfBUpdate = 1 + PosOfVoteLimitLogins + 1 130 PosOfPostLimitPosts = PosOfBUpdate + 4 131 PosOfPostLimitLogins = PosOfPostLimitPosts + 1 132 PosOfBVote = 1 + PosOfPostLimitLogins + 1 133 PosOfVTime = PosOfBVote + 1 134 PosOfLevel = PosOfVTime + 4 135 PosOfPermReload = PosOfLevel + 4 136 PosOfGid = PosOfPermReload + 4 137 PosOfNext = PosOfGid + 4 138 PosOfFirstChild = PosOfNext + 4*2 139 PosOfParent = PosOfFirstChild + 4*2 140 PosOfChildCount = PosOfParent + 4 141 PosOfNuser = PosOfChildCount + 4 142 PosOfPostExpire = PosOfNuser + 4 143 PosOfEndGamble = PosOfPostExpire + 4 144 PosOfPostType = PosOfEndGamble + 4 145 PosOfPostTypeF = PosOfPostType + 33 146 PosOfFastRecommendPause = PosOfPostTypeF + 1 147 PosOfVoteLimitBadPost = PosOfFastRecommendPause + 1 148 PosOfPostLimitBadPost = PosOfVoteLimitBadPost + 1 149 PosOfSRExpire = 3 + PosOfPostLimitBadPost + 1 150 ) 151 152 const ( 153 // BoardPostMask https://github.com/ptt/pttbbs/blob/master/include/pttstruct.h#L211 154 BoardPostMask = 0x00000020 155 // BoardGroupBoard https://github.com/ptt/pttbbs/blob/master/include/pttstruct.h#L209 156 BoardGroupBoard = 0x00000008 157 // PermSYSOP https://github.com/ptt/pttbbs/blob/master/include/perm.h#L22 158 PermSYSOP = 000000040000 159 // PermBM https://github.com/ptt/pttbbs/blob/master/include/perm.h#L18 160 PermBM = 000000002000 161 // BoardHide https://github.com/ptt/pttbbs/blob/master/include/pttstruct.h#L210 162 BoardHide = 0x00000010 163 164 BoardHeaderRecordLength = 256 165 ) 166 167 func NewBoardHeader() *BoardHeader { 168 return &BoardHeader{} 169 } 170 171 func OpenBoardHeaderFile(filename string) ([]*BoardHeader, error) { 172 file, err := os.Open(filename) 173 if err != nil { 174 log.Println(err) 175 return nil, err 176 } 177 178 ret := []*BoardHeader{} 179 180 for { 181 hdr := make([]byte, BoardHeaderRecordLength) 182 _, err := file.Read(hdr) 183 // log.Println(len, err) 184 if err == io.EOF { 185 break 186 } 187 188 f, err := UnmarshalBoardHeader(hdr) 189 if err != nil { 190 return nil, err 191 } 192 ret = append(ret, f) 193 // log.Println(f.Filename) 194 195 } 196 197 return ret, nil 198 } 199 200 func AppendBoardHeaderFileRecord(filename string, newBoardHeader *BoardHeader) error { 201 // If the file doesn't exist, create it, or append to the file 202 203 f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 204 if err != nil { 205 return err 206 } 207 defer f.Close() 208 209 err = filelock.Lock(f) 210 if err != nil { 211 // File is lock 212 return err 213 } 214 215 data, err := newBoardHeader.MarshalBinary() 216 if err != nil { 217 return err 218 } 219 220 if _, err := f.Write(data); err != nil { 221 return err 222 } 223 224 filelock.Unlock(f) 225 return nil 226 } 227 228 func RemoveBoardHeaderFileRecord(filename string, index int) error { 229 230 fi, err := os.OpenFile(filename, os.O_RDONLY, 0644) 231 if err != nil { 232 return fmt.Errorf("fi.OpenFile error: %v", err) 233 } 234 defer fi.Close() 235 236 err = filelock.Lock(fi) 237 if err != nil { 238 // File is lock 239 return err 240 } 241 242 fo, err := os.OpenFile(filename, os.O_WRONLY, 0644) 243 if err != nil { 244 return fmt.Errorf("fo.OpenFile error: %v", err) 245 } 246 defer fo.Close() 247 248 _, err = fi.Seek(int64((index+1)*BoardHeaderRecordLength), os.SEEK_SET) 249 if err != nil { 250 return fmt.Errorf("fi.Seek error: %v", err) 251 } 252 _, err = fo.Seek(int64((index)*BoardHeaderRecordLength), os.SEEK_SET) 253 if err != nil { 254 return fmt.Errorf("fo.Seek error: %v", err) 255 } 256 _, err = io.CopyBuffer(fo, fi, make([]byte, 256)) 257 if err != nil { 258 return fmt.Errorf("copy error: %v", err) 259 } 260 log.Println("copy finish") 261 262 size, err := fo.Seek(0, os.SEEK_CUR) 263 if err != nil { 264 return fmt.Errorf("fo.Seek for SEEK_CUR error: %v", err) 265 } 266 267 fo.Truncate(size) 268 filelock.Unlock(fi) 269 return nil 270 271 } 272 273 func UnmarshalBoardHeader(data []byte) (*BoardHeader, error) { 274 ret := BoardHeader{} 275 276 ret.BrdName = big5uaoToUTF8String(bytes.Split(data[PosOfBoardName:PosOfBoardName+IDLength+1], []byte("\x00"))[0]) 277 ret.title = big5uaoToUTF8String(bytes.Split(data[PosOfBoardTitle:PosOfBoardTitle+BoardTitleLength+1], []byte("\x00"))[0]) // Be careful about C-string end char \0 278 ret.bm = string(bytes.Trim(data[PosOfBM:PosOfBM+IDLength*3+3], "\x00")) 279 ret.Brdattr = binary.LittleEndian.Uint32(data[PosOfBrdAttr : PosOfBrdAttr+4]) 280 ret.VoteLimitPosts = uint8(data[PosOfVoteLimitPosts]) 281 ret.VoteLimitLogins = uint8(data[PosOfVoteLimitLogins]) 282 ret.ChessCountry = string(bytes.Trim(data[PosOfChessCountry:PosOfChessCountry+1], "\x00")) 283 bUpdateInt := binary.LittleEndian.Uint32(data[PosOfBUpdate : PosOfBUpdate+4]) 284 ret.BUpdate = time.Unix(int64(bUpdateInt), 0) 285 ret.PostLimitPosts = uint8(data[PosOfPostLimitPosts]) 286 ret.PostLimitLogins = uint8(data[PosOfPostLimitLogins]) 287 ret.BVote = uint8(data[PosOfBVote]) 288 vTime := binary.LittleEndian.Uint32(data[PosOfVTime : PosOfVTime+4]) 289 ret.VTime = time.Unix(int64(vTime), 0) 290 ret.Level = binary.LittleEndian.Uint32(data[PosOfLevel : PosOfLevel+4]) 291 permReload := binary.LittleEndian.Uint32(data[PosOfPermReload : PosOfPermReload+4]) 292 ret.PermReload = time.Unix(int64(permReload), 0) 293 ret.Gid = int32(binary.LittleEndian.Uint32(data[PosOfGid : PosOfGid+4])) 294 295 ret.Next = []int32{int32(binary.LittleEndian.Uint32(data[PosOfNext : PosOfNext+4])), int32(binary.LittleEndian.Uint32(data[PosOfNext+4 : PosOfNext+8]))} 296 ret.FirstChild = []int32{int32(binary.LittleEndian.Uint32(data[PosOfFirstChild : PosOfFirstChild+4])), int32(binary.LittleEndian.Uint32(data[PosOfFirstChild+4 : PosOfFirstChild+8]))} 297 298 ret.Parent = int32(binary.LittleEndian.Uint32(data[PosOfParent : PosOfParent+4])) 299 ret.ChildCount = int32(binary.LittleEndian.Uint32(data[PosOfChildCount : PosOfChildCount+4])) 300 301 ret.Nuser = int32(binary.LittleEndian.Uint32(data[PosOfNuser : PosOfNuser+4])) 302 ret.PostExpire = int32(binary.LittleEndian.Uint32(data[PosOfPostExpire : PosOfPostExpire+4])) 303 endGamble := binary.LittleEndian.Uint32(data[PosOfEndGamble : PosOfEndGamble+4]) 304 ret.EndGamble = time.Unix(int64(endGamble), 0) 305 ret.PostType = big5uaoToUTF8String(bytes.Trim(data[PosOfPostType:PosOfPostType+33], "\x00")) 306 ret.PostTypeF = big5uaoToUTF8String(bytes.Trim(data[PosOfPostTypeF:PosOfPostTypeF+1], "\x00")) 307 308 ret.FastRecommendPause = uint8(data[PosOfFastRecommendPause]) 309 ret.VoteLimitBadPost = uint8(data[PosOfVoteLimitBadPost]) 310 ret.PostLimitBadPost = uint8(data[PosOfPostLimitBadPost]) 311 srExpire := binary.LittleEndian.Uint32(data[PosOfSRExpire : PosOfSRExpire+4]) 312 ret.SRexpire = time.Unix(int64(srExpire), 0) 313 314 return &ret, nil 315 } 316 317 func (b *BoardHeader) MarshalBinary() ([]byte, error) { 318 ret := make([]byte, BoardHeaderRecordLength) 319 320 copy(ret[PosOfBoardName:PosOfBoardName+IDLength+1], utf8ToBig5UAOString(b.BrdName)) 321 copy(ret[PosOfBoardTitle:PosOfBoardTitle+BoardTitleLength+1], utf8ToBig5UAOString(b.title)) 322 copy(ret[PosOfBM:PosOfBM+IDLength*3+3], b.bm) 323 binary.LittleEndian.PutUint32(ret[PosOfBrdAttr:PosOfBrdAttr+4], b.Brdattr) 324 ret[PosOfVoteLimitPosts] = b.VoteLimitPosts 325 ret[PosOfVoteLimitLogins] = b.VoteLimitLogins 326 copy(ret[PosOfChessCountry:PosOfChessCountry+1], b.ChessCountry) 327 binary.LittleEndian.PutUint32(ret[PosOfBUpdate:PosOfBUpdate+4], uint32(b.BUpdate.Unix())) 328 ret[PosOfPostLimitPosts] = b.PostLimitPosts 329 ret[PosOfPostLimitLogins] = b.PostLimitLogins 330 ret[PosOfBVote] = b.BVote 331 binary.LittleEndian.PutUint32(ret[PosOfVTime:PosOfVTime+4], uint32(b.VTime.Unix())) 332 binary.LittleEndian.PutUint32(ret[PosOfLevel:PosOfLevel+4], b.Level) 333 binary.LittleEndian.PutUint32(ret[PosOfPermReload:PosOfPermReload+4], uint32(b.PermReload.Unix())) 334 binary.LittleEndian.PutUint32(ret[PosOfGid:PosOfGid+4], uint32(b.Gid)) 335 336 if len(b.Next) == 2 { 337 binary.LittleEndian.PutUint32(ret[PosOfNext:PosOfNext+4], uint32(b.Next[0])) 338 binary.LittleEndian.PutUint32(ret[PosOfNext+4:PosOfNext+8], uint32(b.Next[1])) 339 } 340 341 if len(b.FirstChild) == 2 { 342 binary.LittleEndian.PutUint32(ret[PosOfFirstChild:PosOfFirstChild+4], uint32(b.FirstChild[0])) 343 binary.LittleEndian.PutUint32(ret[PosOfFirstChild+4:PosOfFirstChild+8], uint32(b.FirstChild[1])) 344 } 345 346 binary.LittleEndian.PutUint32(ret[PosOfParent:PosOfParent+4], uint32(b.Parent)) 347 binary.LittleEndian.PutUint32(ret[PosOfChildCount:PosOfChildCount+4], uint32(b.ChildCount)) 348 349 binary.LittleEndian.PutUint32(ret[PosOfNuser:PosOfNuser+4], uint32(b.Nuser)) 350 binary.LittleEndian.PutUint32(ret[PosOfPostExpire:PosOfPostExpire+4], uint32(b.PostExpire)) 351 binary.LittleEndian.PutUint32(ret[PosOfEndGamble:PosOfEndGamble+4], uint32(b.EndGamble.Unix())) 352 copy(ret[PosOfPostType:PosOfPostType+33], utf8ToBig5UAOString(b.PostType)) 353 copy(ret[PosOfPostTypeF:PosOfPostTypeF+1], utf8ToBig5UAOString(b.PostTypeF)) 354 355 ret[PosOfFastRecommendPause] = b.FastRecommendPause 356 ret[PosOfVoteLimitBadPost] = b.VoteLimitBadPost 357 ret[PosOfPostLimitBadPost] = b.PostLimitBadPost 358 binary.LittleEndian.PutUint32(ret[PosOfSRExpire:PosOfSRExpire+4], uint32(b.SRexpire.Unix())) 359 360 return ret, nil 361 362 }