diff options
| author | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2024-07-09 21:36:27 -0700 |
|---|---|---|
| committer | Jeff Halter <868228+jhalter@users.noreply.github.com> | 2024-07-09 21:42:05 -0700 |
| commit | d9bc63a10d0978d9a5222cf7be74044e55f409b7 (patch) | |
| tree | 1797c9593c279bf1334ed8285de8054a92a28dcb /internal | |
| parent | 8fa166777cbcd92e871e937d9557f0f1a732c04d (diff) | |
Extensive refactor and clean up
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/mobius/ban.go | 76 | ||||
| -rw-r--r-- | internal/mobius/config.go | 30 | ||||
| -rw-r--r-- | internal/mobius/news.go | 87 | ||||
| -rw-r--r-- | internal/mobius/threaded_news.go | 251 |
4 files changed, 444 insertions, 0 deletions
diff --git a/internal/mobius/ban.go b/internal/mobius/ban.go new file mode 100644 index 0000000..f78e3f3 --- /dev/null +++ b/internal/mobius/ban.go @@ -0,0 +1,76 @@ +package mobius + +import ( + "gopkg.in/yaml.v3" + "os" + "path/filepath" + "sync" + "time" +) + +type BanFile struct { + banList map[string]*time.Time + filePath string + + sync.Mutex +} + +func NewBanFile(path string) (*BanFile, error) { + bf := &BanFile{ + filePath: path, + banList: make(map[string]*time.Time), + } + + err := bf.Load() + + return bf, err +} + +func (bf *BanFile) Load() error { + bf.Lock() + defer bf.Unlock() + + bf.banList = make(map[string]*time.Time) + + fh, err := os.Open(bf.filePath) + if err != nil { + if os.IsNotExist(err) { + return nil + } + return err + } + defer fh.Close() + + decoder := yaml.NewDecoder(fh) + err = decoder.Decode(&bf.banList) + if err != nil { + return err + } + + return nil +} + +func (bf *BanFile) Add(ip string, until *time.Time) error { + bf.Lock() + defer bf.Unlock() + + bf.banList[ip] = until + + out, err := yaml.Marshal(bf.banList) + if err != nil { + return err + } + + return os.WriteFile(filepath.Join(bf.filePath), out, 0644) +} + +func (bf *BanFile) IsBanned(ip string) (bool, *time.Time) { + bf.Lock() + defer bf.Unlock() + + if until, ok := bf.banList[ip]; ok { + return true, until + } + + return false, nil +} diff --git a/internal/mobius/config.go b/internal/mobius/config.go new file mode 100644 index 0000000..e985dd7 --- /dev/null +++ b/internal/mobius/config.go @@ -0,0 +1,30 @@ +package mobius + +import ( + "github.com/go-playground/validator/v10" + "github.com/jhalter/mobius/hotline" + "gopkg.in/yaml.v3" + "log" + "os" +) + +func LoadConfig(path string) (*hotline.Config, error) { + var config hotline.Config + + yamlFile, err := os.ReadFile(path) + if err != nil { + return nil, err + } + err = yaml.Unmarshal(yamlFile, &config) + if err != nil { + log.Fatalf("Unmarshal: %v", err) + } + + validate := validator.New() + err = validate.Struct(config) + if err != nil { + return nil, err + } + + return &config, nil +} diff --git a/internal/mobius/news.go b/internal/mobius/news.go new file mode 100644 index 0000000..51e212d --- /dev/null +++ b/internal/mobius/news.go @@ -0,0 +1,87 @@ +package mobius + +import ( + "fmt" + "io" + "os" + "slices" + "sync" +) + +type FlatNews struct { + mu sync.Mutex + + data []byte + filePath string + + readOffset int // Internal offset to track read progress +} + +func NewFlatNews(path string) (*FlatNews, error) { + data, err := os.ReadFile(path) + if err != nil { + return &FlatNews{}, err + } + + return &FlatNews{ + data: data, + filePath: path, + }, nil +} + +func (f *FlatNews) Reload() error { + f.mu.Lock() + defer f.mu.Unlock() + + data, err := os.ReadFile(f.filePath) + if err != nil { + return err + } + f.data = data + + return nil +} + +// It returns the number of bytes read and any error encountered. +func (f *FlatNews) Read(p []byte) (int, error) { + f.mu.Lock() + defer f.mu.Unlock() + + if f.readOffset >= len(f.data) { + return 0, io.EOF // All bytes have been read + } + + n := copy(p, f.data[f.readOffset:]) + + f.readOffset += n + + return n, nil +} + +// Write implements io.Writer for flat news. +// p is guaranteed to contain the full data of a news post. +func (f *FlatNews) Write(p []byte) (int, error) { + f.mu.Lock() + defer f.mu.Unlock() + + f.data = slices.Concat(p, f.data) + + tempFilePath := f.filePath + ".tmp" + + if err := os.WriteFile(tempFilePath, f.data, 0644); err != nil { + return 0, fmt.Errorf("write to temporary file: %v", err) + } + + // Atomically rename the temporary file to the final file path. + if err := os.Rename(tempFilePath, f.filePath); err != nil { + return 0, fmt.Errorf("rename temporary file to final file: %v", err) + } + + return len(p), os.WriteFile(f.filePath, f.data, 0644) +} + +func (f *FlatNews) Seek(offset int64, _ int) (int64, error) { + f.readOffset = int(offset) + + return 0, nil +} diff --git a/internal/mobius/threaded_news.go b/internal/mobius/threaded_news.go new file mode 100644 index 0000000..bae6779 --- /dev/null +++ b/internal/mobius/threaded_news.go @@ -0,0 +1,251 @@ +package mobius + +import ( + "cmp" + "encoding/binary" + "fmt" + "github.com/jhalter/mobius/hotline" + "gopkg.in/yaml.v3" + "os" + "slices" + "sort" + "sync" +) + +type ThreadedNewsYAML struct { + ThreadedNews hotline.ThreadedNews + + filePath string + + mu sync.Mutex +} + +func NewThreadedNewsYAML(filePath string) (*ThreadedNewsYAML, error) { + tn := &ThreadedNewsYAML{filePath: filePath} + + err := tn.Load() + + return tn, err +} + +func (n *ThreadedNewsYAML) CreateGrouping(newsPath []string, name string, t [2]byte) error { + n.mu.Lock() + defer n.mu.Unlock() + + cats := n.getCatByPath(newsPath) + cats[name] = hotline.NewsCategoryListData15{ + Name: name, + Type: t, + Articles: map[uint32]*hotline.NewsArtData{}, + SubCats: make(map[string]hotline.NewsCategoryListData15), + } + + return n.writeFile() +} + +func (n *ThreadedNewsYAML) NewsItem(newsPath []string) hotline.NewsCategoryListData15 { + n.mu.Lock() + defer n.mu.Unlock() + + cats := n.ThreadedNews.Categories + delName := newsPath[len(newsPath)-1] + if len(newsPath) > 1 { + for _, fp := range newsPath[0 : len(newsPath)-1] { + cats = cats[fp].SubCats + } + } + + return cats[delName] +} + +func (n *ThreadedNewsYAML) DeleteNewsItem(newsPath []string) error { + n.mu.Lock() + defer n.mu.Unlock() + + cats := n.ThreadedNews.Categories + delName := newsPath[len(newsPath)-1] + if len(newsPath) > 1 { + for _, fp := range newsPath[0 : len(newsPath)-1] { + cats = cats[fp].SubCats + } + } + + delete(cats, delName) + + return n.writeFile() +} + +func (n *ThreadedNewsYAML) GetArticle(newsPath []string, articleID uint32) *hotline.NewsArtData { + n.mu.Lock() + defer n.mu.Unlock() + + var cat hotline.NewsCategoryListData15 + cats := n.ThreadedNews.Categories + + for _, fp := range newsPath { + cat = cats[fp] + cats = cats[fp].SubCats + } + + art := cat.Articles[articleID] + if art == nil { + return nil + } + + return art +} + +// +//func (n *ThreadedNewsYAML) GetNewsCatByPath(paths []string) map[string]hotline.NewsCategoryListData15 { +// n.mu.Lock() +// defer n.mu.Unlock() +// +// cats := n.getCatByPath(paths) +// +// return cats +//} + +func (n *ThreadedNewsYAML) GetCategories(paths []string) []hotline.NewsCategoryListData15 { + n.mu.Lock() + defer n.mu.Unlock() + + var categories []hotline.NewsCategoryListData15 + for _, c := range n.getCatByPath(paths) { + categories = append(categories, c) + } + + slices.SortFunc(categories, func(a, b hotline.NewsCategoryListData15) int { + return cmp.Compare( + a.Name, + b.Name, + ) + }) + + return categories +} + +func (n *ThreadedNewsYAML) getCatByPath(paths []string) map[string]hotline.NewsCategoryListData15 { + cats := n.ThreadedNews.Categories + for _, path := range paths { + cats = cats[path].SubCats + } + + return cats +} + +func (n *ThreadedNewsYAML) PostArticle(newsPath []string, parentArticleID uint32, article hotline.NewsArtData) error { + n.mu.Lock() + defer n.mu.Unlock() + + binary.BigEndian.PutUint32(article.ParentArt[:], parentArticleID) + + cats := n.getCatByPath(newsPath[:len(newsPath)-1]) + + catName := newsPath[len(newsPath)-1] + cat := cats[catName] + + var keys []int + for k := range cat.Articles { + keys = append(keys, int(k)) + } + + nextID := uint32(1) + if len(keys) > 0 { + sort.Ints(keys) + prevID := uint32(keys[len(keys)-1]) + nextID = prevID + 1 + + binary.BigEndian.PutUint32(article.PrevArt[:], prevID) + + // Set next article Type + binary.BigEndian.PutUint32(cat.Articles[prevID].NextArt[:], nextID) + } + + // Update parent article with first child reply + parentID := parentArticleID + if parentID != 0 { + parentArt := cat.Articles[parentID] + + if parentArt.FirstChildArt == [4]byte{0, 0, 0, 0} { + binary.BigEndian.PutUint32(parentArt.FirstChildArt[:], nextID) + } + } + + cat.Articles[nextID] = &article + + cats[catName] = cat + + return n.writeFile() +} + +func (n *ThreadedNewsYAML) DeleteArticle(newsPath []string, articleID uint32, recursive bool) error { + n.mu.Lock() + defer n.mu.Unlock() + + if recursive { + // TODO: Handle delete recursive + } + + cats := n.getCatByPath(newsPath[:len(newsPath)-1]) + + catName := newsPath[len(newsPath)-1] + + cat := cats[catName] + delete(cat.Articles, articleID) + cats[catName] = cat + + return n.writeFile() +} + +func (n *ThreadedNewsYAML) ListArticles(newsPath []string) hotline.NewsArtListData { + n.mu.Lock() + defer n.mu.Unlock() + + var cat hotline.NewsCategoryListData15 + cats := n.ThreadedNews.Categories + + for _, fp := range newsPath { + cat = cats[fp] + cats = cats[fp].SubCats + } + + return cat.GetNewsArtListData() +} + +func (n *ThreadedNewsYAML) Load() error { + n.mu.Lock() + defer n.mu.Unlock() + + fh, err := os.Open(n.filePath) + if err != nil { + return err + } + defer fh.Close() + + n.ThreadedNews = hotline.ThreadedNews{} + + decoder := yaml.NewDecoder(fh) + return decoder.Decode(&n.ThreadedNews) +} + +func (n *ThreadedNewsYAML) writeFile() error { + out, err := yaml.Marshal(&n.ThreadedNews) + if err != nil { + return err + } + + // Define a temporary file path in the same directory. + tempFilePath := n.filePath + ".tmp" + + // Write the marshaled YAML to the temporary file. + if err := os.WriteFile(tempFilePath, out, 0644); err != nil { + return fmt.Errorf("write to temporary file: %v", err) + } + + // Atomically rename the temporary file to the final file path. + if err := os.Rename(tempFilePath, n.filePath); err != nil { + return fmt.Errorf("rename temporary file to final file: %v", err) + } + + return nil +} |