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  }