aboutsummaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorJeff Halter <868228+jhalter@users.noreply.github.com>2024-07-09 21:36:27 -0700
committerJeff Halter <868228+jhalter@users.noreply.github.com>2024-07-09 21:42:05 -0700
commitd9bc63a10d0978d9a5222cf7be74044e55f409b7 (patch)
tree1797c9593c279bf1334ed8285de8054a92a28dcb /internal
parent8fa166777cbcd92e871e937d9557f0f1a732c04d (diff)
Extensive refactor and clean up
Diffstat (limited to 'internal')
-rw-r--r--internal/mobius/ban.go76
-rw-r--r--internal/mobius/config.go30
-rw-r--r--internal/mobius/news.go87
-rw-r--r--internal/mobius/threaded_news.go251
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
+}