refact round 2
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
.env
|
.env
|
||||||
bbs-server
|
bbs-server
|
||||||
tmp
|
tmp
|
||||||
|
data/messages.dat
|
||||||
133
content/catalog.go
Normal file
133
content/catalog.go
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
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
|
||||||
|
type CatalogHandler struct {
|
||||||
|
repo *catalogRepository
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCatalogHandler() *CatalogHandler {
|
||||||
|
return &CatalogHandler{repo: &catalogRepository{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show displays the game catalog
|
||||||
|
func (h *CatalogHandler) Show(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))
|
||||||
|
|
||||||
|
softwares, err := h.repo.fetchGames()
|
||||||
|
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(softwares) == 0 {
|
||||||
|
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", engine.GY, s.Lang["CatNoGames"], engine.R))
|
||||||
|
s.Printer.Pause(s.Lang)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
base := "https://games.teletype.hu"
|
||||||
|
|
||||||
|
for i, entry := range softwares {
|
||||||
|
sw := entry.Software
|
||||||
|
lr := entry.LatestRelease
|
||||||
|
|
||||||
|
s.Printer.Send(fmt.Sprintf("\r\n %s%s%s\r\n", engine.YL, strings.Repeat("─", engine.W-4), engine.R))
|
||||||
|
s.Printer.Send(fmt.Sprintf(" %s%2d.%s %s%s%s%s %s[%s] by %s%s\r\n",
|
||||||
|
engine.YL, i+1, engine.R,
|
||||||
|
engine.WH, engine.B, sw.Title, engine.R,
|
||||||
|
engine.GY, sw.Platform, sw.Author, engine.R))
|
||||||
|
|
||||||
|
if sw.Desc != "" {
|
||||||
|
wrappedDesc := s.Printer.Wrapped(sw.Desc, 7, 1000)
|
||||||
|
for _, line := range strings.Split(wrappedDesc, "\r\n") {
|
||||||
|
s.Printer.Send(fmt.Sprintf("%s%s\r\n", engine.GY, line))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if lr != nil {
|
||||||
|
badges := []string{}
|
||||||
|
if lr.HTMLFolderPath != "" {
|
||||||
|
badges = append(badges, fmt.Sprintf("%s[▶ Play]%s", engine.GR, engine.R))
|
||||||
|
}
|
||||||
|
if lr.CartridgePath != "" {
|
||||||
|
badges = append(badges, fmt.Sprintf("%s[⬇ Download]%s", engine.BL, engine.R))
|
||||||
|
}
|
||||||
|
if lr.SourcePath != "" {
|
||||||
|
badges = append(badges, fmt.Sprintf("%s[Source]%s", engine.MG, engine.R))
|
||||||
|
}
|
||||||
|
if lr.DocsFolderPath != "" {
|
||||||
|
badges = append(badges, fmt.Sprintf("%s[Docs]%s", engine.YL, engine.R))
|
||||||
|
}
|
||||||
|
|
||||||
|
badgeStr := "–"
|
||||||
|
if len(badges) > 0 {
|
||||||
|
badgeStr = strings.Join(badges, " ")
|
||||||
|
}
|
||||||
|
s.Printer.Send(fmt.Sprintf(" %s%s: v%s%s %s\r\n",
|
||||||
|
engine.GY, s.Lang["CatLatest"], lr.Version, engine.R, badgeStr))
|
||||||
|
|
||||||
|
if lr.HTMLFolderPath != "" {
|
||||||
|
url := base + lr.HTMLFolderPath
|
||||||
|
s.Printer.Send(fmt.Sprintf(" %s▶ %s%s\r\n", engine.DIM, url, engine.R))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n",
|
||||||
|
engine.GY, fmt.Sprintf(s.Lang["CatVersions"], len(entry.Releases)), engine.R))
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Printer.HR("─", engine.YL)
|
||||||
|
s.Printer.Send("\r\n")
|
||||||
|
s.Printer.Send(fmt.Sprintf(" %s%s: %s%s\r\n", engine.GY, s.Lang["CatFull"], base, engine.R))
|
||||||
|
s.Printer.Pause(s.Lang)
|
||||||
|
}
|
||||||
121
content/messages.go
Normal file
121
content/messages.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
content/online.go
Normal file
32
content/online.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package content
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bbs-server/engine"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Online displays currently logged-in users
|
||||||
|
func Online(s *engine.Session) {
|
||||||
|
s.Printer.BoxHeader(s.Lang["OnlineTitle"], engine.CY)
|
||||||
|
|
||||||
|
snap := s.State.Snapshot()
|
||||||
|
keys := make([]string, 0, len(snap))
|
||||||
|
for k := range snap {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
for _, addr := range keys {
|
||||||
|
user := snap[addr]
|
||||||
|
marker := ""
|
||||||
|
if addr == s.Addr {
|
||||||
|
marker = fmt.Sprintf(" %s%s%s", engine.GR, s.Lang["OnlineYou"], engine.R)
|
||||||
|
}
|
||||||
|
s.Printer.Send(fmt.Sprintf(" %s•%s %s%s%s%s\r\n", engine.CY, engine.R, engine.WH, user, engine.R, marker))
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Printer.Send(fmt.Sprintf("\r\n %s%s\r\n", engine.GY,
|
||||||
|
fmt.Sprintf(s.Lang["OnlineTotal"], engine.WH, len(snap), engine.GY, engine.R)))
|
||||||
|
s.Printer.Pause(s.Lang)
|
||||||
|
}
|
||||||
32
content/sysinfo.go
Normal file
32
content/sysinfo.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
203
content/wiki.go
Normal file
203
content/wiki.go
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
0
data/.keep
Normal file
0
data/.keep
Normal file
@@ -8,6 +8,9 @@ services:
|
|||||||
- "2323:2323"
|
- "2323:2323"
|
||||||
environment:
|
environment:
|
||||||
- WEBAPP_WIKIJS_TOKEN=${WEBAPP_WIKIJS_TOKEN}
|
- WEBAPP_WIKIJS_TOKEN=${WEBAPP_WIKIJS_TOKEN}
|
||||||
|
- MESSAGES_PATH=/data/messages.dat
|
||||||
|
volumes:
|
||||||
|
- bbs-messages:/data
|
||||||
restart: always
|
restart: always
|
||||||
read_only: true
|
read_only: true
|
||||||
tmpfs:
|
tmpfs:
|
||||||
@@ -15,4 +18,7 @@ services:
|
|||||||
cap_drop:
|
cap_drop:
|
||||||
- ALL
|
- ALL
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
bbs-messages:
|
||||||
@@ -8,8 +8,10 @@ services:
|
|||||||
- "2323:2323"
|
- "2323:2323"
|
||||||
volumes:
|
volumes:
|
||||||
- ./:/app/
|
- ./:/app/
|
||||||
|
- ./data:/data
|
||||||
environment:
|
environment:
|
||||||
- WEBAPP_WIKIJS_TOKEN=${WEBAPP_WIKIJS_TOKEN:-}
|
- WEBAPP_WIKIJS_TOKEN=${WEBAPP_WIKIJS_TOKEN:-}
|
||||||
|
- MESSAGES_PATH=/data/messages.dat
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
|
|||||||
@@ -1,22 +1,15 @@
|
|||||||
package lib
|
package engine
|
||||||
|
|
||||||
|
// T is the i18n dictionary type
|
||||||
type T map[string]string
|
type T map[string]string
|
||||||
|
|
||||||
|
// En is the default English dictionary
|
||||||
var En = T{
|
var En = T{
|
||||||
"Welcome": "Welcome to Teletype's Bulletin Board System!",
|
|
||||||
"AskName": "Enter your name:",
|
"AskName": "Enter your name:",
|
||||||
"Greeting": "Hello, %s%s%s! Welcome to Teletype BBS!",
|
"Greeting": "Hello, %s%s%s! Welcome to Teletype BBS!",
|
||||||
"MainMenuTitle": "MAIN MENU",
|
|
||||||
"MenuUzenopal": "Message Board",
|
|
||||||
"MenuBlog": "Blog Posts",
|
|
||||||
"MenuHowto": "HowTo Guides",
|
|
||||||
"MenuCatalog": "Game Catalog",
|
|
||||||
"MenuOnline": "Online Users",
|
|
||||||
"MenuSysinfo": "System Info",
|
|
||||||
"MenuExit": "Exit",
|
|
||||||
"Choice": "Choice: ",
|
|
||||||
"Pause": "Press ENTER...",
|
|
||||||
"Goodbye": "Goodbye, %s! 👋",
|
"Goodbye": "Goodbye, %s! 👋",
|
||||||
|
"Pause": "Press ENTER...",
|
||||||
|
"Choice": "Choice: ",
|
||||||
"MsgBoardTitle": "📋 MESSAGE BOARD",
|
"MsgBoardTitle": "📋 MESSAGE BOARD",
|
||||||
"MsgNoMessages": "(No messages yet — be the first!)",
|
"MsgNoMessages": "(No messages yet — be the first!)",
|
||||||
"MsgNew": "Write new message",
|
"MsgNew": "Write new message",
|
||||||
@@ -44,6 +37,4 @@ var En = T{
|
|||||||
"SysWikiURL": "Wiki URL",
|
"SysWikiURL": "Wiki URL",
|
||||||
"SysGamesAPI": "Games API",
|
"SysGamesAPI": "Games API",
|
||||||
"SysPlatform": "Platform",
|
"SysPlatform": "Platform",
|
||||||
"SysYes": "yes",
|
|
||||||
"SysNo": "no",
|
|
||||||
}
|
}
|
||||||
78
engine/menu.go
Normal file
78
engine/menu.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandlerFunc is the handler type for a menu item
|
||||||
|
type HandlerFunc func(*Session)
|
||||||
|
|
||||||
|
// Exit is a built-in handler that terminates the session
|
||||||
|
var Exit HandlerFunc = func(s *Session) { s.Quit = true }
|
||||||
|
|
||||||
|
type menuItem struct {
|
||||||
|
Key string
|
||||||
|
Label string
|
||||||
|
Color string
|
||||||
|
Handler HandlerFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu defines the BBS main menu using a declarative DSL
|
||||||
|
type Menu struct {
|
||||||
|
title string
|
||||||
|
items []menuItem
|
||||||
|
}
|
||||||
|
|
||||||
|
// Title sets the menu header
|
||||||
|
func (m *Menu) Title(t string) {
|
||||||
|
m.title = t
|
||||||
|
}
|
||||||
|
|
||||||
|
// Item adds a menu entry (key, display label, ANSI color, handler)
|
||||||
|
func (m *Menu) Item(key, label, color string, handler HandlerFunc) {
|
||||||
|
m.items = append(m.items, menuItem{
|
||||||
|
Key: strings.ToUpper(key),
|
||||||
|
Label: label,
|
||||||
|
Color: color,
|
||||||
|
Handler: handler,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatch finds and calls the handler for the given key
|
||||||
|
func (m *Menu) Dispatch(s *Session, key string) {
|
||||||
|
key = strings.ToUpper(strings.TrimSpace(key))
|
||||||
|
for _, item := range m.items {
|
||||||
|
if item.Key == key {
|
||||||
|
item.Handler(s)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render returns the rendered menu string
|
||||||
|
func (m *Menu) Render(p *Printer, lang T, username string) string {
|
||||||
|
headerLine := strings.Repeat("═", W)
|
||||||
|
l1 := p.PadLine(fmt.Sprintf(" %s%s%s %s@%s%s", YL, m.title, R, GY, username, R), W)
|
||||||
|
|
||||||
|
var rows []string
|
||||||
|
for i := 0; i < len(m.items); i += 2 {
|
||||||
|
if i+1 < len(m.items) {
|
||||||
|
left := p.PadLine(fmt.Sprintf(" %s[%s]%s %s", m.items[i].Color, m.items[i].Key, R, m.items[i].Label), W/2)
|
||||||
|
right := p.PadLine(fmt.Sprintf(" %s[%s]%s %s", m.items[i+1].Color, m.items[i+1].Key, R, m.items[i+1].Label), W/2)
|
||||||
|
rows = append(rows, left+right)
|
||||||
|
} else {
|
||||||
|
rows = append(rows, p.PadLine(fmt.Sprintf(" %s[%s]%s %s", m.items[i].Color, m.items[i].Key, R, m.items[i].Label), W))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(fmt.Sprintf("\n%s╔%s╗%s\n", WH, headerLine, R))
|
||||||
|
sb.WriteString(fmt.Sprintf("%s║%s%s║%s\n", WH, l1, WH, R))
|
||||||
|
sb.WriteString(fmt.Sprintf("%s╠%s╣%s\n", WH, headerLine, R))
|
||||||
|
for _, row := range rows {
|
||||||
|
sb.WriteString(fmt.Sprintf("%s║%s%s║%s\n", WH, row, WH, R))
|
||||||
|
}
|
||||||
|
sb.WriteString(fmt.Sprintf("%s╚%s╝%s\n%s", WH, headerLine, R, lang["Choice"]))
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package lib
|
package engine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ANSI Colors
|
// ANSI color codes
|
||||||
const (
|
const (
|
||||||
R = "\033[0m"
|
R = "\033[0m"
|
||||||
B = "\033[1m"
|
B = "\033[1m"
|
||||||
@@ -25,14 +25,9 @@ const (
|
|||||||
GY = "\033[0;37m"
|
GY = "\033[0;37m"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// W is the default terminal width
|
||||||
const W = 70
|
const W = 70
|
||||||
|
|
||||||
type Message struct {
|
|
||||||
User string
|
|
||||||
Timestamp string
|
|
||||||
Text string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Printer struct {
|
type Printer struct {
|
||||||
Conn net.Conn
|
Conn net.Conn
|
||||||
}
|
}
|
||||||
@@ -152,7 +147,7 @@ func (p *Printer) StripMD(text string) string {
|
|||||||
reImg := regexp.MustCompile(`!\[.*?\]\(.*?\)|\[(.*?)\]\(.*?\)|#{1,6}\s*|[*_` + "`" + `~>|]`)
|
reImg := regexp.MustCompile(`!\[.*?\]\(.*?\)|\[(.*?)\]\(.*?\)|#{1,6}\s*|[*_` + "`" + `~>|]`)
|
||||||
text = reImg.ReplaceAllStringFunc(text, func(s string) string {
|
text = reImg.ReplaceAllStringFunc(text, func(s string) string {
|
||||||
if strings.HasPrefix(s, "[") {
|
if strings.HasPrefix(s, "[") {
|
||||||
sub := regexp.MustCompile(`\[(.*?)\]\(.*?\)` )
|
sub := regexp.MustCompile(`\[(.*?)\]\(.*?\)`)
|
||||||
matches := sub.FindStringSubmatch(s)
|
matches := sub.FindStringSubmatch(s)
|
||||||
if len(matches) > 1 {
|
if len(matches) > 1 {
|
||||||
return matches[1]
|
return matches[1]
|
||||||
@@ -181,12 +176,12 @@ func (p *Printer) Wrapped(text string, indent int, maxChars int) string {
|
|||||||
lines = append(lines, "")
|
lines = append(lines, "")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
words := strings.Fields(para)
|
words := strings.Fields(para)
|
||||||
if len(words) == 0 {
|
if len(words) == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
currentLine := prefix + words[0]
|
currentLine := prefix + words[0]
|
||||||
for _, word := range words[1:] {
|
for _, word := range words[1:] {
|
||||||
if len(currentLine)+1+len(word) > W {
|
if len(currentLine)+1+len(word) > W {
|
||||||
123
engine/server.go
Normal file
123
engine/server.go
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds the BBS server configuration
|
||||||
|
type Config struct {
|
||||||
|
Host string
|
||||||
|
Port string
|
||||||
|
Banner string
|
||||||
|
Lang T
|
||||||
|
}
|
||||||
|
|
||||||
|
// BBS is the main server struct
|
||||||
|
type BBS struct {
|
||||||
|
config Config
|
||||||
|
state *State
|
||||||
|
menu *Menu
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new BBS server with the given configuration
|
||||||
|
func New(cfg Config) *BBS {
|
||||||
|
return &BBS{
|
||||||
|
config: cfg,
|
||||||
|
state: newState(),
|
||||||
|
menu: &Menu{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// State returns the shared server state (for use by content handlers)
|
||||||
|
func (b *BBS) State() *State {
|
||||||
|
return b.state
|
||||||
|
}
|
||||||
|
|
||||||
|
// Menu configures the main menu using the DSL
|
||||||
|
func (b *BBS) Menu(fn func(*Menu)) {
|
||||||
|
fn(b.menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start launches the TCP server and blocks waiting for connections
|
||||||
|
func (b *BBS) Start() {
|
||||||
|
ln, err := net.Listen("tcp", b.config.Host+":"+b.config.Port)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Server start error: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
fmt.Printf("BBS running → telnet localhost %s\n", b.config.Port)
|
||||||
|
fmt.Println("Stop: Ctrl+C")
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Accept error: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Printf("[+] Connected: %s\n", conn.RemoteAddr().String())
|
||||||
|
go b.handleClient(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BBS) handleClient(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
addr := conn.RemoteAddr().String()
|
||||||
|
lang := b.config.Lang
|
||||||
|
printer := NewPrinter(conn)
|
||||||
|
|
||||||
|
// Telnet negotiation (IAC WILL ECHO, IAC WILL SGA, IAC WONT LINEMODE)
|
||||||
|
printer.Send("\xff\xfb\x01\xff\xfb\x03\xff\xfe\x22")
|
||||||
|
|
||||||
|
printer.Send(CY + b.config.Banner + R)
|
||||||
|
printer.Send(fmt.Sprintf("\n%s%s%s ", WH, lang["AskName"], R))
|
||||||
|
|
||||||
|
username, err := printer.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
username = strings.TrimSpace(username)
|
||||||
|
if username == "" {
|
||||||
|
username = "Anonymous"
|
||||||
|
}
|
||||||
|
if len(username) > 20 {
|
||||||
|
username = username[:20]
|
||||||
|
}
|
||||||
|
|
||||||
|
b.state.Mu.Lock()
|
||||||
|
b.state.OnlineUsers[addr] = username
|
||||||
|
b.state.Mu.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
b.state.Mu.Lock()
|
||||||
|
delete(b.state.OnlineUsers, addr)
|
||||||
|
b.state.Mu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n", GR, fmt.Sprintf(lang["Greeting"], WH, username, GR), R))
|
||||||
|
|
||||||
|
session := &Session{
|
||||||
|
State: b.state,
|
||||||
|
Printer: printer,
|
||||||
|
Username: username,
|
||||||
|
Addr: addr,
|
||||||
|
Lang: lang,
|
||||||
|
}
|
||||||
|
|
||||||
|
for !session.Quit {
|
||||||
|
printer.Send(b.menu.Render(printer, lang, username))
|
||||||
|
choice, err := printer.ReadLine()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
b.menu.Dispatch(session, choice)
|
||||||
|
}
|
||||||
|
|
||||||
|
if session.Quit {
|
||||||
|
printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n\r\n", RD, fmt.Sprintf(lang["Goodbye"], username), R))
|
||||||
|
}
|
||||||
|
}
|
||||||
43
engine/session.go
Normal file
43
engine/session.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package engine
|
||||||
|
|
||||||
|
import "sync"
|
||||||
|
|
||||||
|
// State holds shared server state (thread-safe)
|
||||||
|
type State struct {
|
||||||
|
OnlineUsers map[string]string // addr -> username
|
||||||
|
Mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newState() *State {
|
||||||
|
return &State{
|
||||||
|
OnlineUsers: make(map[string]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserCount returns the number of online users in a thread-safe manner
|
||||||
|
func (st *State) UserCount() int {
|
||||||
|
st.Mu.Lock()
|
||||||
|
defer st.Mu.Unlock()
|
||||||
|
return len(st.OnlineUsers)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot returns a thread-safe copy of the online users map
|
||||||
|
func (st *State) Snapshot() map[string]string {
|
||||||
|
st.Mu.Lock()
|
||||||
|
defer st.Mu.Unlock()
|
||||||
|
snap := make(map[string]string)
|
||||||
|
for k, v := range st.OnlineUsers {
|
||||||
|
snap[k] = v
|
||||||
|
}
|
||||||
|
return snap
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session represents an active user connection
|
||||||
|
type Session struct {
|
||||||
|
State *State
|
||||||
|
Printer *Printer
|
||||||
|
Username string
|
||||||
|
Addr string
|
||||||
|
Lang T
|
||||||
|
Quit bool
|
||||||
|
}
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Session) ShowGames() {
|
|
||||||
s.Printer.BoxHeader(s.Lang["CatTitle"], YL)
|
|
||||||
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, s.Lang["WikiLoading"], R))
|
|
||||||
|
|
||||||
softwares, err := s.BBS.CatalogRepo.FetchGames()
|
|
||||||
if err != nil {
|
|
||||||
s.Printer.Send(fmt.Sprintf("\r\n%s%s: %v%s\r\n", RD, s.Lang["WikiConnError"], err, R))
|
|
||||||
s.Printer.Pause(s.Lang)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Printer.Send("\r\033[A\033[2K")
|
|
||||||
|
|
||||||
if len(softwares) == 0 {
|
|
||||||
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, s.Lang["CatNoGames"], R))
|
|
||||||
s.Printer.Pause(s.Lang)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
base := "https://games.teletype.hu"
|
|
||||||
|
|
||||||
for i, entry := range softwares {
|
|
||||||
sw := entry.Software
|
|
||||||
lr := entry.LatestRelease
|
|
||||||
|
|
||||||
s.Printer.Send(fmt.Sprintf("\r\n %s%s%s\r\n", YL, strings.Repeat("─", W-4), R))
|
|
||||||
s.Printer.Send(fmt.Sprintf(" %s%2d.%s %s%s%s%s %s[%s] by %s%s\r\n", YL, i+1, R, WH, B, sw.Title, R, GY, sw.Platform, sw.Author, R))
|
|
||||||
|
|
||||||
if sw.Desc != "" {
|
|
||||||
wrappedDesc := s.Printer.Wrapped(sw.Desc, 7, 1000)
|
|
||||||
for _, line := range strings.Split(wrappedDesc, "\r\n") {
|
|
||||||
s.Printer.Send(fmt.Sprintf("%s%s\r\n", GY, line))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if lr != nil {
|
|
||||||
badges := []string{}
|
|
||||||
if lr.HTMLFolderPath != "" {
|
|
||||||
badges = append(badges, fmt.Sprintf("%s[▶ Play]%s", GR, R))
|
|
||||||
}
|
|
||||||
if lr.CartridgePath != "" {
|
|
||||||
badges = append(badges, fmt.Sprintf("%s[⬇ Download]%s", BL, R))
|
|
||||||
}
|
|
||||||
if lr.SourcePath != "" {
|
|
||||||
badges = append(badges, fmt.Sprintf("%s[Source]%s", MG, R))
|
|
||||||
}
|
|
||||||
if lr.DocsFolderPath != "" {
|
|
||||||
badges = append(badges, fmt.Sprintf("%s[Docs]%s", YL, R))
|
|
||||||
}
|
|
||||||
|
|
||||||
badgeStr := "–"
|
|
||||||
if len(badges) > 0 {
|
|
||||||
badgeStr = strings.Join(badges, " ")
|
|
||||||
}
|
|
||||||
s.Printer.Send(fmt.Sprintf(" %s%s: v%s%s %s\r\n", GY, s.Lang["CatLatest"], lr.Version, R, badgeStr))
|
|
||||||
|
|
||||||
if lr.HTMLFolderPath != "" {
|
|
||||||
url := base + lr.HTMLFolderPath
|
|
||||||
s.Printer.Send(fmt.Sprintf(" %s▶ %s%s\r\n", DIM, url, R))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, fmt.Sprintf(s.Lang["CatVersions"], len(entry.Releases)), R))
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Printer.HR("─", YL)
|
|
||||||
s.Printer.Send("\r\n")
|
|
||||||
s.Printer.Send(fmt.Sprintf(" %s%s: %s%s\r\n", GY, s.Lang["CatFull"], base, R))
|
|
||||||
s.Printer.Pause(s.Lang)
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sort"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Session) ShowOnline() {
|
|
||||||
s.Printer.BoxHeader(s.Lang["OnlineTitle"], CY)
|
|
||||||
s.BBS.Mu.Lock()
|
|
||||||
snap := make(map[string]string)
|
|
||||||
for k, v := range s.BBS.OnlineUsers {
|
|
||||||
snap[k] = v
|
|
||||||
}
|
|
||||||
s.BBS.Mu.Unlock()
|
|
||||||
|
|
||||||
keys := make([]string, 0, len(snap))
|
|
||||||
for k := range snap {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
|
|
||||||
for _, addr := range keys {
|
|
||||||
user := snap[addr]
|
|
||||||
marker := ""
|
|
||||||
if addr == s.Addr {
|
|
||||||
marker = fmt.Sprintf(" %s%s%s", GR, s.Lang["OnlineYou"], R)
|
|
||||||
}
|
|
||||||
s.Printer.Send(fmt.Sprintf(" %s•%s %s%s%s%s\r\n", CY, R, WH, user, R, marker))
|
|
||||||
}
|
|
||||||
s.Printer.Send(fmt.Sprintf("\r\n %s%s\r\n", GY, fmt.Sprintf(s.Lang["OnlineTotal"], WH, len(snap), GY, R)))
|
|
||||||
s.Printer.Pause(s.Lang)
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Session) ShowSysinfo() {
|
|
||||||
s.Printer.BoxHeader(s.Lang["SysInfoTitle"], GY)
|
|
||||||
s.BBS.Mu.Lock()
|
|
||||||
uc := len(s.BBS.OnlineUsers)
|
|
||||||
mc := len(s.BBS.Messages)
|
|
||||||
s.BBS.Mu.Unlock()
|
|
||||||
|
|
||||||
now := time.Now().Format("2006-01-02 15:04:05")
|
|
||||||
|
|
||||||
rows := [][]string{
|
|
||||||
{s.Lang["SysServerTime"], now},
|
|
||||||
{s.Lang["SysOnlineUsers"], strconv.Itoa(uc)},
|
|
||||||
{s.Lang["SysMsgCount"], strconv.Itoa(mc)},
|
|
||||||
{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", GY, row[0], R, WH, row[1], R))
|
|
||||||
}
|
|
||||||
s.Printer.Pause(s.Lang)
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Session) ShowUzenopal() {
|
|
||||||
s.Printer.BoxHeader(s.Lang["MsgBoardTitle"], GR)
|
|
||||||
s.BBS.Mu.Lock()
|
|
||||||
snap := make([]Message, len(s.BBS.Messages))
|
|
||||||
copy(snap, s.BBS.Messages)
|
|
||||||
s.BBS.Mu.Unlock()
|
|
||||||
|
|
||||||
if len(snap) == 0 {
|
|
||||||
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, s.Lang["MsgNoMessages"], 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", GR, i+1, R, GY, msg.Timestamp, R, WH, msg.User, R, msg.Text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Printer.Send(fmt.Sprintf("\r\n%s[N]%s %s %s[ENTER]%s %s → ", GY, R, s.Lang["MsgNew"], GY, 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 ", WH, s.Lang["MsgEnterText"], 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")
|
|
||||||
s.BBS.Mu.Lock()
|
|
||||||
s.BBS.Messages = append(s.BBS.Messages, Message{User: s.Username, Timestamp: ts, Text: msgText})
|
|
||||||
s.BBS.Mu.Unlock()
|
|
||||||
s.Printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n", GR, s.Lang["MsgSent"], R))
|
|
||||||
} else {
|
|
||||||
s.Printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n", GY, s.Lang["MsgEmpty"], R))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (s *Session) ShowWikiList(tag string, color string, title string) {
|
|
||||||
s.Printer.BoxHeader(title, color)
|
|
||||||
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, s.Lang["WikiLoading"], R))
|
|
||||||
|
|
||||||
pages, err := s.BBS.WikiRepo.FetchList(tag)
|
|
||||||
if err != nil {
|
|
||||||
s.Printer.Send(fmt.Sprintf("\r\n%s%s: %v%s\r\n", RD, s.Lang["WikiConnError"], err, 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", GY, fmt.Sprintf(s.Lang["WikiNoResults"], tag), 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, R, WH, titleP, R))
|
|
||||||
if desc != "" {
|
|
||||||
s.Printer.Send(fmt.Sprintf(" %s%s %s%s%s\r\n", GY, date, DIM, desc, R))
|
|
||||||
} else {
|
|
||||||
s.Printer.Send(fmt.Sprintf(" %s%s%s\r\n", GY, date, R))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Printer.Send(fmt.Sprintf("\r\n%s%s%s ", GY, s.Lang["WikiEnterNum"], 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", GY, s.Lang["WikiFetchContent"], R))
|
|
||||||
content, err := s.BBS.WikiRepo.FetchContent(page.ID)
|
|
||||||
if err != nil {
|
|
||||||
s.Printer.Send(fmt.Sprintf("\r\n%sHiba: %v%s\r\n", RD, err, R))
|
|
||||||
s.Printer.Pause(s.Lang)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Printer.Send("\r\033[A\033[2K")
|
|
||||||
s.Printer.Send(fmt.Sprintf("\r\n"))
|
|
||||||
s.Printer.HR("═", color)
|
|
||||||
s.Printer.Send("\r\n")
|
|
||||||
s.Printer.Send(fmt.Sprintf(" %s%s%s%s\r\n", WH, B, page.Title, R))
|
|
||||||
url := fmt.Sprintf("%s/%s/%s", WikiJSBaseURL, page.Locale, page.Path)
|
|
||||||
s.Printer.Send(fmt.Sprintf(" %s%s %s%s%s\r\n", GY, s.Printer.FmtDate(page.CreatedAt), DIM, url, R))
|
|
||||||
s.Printer.HR("─", GY)
|
|
||||||
s.Printer.Send("\r\n\r\n")
|
|
||||||
|
|
||||||
body := s.Printer.Wrapped(content, 2, 5000)
|
|
||||||
for _, line := range strings.Split(body, "\r\n") {
|
|
||||||
s.Printer.Send(line+"\r\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.Printer.Send("\r\n")
|
|
||||||
s.Printer.HR("─", GY)
|
|
||||||
s.Printer.Send("\r\n")
|
|
||||||
s.Printer.Pause(s.Lang)
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package lib
|
|
||||||
|
|
||||||
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 NewCatalogRepository() *CatalogRepository {
|
|
||||||
return &CatalogRepository{}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
package lib
|
|
||||||
|
|
||||||
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 NewWikiRepository(token string) *WikiRepository {
|
|
||||||
return &WikiRepository{Token: token}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
package lib
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
const Banner = `
|
|
||||||
████████╗███████╗██╗ ███████╗████████╗██╗ ██╗██████╗ ███████╗
|
|
||||||
██╔══╝██╔════╝██║ ██╔════╝╚══██╔══╝╚██╗ ██╔╝██╔══██╗██╔════╝
|
|
||||||
██║ █████╗ ██║ █████╗ ██║ ╚████╔╝ ██████╔╝█████╗
|
|
||||||
██║ ██╔══╝ ██║ ██╔══╝ ██║ ╚██╔╝ ██╔═══╝ ██╔══╝
|
|
||||||
██║ ███████╗███████╗███████╗ ██║ ██║ ██║ ███████╗
|
|
||||||
╚═╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝
|
|
||||||
|
|
||||||
██████╗ █████╗ ███╗ ███╗███████╗███████╗
|
|
||||||
██╔════╝ ██╔══██╗████╗ ████║██╔════╝██╔════╝
|
|
||||||
██║ ███╗███████║██╔████╔██║█████╗ ███████╗
|
|
||||||
██║ ██║██╔══██║██║╚██╔╝██║██╔══╝ ╚════██║
|
|
||||||
╚██████╔╝██║ ██║██║ ╚═╝ ██║███████╗███████║
|
|
||||||
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝
|
|
||||||
|
|
||||||
░░ BBS v2.0 ░░ teletype.hu ░░
|
|
||||||
Welcome to the Teletype community bulletin board!
|
|
||||||
`
|
|
||||||
|
|
||||||
type BBS struct {
|
|
||||||
Messages []Message
|
|
||||||
OnlineUsers map[string]string // addr -> username
|
|
||||||
Mu sync.Mutex
|
|
||||||
WikiToken string
|
|
||||||
WikiRepo *WikiRepository
|
|
||||||
CatalogRepo *CatalogRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewBBS(wikiToken string) *BBS {
|
|
||||||
return &BBS{
|
|
||||||
Messages: []Message{},
|
|
||||||
OnlineUsers: make(map[string]string),
|
|
||||||
WikiToken: wikiToken,
|
|
||||||
WikiRepo: NewWikiRepository(wikiToken),
|
|
||||||
CatalogRepo: NewCatalogRepository(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type UI struct {
|
|
||||||
Printer *Printer
|
|
||||||
Lang T
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUI(printer *Printer, lang T) *UI {
|
|
||||||
return &UI{Printer: printer, Lang: lang}
|
|
||||||
}
|
|
||||||
|
|
||||||
type Session struct {
|
|
||||||
BBS *BBS
|
|
||||||
Printer *Printer
|
|
||||||
Username string
|
|
||||||
Addr string
|
|
||||||
Lang T
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewSession(bbs *BBS, printer *Printer, username string, addr string, lang T) *Session {
|
|
||||||
return &Session{
|
|
||||||
BBS: bbs,
|
|
||||||
Printer: printer,
|
|
||||||
Username: username,
|
|
||||||
Addr: addr,
|
|
||||||
Lang: lang,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ui *UI) MainMenu(username string) string {
|
|
||||||
headerLine := strings.Repeat("═", W)
|
|
||||||
|
|
||||||
l1 := ui.Printer.PadLine(fmt.Sprintf(" %s%s%s %s@%s%s", YL, ui.Lang["MainMenuTitle"], R, GY, username, R), W)
|
|
||||||
l2 := ui.Printer.PadLine(fmt.Sprintf(" %s[1]%s %s", GR, R, ui.Lang["MenuUzenopal"]), W/2) + ui.Printer.PadLine(fmt.Sprintf(" %s[2]%s %s", BL, R, ui.Lang["MenuBlog"]), W/2)
|
|
||||||
l3 := ui.Printer.PadLine(fmt.Sprintf(" %s[3]%s %s", MG, R, ui.Lang["MenuHowto"]), W/2) + ui.Printer.PadLine(fmt.Sprintf(" %s[4]%s %s", YL, R, ui.Lang["MenuCatalog"]), W/2)
|
|
||||||
l4 := ui.Printer.PadLine(fmt.Sprintf(" %s[5]%s %s", CY, R, ui.Lang["MenuOnline"]), W/2) + ui.Printer.PadLine(fmt.Sprintf(" %s[6]%s %s", GY, R, ui.Lang["MenuSysinfo"]), W/2)
|
|
||||||
l5 := ui.Printer.PadLine(fmt.Sprintf(" %s[Q]%s %s", RD, R, ui.Lang["MenuExit"]), W)
|
|
||||||
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"\n%s╔%s╗%s\n"+
|
|
||||||
"%s║%s%s║%s\n"+
|
|
||||||
"%s╠%s╣%s\n"+
|
|
||||||
"%s║%s%s║%s\n"+
|
|
||||||
"%s║%s%s║%s\n"+
|
|
||||||
"%s║%s%s║%s\n"+
|
|
||||||
"%s║%s%s║%s\n"+
|
|
||||||
"%s╚%s╝%s\n%s",
|
|
||||||
WH, headerLine, R,
|
|
||||||
WH, l1, WH, R,
|
|
||||||
WH, headerLine, R,
|
|
||||||
WH, l2, WH, R,
|
|
||||||
WH, l3, WH, R,
|
|
||||||
WH, l4, WH, R,
|
|
||||||
WH, l5, WH, R,
|
|
||||||
WH, headerLine, R,
|
|
||||||
ui.Lang["Choice"],
|
|
||||||
)
|
|
||||||
}
|
|
||||||
144
main.go
144
main.go
@@ -1,116 +1,68 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bbs-server/lib"
|
"bbs-server/content"
|
||||||
|
"bbs-server/engine"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const banner = `
|
||||||
Host = "0.0.0.0"
|
████████╗███████╗██╗ ███████╗████████╗██╗ ██╗██████╗ ███████╗
|
||||||
Port = "2323"
|
██╔══╝██╔════╝██║ ██╔════╝╚══██╔══╝╚██╗ ██╔╝██╔══██╗██╔════╝
|
||||||
)
|
██║ █████╗ ██║ █████╗ ██║ ╚████╔╝ ██████╔╝█████╗
|
||||||
|
██║ ██╔══╝ ██║ ██╔══╝ ██║ ╚██╔╝ ██╔═══╝ ██╔══╝
|
||||||
|
██║ ███████╗███████╗███████╗ ██║ ██║ ██║ ███████╗
|
||||||
|
╚═╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝
|
||||||
|
|
||||||
var bbs *lib.BBS
|
██████╗ █████╗ ███╗ ███╗███████╗███████╗
|
||||||
|
██╔════╝ ██╔══██╗████╗ ████║██╔════╝██╔════╝
|
||||||
|
██║ ███╗███████║██╔████╔██║█████╗ ███████╗
|
||||||
|
██║ ██║██╔══██║██║╚██╔╝██║██╔══╝ ╚════██║
|
||||||
|
╚██████╔╝██║ ██║██║ ╚═╝ ██║███████╗███████║
|
||||||
|
╚═════╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝
|
||||||
|
|
||||||
|
░░ BBS v2.0 ░░ teletype.hu ░░
|
||||||
|
Welcome to the Teletype community bulletin board!
|
||||||
|
`
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
wikiToken := os.Getenv("WEBAPP_WIKIJS_TOKEN")
|
wikiToken := os.Getenv("WEBAPP_WIKIJS_TOKEN")
|
||||||
bbs = lib.NewBBS(wikiToken)
|
|
||||||
|
|
||||||
ln, err := net.Listen("tcp", Host+":"+Port)
|
boardPath := os.Getenv("MESSAGES_PATH")
|
||||||
if err != nil {
|
if boardPath == "" {
|
||||||
fmt.Printf("Hiba a szerver indításakor: %v\n", err)
|
boardPath = "messages.dat"
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
defer ln.Close()
|
|
||||||
|
|
||||||
fmt.Printf("Teletype BBS running → telnet localhost %s\n", Port)
|
|
||||||
fmt.Printf("Wiki: %s\n", lib.WikiJSBaseURL)
|
|
||||||
fmt.Printf("Games API: %s\n", lib.GamesAPIURL)
|
|
||||||
tokenStatus := "✗ not set"
|
tokenStatus := "✗ not set"
|
||||||
if wikiToken != "" {
|
if wikiToken != "" {
|
||||||
tokenStatus = "✓ set"
|
tokenStatus = "✓ set"
|
||||||
}
|
}
|
||||||
fmt.Printf("Token: %s\n", tokenStatus)
|
fmt.Printf("Wiki: %s\n", content.WikiJSBaseURL)
|
||||||
fmt.Println("Stop: Ctrl+C")
|
fmt.Printf("Games API: %s\n", content.GamesAPIURL)
|
||||||
|
fmt.Printf("Token: %s\n", tokenStatus)
|
||||||
|
|
||||||
for {
|
wiki := content.NewWikiHandler(wikiToken)
|
||||||
conn, err := ln.Accept()
|
cat := content.NewCatalogHandler()
|
||||||
if err != nil {
|
board := content.NewMessageBoard(boardPath)
|
||||||
fmt.Printf("Error accepting connection: %v\n", err)
|
|
||||||
continue
|
bbs := engine.New(engine.Config{
|
||||||
}
|
Host: "0.0.0.0",
|
||||||
fmt.Printf("[+] Connected: %s\n", conn.RemoteAddr().String())
|
Port: "2323",
|
||||||
go handleClient(conn)
|
Banner: banner,
|
||||||
}
|
Lang: engine.En,
|
||||||
}
|
})
|
||||||
|
|
||||||
func handleClient(conn net.Conn) {
|
bbs.Menu(func(m *engine.Menu) {
|
||||||
defer conn.Close()
|
m.Title("MAIN MENU")
|
||||||
addr := conn.RemoteAddr().String()
|
m.Item("1", "Message Board", engine.GR, board.Show)
|
||||||
|
m.Item("2", "Blog Posts", engine.BL, wiki.List("blog", "Blog Posts", engine.BL))
|
||||||
printer := lib.NewPrinter(conn)
|
m.Item("3", "HowTo Guides", engine.MG, wiki.List("howto", "HowTo Guides", engine.MG))
|
||||||
lang := lib.En
|
m.Item("4", "Game Catalog", engine.YL, cat.Show)
|
||||||
ui := lib.NewUI(printer, lang)
|
m.Item("5", "Online Users", engine.CY, content.Online)
|
||||||
|
m.Item("6", "System Info", engine.GY, content.Sysinfo(board))
|
||||||
// Telnet negotiation
|
m.Item("Q", "Exit", engine.RD, engine.Exit)
|
||||||
printer.Send("\xff\xfb\x01\xff\xfb\x03\xff\xfe\x22")
|
})
|
||||||
|
|
||||||
printer.Send(lib.CY + lib.Banner + lib.R)
|
bbs.Start()
|
||||||
printer.Send(fmt.Sprintf("\n%s%s%s ", lib.WH, lang["AskName"], lib.R))
|
|
||||||
|
|
||||||
username, err := printer.ReadLine()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
username = strings.TrimSpace(username)
|
|
||||||
if username == "" {
|
|
||||||
username = "Anonymous"
|
|
||||||
}
|
|
||||||
if len(username) > 20 {
|
|
||||||
username = username[:20]
|
|
||||||
}
|
|
||||||
|
|
||||||
bbs.Mu.Lock()
|
|
||||||
bbs.OnlineUsers[addr] = username
|
|
||||||
bbs.Mu.Unlock()
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
bbs.Mu.Lock()
|
|
||||||
delete(bbs.OnlineUsers, addr)
|
|
||||||
bbs.Mu.Unlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n", lib.GR, fmt.Sprintf(lang["Greeting"], lib.WH, username, lib.GR), lib.R))
|
|
||||||
|
|
||||||
session := lib.NewSession(bbs, printer, username, addr, lang)
|
|
||||||
|
|
||||||
for {
|
|
||||||
printer.Send(ui.MainMenu(username))
|
|
||||||
choice, err := printer.ReadLine()
|
|
||||||
if err != nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
c := strings.ToUpper(strings.TrimSpace(choice))
|
|
||||||
|
|
||||||
switch c {
|
|
||||||
case "1":
|
|
||||||
session.ShowUzenopal()
|
|
||||||
case "2":
|
|
||||||
session.ShowWikiList("blog", lib.BL, lang["MenuBlog"])
|
|
||||||
case "3":
|
|
||||||
session.ShowWikiList("howto", lib.MG, lang["MenuHowto"])
|
|
||||||
case "4":
|
|
||||||
session.ShowGames()
|
|
||||||
case "5":
|
|
||||||
session.ShowOnline()
|
|
||||||
case "6":
|
|
||||||
session.ShowSysinfo()
|
|
||||||
case "Q":
|
|
||||||
printer.Send(fmt.Sprintf("\r\n%s%s%s\r\n\r\n", lib.RD, fmt.Sprintf(lang["Goodbye"], username), lib.R))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user