diff --git a/content/data.catalog.go b/content/data.catalog.go new file mode 100644 index 0000000..83ee75e --- /dev/null +++ b/content/data.catalog.go @@ -0,0 +1,45 @@ +package content + +import ( + "encoding/json" + "net/http" + "time" +) + +const GamesAPIURL = "https://games.teletype.hu/api/software" + +type softwareEntry struct { + Software struct { + Title string `json:"title"` + Platform string `json:"platform"` + Author string `json:"author"` + Desc string `json:"desc"` + } `json:"software"` + Releases []interface{} `json:"releases"` + LatestRelease *struct { + Version string `json:"version"` + HTMLFolderPath string `json:"htmlFolderPath"` + CartridgePath string `json:"cartridgePath"` + SourcePath string `json:"sourcePath"` + DocsFolderPath string `json:"docsFolderPath"` + } `json:"latestRelease"` +} + +type catalogRepository struct{} + +func (r *catalogRepository) fetchGames() ([]softwareEntry, error) { + client := &http.Client{Timeout: 12 * time.Second} + resp, err := client.Get(GamesAPIURL) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result struct { + Softwares []softwareEntry `json:"softwares"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + return result.Softwares, nil +} diff --git a/content/data.message.go b/content/data.message.go new file mode 100644 index 0000000..c577487 --- /dev/null +++ b/content/data.message.go @@ -0,0 +1,66 @@ +package content + +import ( + "encoding/csv" + "os" + "sync" +) + +type message struct { + Timestamp string + User string + Text string +} + +// MessageBoard holds message board data +type MessageBoard struct { + messages []message + mu sync.Mutex + path string +} + +// NewMessageBoard loads messages from the given .dat file +func NewMessageBoard(path string) *MessageBoard { + b := &MessageBoard{path: path} + b.load() + return b +} + +func (b *MessageBoard) load() { + f, err := os.Open(b.path) + if err != nil { + return + } + defer f.Close() + + r := csv.NewReader(f) + records, err := r.ReadAll() + if err != nil { + return + } + for _, row := range records { + if len(row) != 3 { + continue + } + b.messages = append(b.messages, message{Timestamp: row[0], User: row[1], Text: row[2]}) + } +} + +func (b *MessageBoard) append(msg message) error { + f, err := os.OpenFile(b.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) + if err != nil { + return err + } + defer f.Close() + + w := csv.NewWriter(f) + err = w.Write([]string{msg.Timestamp, msg.User, msg.Text}) + w.Flush() + return err +} + +func (b *MessageBoard) Count() int { + b.mu.Lock() + defer b.mu.Unlock() + return len(b.messages) +} diff --git a/content/data.wiki.go b/content/data.wiki.go new file mode 100644 index 0000000..29596d5 --- /dev/null +++ b/content/data.wiki.go @@ -0,0 +1,102 @@ +package content + +import ( + "bytes" + "encoding/json" + "fmt" + "net/http" + "time" +) + +const WikiJSBaseURL = "https://wiki.teletype.hu" + +type wikiPage struct { + ID interface{} `json:"id"` + Path string `json:"path"` + Title string `json:"title"` + Description string `json:"description"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + Locale string `json:"locale"` +} + +type wikiRepository struct { + token string +} + +func (r *wikiRepository) graphql(query string) (map[string]interface{}, error) { + payload := map[string]string{"query": query} + data, _ := json.Marshal(payload) + + req, err := http.NewRequest("POST", WikiJSBaseURL+"/graphql", bytes.NewBuffer(data)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json") + if r.token != "" { + req.Header.Set("Authorization", "Bearer "+r.token) + } + + client := &http.Client{Timeout: 12 * time.Second} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + var result map[string]interface{} + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +func (r *wikiRepository) fetchList(tag string) ([]wikiPage, error) { + q := fmt.Sprintf(`{ pages { list(orderBy: CREATED, orderByDirection: DESC, tags: ["%s"]) { id path title description createdAt updatedAt locale } } }`, tag) + res, err := r.graphql(q) + if err != nil { + return nil, err + } + + data, ok := res["data"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid response format") + } + pages, ok := data["pages"].(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("invalid response format") + } + listRaw, ok := pages["list"].([]interface{}) + if !ok { + return nil, fmt.Errorf("invalid response format") + } + + var list []wikiPage + jsonBytes, _ := json.Marshal(listRaw) + json.Unmarshal(jsonBytes, &list) + return list, nil +} + +func (r *wikiRepository) fetchContent(pageID interface{}) (string, error) { + var idStr string + switch v := pageID.(type) { + case string: + idStr = v + case float64: + idStr = fmt.Sprintf("%.0f", v) + default: + idStr = fmt.Sprintf("%v", v) + } + + q := fmt.Sprintf(`{ pages { single(id: %s) { content } } }`, idStr) + res, err := r.graphql(q) + if err != nil { + return "", err + } + + data := res["data"].(map[string]interface{}) + pages := data["pages"].(map[string]interface{}) + single := pages["single"].(map[string]interface{}) + return single["content"].(string), nil +} diff --git a/content/handler.blog.go b/content/handler.blog.go new file mode 100644 index 0000000..37ea09b --- /dev/null +++ b/content/handler.blog.go @@ -0,0 +1,108 @@ +package content + +import ( + "bbs-server/engine" + "fmt" + "strconv" + "strings" +) + +// BlogHandler handles the Blog Posts menu item +type BlogHandler struct { + repo *wikiRepository +} + +func NewBlogHandler(token string) *BlogHandler { + return &BlogHandler{repo: &wikiRepository{token: token}} +} + +func (h *BlogHandler) Handle(s *engine.Session) { + renderWikiList(s, h.repo, "blog", "Blog Posts", engine.BL) +} + +// renderWikiList is a helper used by wiki-based handlers +func renderWikiList(s *engine.Session, repo *wikiRepository, tag, title, color string) { + s.Printer.BoxHeader(title, color) + s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", engine.GY, s.Lang["WikiLoading"], engine.R)) + + pages, err := repo.fetchList(tag) + if err != nil { + s.Printer.Send(fmt.Sprintf("\r\n%s%s: %v%s\r\n", engine.RD, s.Lang["WikiConnError"], err, engine.R)) + s.Printer.Pause(s.Lang) + return + } + + s.Printer.Send("\r\033[A\033[2K") + + if len(pages) == 0 { + s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", engine.GY, fmt.Sprintf(s.Lang["WikiNoResults"], tag), engine.R)) + s.Printer.Pause(s.Lang) + return + } + + maxIdx := 25 + if len(pages) < maxIdx { + maxIdx = len(pages) + } + + for i, p := range pages[:maxIdx] { + date := s.Printer.FmtDate(p.CreatedAt) + if date == "–" { + date = s.Printer.FmtDate(p.UpdatedAt) + } + titleP := p.Title + if titleP == "" { + titleP = p.Path + } + if len(titleP) > 55 { + titleP = titleP[:55] + } + desc := p.Description + if len(desc) > 60 { + desc = desc[:60] + } + s.Printer.Send(fmt.Sprintf(" %s%2d%s %s%s%s\r\n", color, i+1, engine.R, engine.WH, titleP, engine.R)) + if desc != "" { + s.Printer.Send(fmt.Sprintf(" %s%s %s%s%s\r\n", engine.GY, date, engine.DIM, desc, engine.R)) + } else { + s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", engine.GY, date, engine.R)) + } + } + + s.Printer.Send(fmt.Sprintf("\r\n%s%s%s ", engine.GY, s.Lang["WikiEnterNum"], engine.R)) + choice, _ := s.Printer.ReadLine() + choice = strings.TrimSpace(choice) + idx, err := strconv.Atoi(choice) + if err != nil || idx < 1 || idx > len(pages) { + return + } + + page := pages[idx-1] + s.Printer.Send(fmt.Sprintf("\r\n%s%s%s", engine.GY, s.Lang["WikiFetchContent"], engine.R)) + pageContent, err := repo.fetchContent(page.ID) + if err != nil { + s.Printer.Send(fmt.Sprintf("\r\n%sHiba: %v%s\r\n", engine.RD, err, engine.R)) + s.Printer.Pause(s.Lang) + return + } + + s.Printer.Send("\r\033[A\033[2K") + s.Printer.Send("\r\n") + s.Printer.HR("═", color) + s.Printer.Send("\r\n") + s.Printer.Send(fmt.Sprintf(" %s%s%s%s\r\n", engine.WH, engine.B, page.Title, engine.R)) + url := fmt.Sprintf("%s/%s/%s", WikiJSBaseURL, page.Locale, page.Path) + s.Printer.Send(fmt.Sprintf(" %s%s %s%s%s\r\n", engine.GY, s.Printer.FmtDate(page.CreatedAt), engine.DIM, url, engine.R)) + s.Printer.HR("─", engine.GY) + s.Printer.Send("\r\n\r\n") + + body := s.Printer.Wrapped(pageContent, 2, 5000) + for _, line := range strings.Split(body, "\r\n") { + s.Printer.Send(line + "\r\n") + } + + s.Printer.Send("\r\n") + s.Printer.HR("─", engine.GY) + s.Printer.Send("\r\n") + s.Printer.Pause(s.Lang) +} diff --git a/content/catalog.go b/content/handler.catalog.go similarity index 68% rename from content/catalog.go rename to content/handler.catalog.go index 55702f6..8d64498 100644 --- a/content/catalog.go +++ b/content/handler.catalog.go @@ -2,52 +2,11 @@ package content import ( "bbs-server/engine" - "encoding/json" "fmt" - "net/http" "strings" - "time" ) -const GamesAPIURL = "https://games.teletype.hu/api/software" - -type softwareEntry struct { - Software struct { - Title string `json:"title"` - Platform string `json:"platform"` - Author string `json:"author"` - Desc string `json:"desc"` - } `json:"software"` - Releases []interface{} `json:"releases"` - LatestRelease *struct { - Version string `json:"version"` - HTMLFolderPath string `json:"htmlFolderPath"` - CartridgePath string `json:"cartridgePath"` - SourcePath string `json:"sourcePath"` - DocsFolderPath string `json:"docsFolderPath"` - } `json:"latestRelease"` -} - -type catalogRepository struct{} - -func (r *catalogRepository) fetchGames() ([]softwareEntry, error) { - client := &http.Client{Timeout: 12 * time.Second} - resp, err := client.Get(GamesAPIURL) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - var result struct { - Softwares []softwareEntry `json:"softwares"` - } - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return nil, err - } - return result.Softwares, nil -} - -// CatalogHandler renders the game catalog +// CatalogHandler handles the Game Catalog menu item type CatalogHandler struct { repo *catalogRepository } @@ -56,8 +15,7 @@ func NewCatalogHandler() *CatalogHandler { return &CatalogHandler{repo: &catalogRepository{}} } -// Show displays the game catalog -func (h *CatalogHandler) Show(s *engine.Session) { +func (h *CatalogHandler) Handle(s *engine.Session) { s.Printer.BoxHeader(s.Lang["CatTitle"], engine.YL) s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", engine.GY, s.Lang["WikiLoading"], engine.R)) diff --git a/content/handler.go b/content/handler.go new file mode 100644 index 0000000..969394d --- /dev/null +++ b/content/handler.go @@ -0,0 +1,8 @@ +package content + +import "bbs-server/engine" + +// Handler is the common interface for all BBS menu handlers +type Handler interface { + Handle(s *engine.Session) +} diff --git a/content/handler.howtos.go b/content/handler.howtos.go new file mode 100644 index 0000000..98a372c --- /dev/null +++ b/content/handler.howtos.go @@ -0,0 +1,16 @@ +package content + +import "bbs-server/engine" + +// HowToHandler handles the HowTo Guides menu item +type HowToHandler struct { + repo *wikiRepository +} + +func NewHowToHandler(token string) *HowToHandler { + return &HowToHandler{repo: &wikiRepository{token: token}} +} + +func (h *HowToHandler) Handle(s *engine.Session) { + renderWikiList(s, h.repo, "howto", "HowTo Guides", engine.MG) +} diff --git a/content/handler.messageboard.index.go b/content/handler.messageboard.index.go new file mode 100644 index 0000000..6e48332 --- /dev/null +++ b/content/handler.messageboard.index.go @@ -0,0 +1,40 @@ +package content + +import ( + "bbs-server/engine" + "fmt" +) + +// MessageBoardIndexHandler displays the message board posts +type MessageBoardIndexHandler struct { + board *MessageBoard +} + +func NewMessageBoardIndexHandler(board *MessageBoard) *MessageBoardIndexHandler { + return &MessageBoardIndexHandler{board: board} +} + +func (h *MessageBoardIndexHandler) Handle(s *engine.Session) { + s.Printer.BoxHeader(s.Lang["MsgBoardTitle"], engine.GR) + + h.board.mu.Lock() + snap := make([]message, len(h.board.messages)) + copy(snap, h.board.messages) + h.board.mu.Unlock() + + if len(snap) == 0 { + s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", engine.GY, s.Lang["MsgNoMessages"], engine.R)) + } else { + start := 0 + if len(snap) > 30 { + start = len(snap) - 30 + } + for i, msg := range snap[start:] { + s.Printer.Send(fmt.Sprintf(" %s%02d%s %s%s%s %s%s:%s %s\r\n", + engine.GR, i+1, engine.R, + engine.GY, msg.Timestamp, engine.R, + engine.WH, msg.User, engine.R, msg.Text)) + } + } + s.Printer.Pause(s.Lang) +} diff --git a/content/handler.messageboard.new.go b/content/handler.messageboard.new.go new file mode 100644 index 0000000..9f916ed --- /dev/null +++ b/content/handler.messageboard.new.go @@ -0,0 +1,38 @@ +package content + +import ( + "bbs-server/engine" + "fmt" + "strings" + "time" +) + +// MessageBoardNewHandler handles posting new messages to the board +type MessageBoardNewHandler struct { + board *MessageBoard +} + +func NewMessageBoardNewHandler(board *MessageBoard) *MessageBoardNewHandler { + return &MessageBoardNewHandler{board: board} +} + +func (h *MessageBoardNewHandler) Handle(s *engine.Session) { + s.Printer.Send(fmt.Sprintf("\r\n%s%s%s ", engine.WH, s.Lang["MsgEnterText"], engine.R)) + msgText, _ := s.Printer.ReadLine() + msgText = strings.TrimSpace(msgText) + if msgText != "" { + if len(msgText) > 200 { + msgText = msgText[:200] + } + ts := time.Now().Format("01-02 15:04") + msg := message{Timestamp: ts, User: s.Username, Text: msgText} + h.board.mu.Lock() + h.board.messages = append(h.board.messages, msg) + h.board.mu.Unlock() + h.board.append(msg) + s.Printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n", engine.GR, s.Lang["MsgSent"], engine.R)) + } else { + s.Printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n", engine.GY, s.Lang["MsgEmpty"], engine.R)) + } + s.Printer.Pause(s.Lang) +} diff --git a/content/online.go b/content/handler.online.go similarity index 77% rename from content/online.go rename to content/handler.online.go index e7433f4..a94b9c7 100644 --- a/content/online.go +++ b/content/handler.online.go @@ -6,8 +6,14 @@ import ( "sort" ) -// Online displays currently logged-in users -func Online(s *engine.Session) { +// OnlineHandler displays currently logged-in users +type OnlineHandler struct{} + +func NewOnlineHandler() *OnlineHandler { + return &OnlineHandler{} +} + +func (h *OnlineHandler) Handle(s *engine.Session) { s.Printer.BoxHeader(s.Lang["OnlineTitle"], engine.CY) snap := s.State.Snapshot() diff --git a/content/handler.sysinfo.go b/content/handler.sysinfo.go new file mode 100644 index 0000000..99970a6 --- /dev/null +++ b/content/handler.sysinfo.go @@ -0,0 +1,27 @@ +package content + +import ( + "bbs-server/engine" + "fmt" + "runtime" +) + +// SysinfoHandler displays system information +type SysinfoHandler struct { + board *MessageBoard +} + +func NewSysinfoHandler(board *MessageBoard) *SysinfoHandler { + return &SysinfoHandler{board: board} +} + +func (h *SysinfoHandler) Handle(s *engine.Session) { + s.Printer.BoxHeader(s.Lang["SysTitle"], engine.GY) + + s.Printer.Send(fmt.Sprintf(" %s%-15s%s %d\r\n", engine.GY, s.Lang["SysUsers"], engine.WH, s.State.UserCount())) + s.Printer.Send(fmt.Sprintf(" %s%-15s%s %d\r\n", engine.GY, s.Lang["SysMessages"], engine.WH, h.board.Count())) + s.Printer.Send(fmt.Sprintf(" %s%-15s%s %s\r\n", engine.GY, s.Lang["SysOS"], engine.WH, runtime.GOOS)) + s.Printer.Send(fmt.Sprintf(" %s%-15s%s %s\r\n", engine.GY, s.Lang["SysArch"], engine.WH, runtime.GOARCH)) + + s.Printer.Pause(s.Lang) +} diff --git a/content/messages.go b/content/messages.go deleted file mode 100644 index 53e8cf1..0000000 --- a/content/messages.go +++ /dev/null @@ -1,121 +0,0 @@ -package content - -import ( - "bbs-server/engine" - "encoding/csv" - "fmt" - "os" - "strings" - "sync" - "time" -) - -type message struct { - Timestamp string - User string - Text string -} - -// MessageBoard holds message board data and its handler -type MessageBoard struct { - messages []message - mu sync.Mutex - path string -} - -// NewMessageBoard loads messages from the given .dat file (if it exists) -func NewMessageBoard(path string) *MessageBoard { - b := &MessageBoard{path: path} - b.load() - return b -} - -func (b *MessageBoard) load() { - f, err := os.Open(b.path) - if err != nil { - return // file does not exist yet, start empty - } - defer f.Close() - - r := csv.NewReader(f) - records, err := r.ReadAll() - if err != nil { - return - } - for _, row := range records { - if len(row) != 3 { - continue - } - b.messages = append(b.messages, message{Timestamp: row[0], User: row[1], Text: row[2]}) - } -} - -func (b *MessageBoard) append(msg message) error { - f, err := os.OpenFile(b.path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644) - if err != nil { - return err - } - defer f.Close() - - w := csv.NewWriter(f) - err = w.Write([]string{msg.Timestamp, msg.User, msg.Text}) - w.Flush() - return err -} - -// Count returns the number of messages in a thread-safe manner -func (b *MessageBoard) Count() int { - b.mu.Lock() - defer b.mu.Unlock() - return len(b.messages) -} - -// Show displays the message board and handles new message input -func (b *MessageBoard) Show(s *engine.Session) { - s.Printer.BoxHeader(s.Lang["MsgBoardTitle"], engine.GR) - - b.mu.Lock() - snap := make([]message, len(b.messages)) - copy(snap, b.messages) - b.mu.Unlock() - - if len(snap) == 0 { - s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", engine.GY, s.Lang["MsgNoMessages"], engine.R)) - } else { - start := 0 - if len(snap) > 30 { - start = len(snap) - 30 - } - for i, msg := range snap[start:] { - s.Printer.Send(fmt.Sprintf(" %s%02d%s %s%s%s %s%s:%s %s\r\n", - engine.GR, i+1, engine.R, - engine.GY, msg.Timestamp, engine.R, - engine.WH, msg.User, engine.R, msg.Text)) - } - } - - s.Printer.Send(fmt.Sprintf("\r\n%s[N]%s %s %s[ENTER]%s %s → ", - engine.GY, engine.R, s.Lang["MsgNew"], - engine.GY, engine.R, s.Lang["MsgBack"])) - - choice, _ := s.Printer.ReadLine() - if strings.ToUpper(strings.TrimSpace(choice)) == "N" { - s.Printer.Send(fmt.Sprintf("\r\n%s%s%s ", engine.WH, s.Lang["MsgEnterText"], engine.R)) - msgText, _ := s.Printer.ReadLine() - msgText = strings.TrimSpace(msgText) - if msgText != "" { - if len(msgText) > 200 { - msgText = msgText[:200] - } - ts := time.Now().Format("01-02 15:04") - msg := message{Timestamp: ts, User: s.Username, Text: msgText} - b.mu.Lock() - b.messages = append(b.messages, msg) - b.mu.Unlock() - b.append(msg) - s.Printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n", engine.GR, s.Lang["MsgSent"], engine.R)) - } else { - s.Printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n", engine.GY, s.Lang["MsgEmpty"], engine.R)) - } - } -} diff --git a/content/sysinfo.go b/content/sysinfo.go deleted file mode 100644 index 2987518..0000000 --- a/content/sysinfo.go +++ /dev/null @@ -1,32 +0,0 @@ -package content - -import ( - "bbs-server/engine" - "fmt" - "strconv" - "time" -) - -// Sysinfo returns a HandlerFunc that displays system information -func Sysinfo(board *MessageBoard) engine.HandlerFunc { - return func(s *engine.Session) { - s.Printer.BoxHeader(s.Lang["SysInfoTitle"], engine.GY) - - now := time.Now().Format("2006-01-02 15:04:05") - - rows := [][]string{ - {s.Lang["SysServerTime"], now}, - {s.Lang["SysOnlineUsers"], strconv.Itoa(s.State.UserCount())}, - {s.Lang["SysMsgCount"], strconv.Itoa(board.Count())}, - {s.Lang["SysWikiURL"], WikiJSBaseURL}, - {s.Lang["SysGamesAPI"], GamesAPIURL}, - {s.Lang["SysPlatform"], "Go BBS v2.0"}, - } - - for _, row := range rows { - s.Printer.Send(fmt.Sprintf(" %s%-18s%s %s%s%s\r\n", - engine.GY, row[0], engine.R, engine.WH, row[1], engine.R)) - } - s.Printer.Pause(s.Lang) - } -} diff --git a/content/wiki.go b/content/wiki.go deleted file mode 100644 index dd1a254..0000000 --- a/content/wiki.go +++ /dev/null @@ -1,203 +0,0 @@ -package content - -import ( - "bbs-server/engine" - "bytes" - "encoding/json" - "fmt" - "net/http" - "strconv" - "strings" - "time" -) - -const WikiJSBaseURL = "https://wiki.teletype.hu" - -type wikiPage struct { - ID interface{} `json:"id"` - Path string `json:"path"` - Title string `json:"title"` - Description string `json:"description"` - CreatedAt string `json:"createdAt"` - UpdatedAt string `json:"updatedAt"` - Locale string `json:"locale"` -} - -type wikiRepository struct { - token string -} - -func (r *wikiRepository) graphql(query string) (map[string]interface{}, error) { - payload := map[string]string{"query": query} - data, _ := json.Marshal(payload) - - req, err := http.NewRequest("POST", WikiJSBaseURL+"/graphql", bytes.NewBuffer(data)) - if err != nil { - return nil, err - } - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Accept", "application/json") - if r.token != "" { - req.Header.Set("Authorization", "Bearer "+r.token) - } - - client := &http.Client{Timeout: 12 * time.Second} - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - var result map[string]interface{} - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return nil, err - } - return result, nil -} - -func (r *wikiRepository) fetchList(tag string) ([]wikiPage, error) { - q := fmt.Sprintf(`{ pages { list(orderBy: CREATED, orderByDirection: DESC, tags: ["%s"]) { id path title description createdAt updatedAt locale } } }`, tag) - res, err := r.graphql(q) - if err != nil { - return nil, err - } - - data, ok := res["data"].(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("invalid response format") - } - pages, ok := data["pages"].(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("invalid response format") - } - listRaw, ok := pages["list"].([]interface{}) - if !ok { - return nil, fmt.Errorf("invalid response format") - } - - var list []wikiPage - jsonBytes, _ := json.Marshal(listRaw) - json.Unmarshal(jsonBytes, &list) - return list, nil -} - -func (r *wikiRepository) fetchContent(pageID interface{}) (string, error) { - var idStr string - switch v := pageID.(type) { - case string: - idStr = v - case float64: - idStr = fmt.Sprintf("%.0f", v) - default: - idStr = fmt.Sprintf("%v", v) - } - - q := fmt.Sprintf(`{ pages { single(id: %s) { content } } }`, idStr) - res, err := r.graphql(q) - if err != nil { - return "", err - } - - data := res["data"].(map[string]interface{}) - pages := data["pages"].(map[string]interface{}) - single := pages["single"].(map[string]interface{}) - return single["content"].(string), nil -} - -// WikiHandler renders Wiki.js content in the BBS -type WikiHandler struct { - repo *wikiRepository -} - -func NewWikiHandler(token string) *WikiHandler { - return &WikiHandler{repo: &wikiRepository{token: token}} -} - -// List returns a HandlerFunc that lists pages with the given tag -func (h *WikiHandler) List(tag, title, color string) engine.HandlerFunc { - return func(s *engine.Session) { - s.Printer.BoxHeader(title, color) - s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", engine.GY, s.Lang["WikiLoading"], engine.R)) - - pages, err := h.repo.fetchList(tag) - if err != nil { - s.Printer.Send(fmt.Sprintf("\r\n%s%s: %v%s\r\n", engine.RD, s.Lang["WikiConnError"], err, engine.R)) - s.Printer.Pause(s.Lang) - return - } - - s.Printer.Send("\r\033[A\033[2K") - - if len(pages) == 0 { - s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", engine.GY, fmt.Sprintf(s.Lang["WikiNoResults"], tag), engine.R)) - s.Printer.Pause(s.Lang) - return - } - - maxIdx := 25 - if len(pages) < maxIdx { - maxIdx = len(pages) - } - - for i, p := range pages[:maxIdx] { - date := s.Printer.FmtDate(p.CreatedAt) - if date == "–" { - date = s.Printer.FmtDate(p.UpdatedAt) - } - titleP := p.Title - if titleP == "" { - titleP = p.Path - } - if len(titleP) > 55 { - titleP = titleP[:55] - } - desc := p.Description - if len(desc) > 60 { - desc = desc[:60] - } - s.Printer.Send(fmt.Sprintf(" %s%2d%s %s%s%s\r\n", color, i+1, engine.R, engine.WH, titleP, engine.R)) - if desc != "" { - s.Printer.Send(fmt.Sprintf(" %s%s %s%s%s\r\n", engine.GY, date, engine.DIM, desc, engine.R)) - } else { - s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", engine.GY, date, engine.R)) - } - } - - s.Printer.Send(fmt.Sprintf("\r\n%s%s%s ", engine.GY, s.Lang["WikiEnterNum"], engine.R)) - choice, _ := s.Printer.ReadLine() - choice = strings.TrimSpace(choice) - idx, err := strconv.Atoi(choice) - if err != nil || idx < 1 || idx > len(pages) { - return - } - - page := pages[idx-1] - s.Printer.Send(fmt.Sprintf("\r\n%s%s%s", engine.GY, s.Lang["WikiFetchContent"], engine.R)) - pageContent, err := h.repo.fetchContent(page.ID) - if err != nil { - s.Printer.Send(fmt.Sprintf("\r\n%sHiba: %v%s\r\n", engine.RD, err, engine.R)) - s.Printer.Pause(s.Lang) - return - } - - s.Printer.Send("\r\033[A\033[2K") - s.Printer.Send("\r\n") - s.Printer.HR("═", color) - s.Printer.Send("\r\n") - s.Printer.Send(fmt.Sprintf(" %s%s%s%s\r\n", engine.WH, engine.B, page.Title, engine.R)) - url := fmt.Sprintf("%s/%s/%s", WikiJSBaseURL, page.Locale, page.Path) - s.Printer.Send(fmt.Sprintf(" %s%s %s%s%s\r\n", engine.GY, s.Printer.FmtDate(page.CreatedAt), engine.DIM, url, engine.R)) - s.Printer.HR("─", engine.GY) - s.Printer.Send("\r\n\r\n") - - body := s.Printer.Wrapped(pageContent, 2, 5000) - for _, line := range strings.Split(body, "\r\n") { - s.Printer.Send(line + "\r\n") - } - - s.Printer.Send("\r\n") - s.Printer.HR("─", engine.GY) - s.Printer.Send("\r\n") - s.Printer.Pause(s.Lang) - } -} diff --git a/main.go b/main.go index 069b9c5..36cd6ef 100644 --- a/main.go +++ b/main.go @@ -22,7 +22,7 @@ const banner = ` ╚██████╔╝██║ ██║██║ ╚═╝ ██║███████╗███████║ ╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝ - ░░ BBS v2.0 ░░ teletype.hu ░░ + ░░ BBS v2.0 ░░ games.teletype.hu ░░ Welcome to the Teletype community bulletin board! ` @@ -42,9 +42,14 @@ func main() { fmt.Printf("Games API: %s\n", content.GamesAPIURL) fmt.Printf("Token: %s\n", tokenStatus) - wiki := content.NewWikiHandler(wikiToken) - cat := content.NewCatalogHandler() - board := content.NewMessageBoard(boardPath) + messageBoard := content.NewMessageBoard(boardPath) + blogHandler := content.NewBlogHandler(wikiToken) + howtoHandler := content.NewHowToHandler(wikiToken) + catalogHandler := content.NewCatalogHandler() + messageBoardIndexHandler := content.NewMessageBoardIndexHandler(messageBoard) + messageBoardNewHandler := content.NewMessageBoardNewHandler(messageBoard) + onlineHandler := content.NewOnlineHandler() + sysinfoHandler := content.NewSysinfoHandler(messageBoard) bbs := engine.New(engine.Config{ Host: "0.0.0.0", @@ -55,13 +60,14 @@ func main() { bbs.Menu(func(m *engine.Menu) { m.Title("MAIN MENU") - m.Item("1", "Message Board", engine.GR, board.Show) - m.Item("2", "Blog Posts", engine.BL, wiki.List("blog", "Blog Posts", engine.BL)) - m.Item("3", "HowTo Guides", engine.MG, wiki.List("howto", "HowTo Guides", engine.MG)) - m.Item("4", "Game Catalog", engine.YL, cat.Show) - m.Item("5", "Online Users", engine.CY, content.Online) - m.Item("6", "System Info", engine.GY, content.Sysinfo(board)) - m.Item("Q", "Exit", engine.RD, engine.Exit) + m.Item("1", "Message Board", engine.GR, messageBoardIndexHandler.Handle) + m.Item("N", "New Message", engine.WH, messageBoardNewHandler.Handle) + m.Item("2", "Blog Posts", engine.BL, blogHandler.Handle) + m.Item("3", "HowTo Guides", engine.MG, howtoHandler.Handle) + m.Item("4", "Game Catalog", engine.YL, catalogHandler.Handle) + m.Item("5", "Online Users", engine.CY, onlineHandler.Handle) + m.Item("6", "System Info", engine.GY, sysinfoHandler.Handle) + m.Item("Q", "Exit", engine.RD, engine.Exit) }) bbs.Start()