initial commit
This commit is contained in:
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
.env
|
||||||
8
Dockerfile.development
Normal file
8
Dockerfile.development
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
FROM --platform=linux/amd64 golang:1.26.1-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
|
||||||
|
EXPOSE 2323
|
||||||
|
|
||||||
|
CMD ["go", "run", "main.go"]
|
||||||
15
docker-compose.yaml
Normal file
15
docker-compose.yaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
services:
|
||||||
|
bbs:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile.development
|
||||||
|
container_name: teletype-bbs
|
||||||
|
ports:
|
||||||
|
- "2323:2323"
|
||||||
|
volumes:
|
||||||
|
- ./:/app/:ro
|
||||||
|
environment:
|
||||||
|
- WEBAPP_WIKIJS_TOKEN=${WEBAPP_WIKIJS_TOKEN:-}
|
||||||
|
restart: unless-stopped
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
1
env-example
Normal file
1
env-example
Normal file
@@ -0,0 +1 @@
|
|||||||
|
WEBAPP_WIKIJS_TOKEN=
|
||||||
730
main.go
Normal file
730
main.go
Normal file
@@ -0,0 +1,730 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
"unicode/utf8"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
Host = "0.0.0.0"
|
||||||
|
Port = "2323"
|
||||||
|
WikiJSBaseURL = "https://wiki.teletype.hu"
|
||||||
|
GamesAPIURL = "https://games.teletype.hu/api/software"
|
||||||
|
W = 70
|
||||||
|
)
|
||||||
|
|
||||||
|
// ANSI Colors
|
||||||
|
const (
|
||||||
|
R = "\033[0m"
|
||||||
|
B = "\033[1m"
|
||||||
|
DIM = "\033[2m"
|
||||||
|
CY = "\033[1;36m"
|
||||||
|
YL = "\033[1;33m"
|
||||||
|
GR = "\033[1;32m"
|
||||||
|
RD = "\033[1;31m"
|
||||||
|
MG = "\033[1;35m"
|
||||||
|
WH = "\033[1;37m"
|
||||||
|
BL = "\033[1;34m"
|
||||||
|
GY = "\033[0;37m"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Message struct {
|
||||||
|
User string
|
||||||
|
Timestamp string
|
||||||
|
Text string
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
messages []Message
|
||||||
|
onlineUsers = make(map[string]string) // addr -> username
|
||||||
|
mu sync.Mutex
|
||||||
|
wikiToken = os.Getenv("WEBAPP_WIKIJS_TOKEN")
|
||||||
|
)
|
||||||
|
|
||||||
|
const Banner = `
|
||||||
|
████████╗███████╗██╗ ███████╗████████╗██╗ ██╗██████╗ ███████╗
|
||||||
|
██╔══╝██╔════╝██║ ██╔════╝╚══██╔══╝╚██╗ ██╔╝██╔══██╗██╔════╝
|
||||||
|
██║ █████╗ ██║ █████╗ ██║ ╚████╔╝ ██████╔╝█████╗
|
||||||
|
██║ ██╔══╝ ██║ ██╔══╝ ██║ ╚██╔╝ ██╔═══╝ ██╔══╝
|
||||||
|
██║ ███████╗███████╗███████╗ ██║ ██║ ██║ ███████╗
|
||||||
|
╚═╝ ╚══════╝╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝
|
||||||
|
|
||||||
|
░░ BBS v2.0 ░░ teletype.hu ░░
|
||||||
|
Üdvözölünk a Teletype közösségi hirdetőtábláján!
|
||||||
|
`
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ln, err := net.Listen("tcp", Host+":"+Port)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Hiba a szerver indításakor: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer ln.Close()
|
||||||
|
|
||||||
|
fmt.Printf("Teletype BBS fut → telnet localhost %s\n", Port)
|
||||||
|
fmt.Printf("Wiki: %s\n", WikiJSBaseURL)
|
||||||
|
fmt.Printf("Games API: %s\n", GamesAPIURL)
|
||||||
|
tokenStatus := "✗ nincs beállítva"
|
||||||
|
if wikiToken != "" {
|
||||||
|
tokenStatus = "✓ beállítva"
|
||||||
|
}
|
||||||
|
fmt.Printf("Token: %s\n", tokenStatus)
|
||||||
|
fmt.Println("Leállítás: Ctrl+C")
|
||||||
|
|
||||||
|
for {
|
||||||
|
conn, err := ln.Accept()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Hiba a kapcsolódáskor: %v\n", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fmt.Printf("[+] Kapcsolódott: %s\n", conn.RemoteAddr().String())
|
||||||
|
go handleClient(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleClient(conn net.Conn) {
|
||||||
|
defer conn.Close()
|
||||||
|
addr := conn.RemoteAddr().String()
|
||||||
|
|
||||||
|
// Telnet negotiation:
|
||||||
|
// IAC WILL ECHO (255, 251, 1)
|
||||||
|
// IAC WILL SUPPRESS GO AHEAD (255, 251, 3)
|
||||||
|
// IAC DONT LINEMODE (255, 254, 34)
|
||||||
|
send(conn, "\xff\xfb\x01\xff\xfb\x03\xff\xfe\x22")
|
||||||
|
|
||||||
|
send(conn, CY+Banner+R)
|
||||||
|
send(conn, fmt.Sprintf("\n%sAdd meg a nevedet:%s ", WH, R))
|
||||||
|
|
||||||
|
username, err := readLine(conn)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
username = strings.TrimSpace(username)
|
||||||
|
if username == "" {
|
||||||
|
username = "Névtelen"
|
||||||
|
}
|
||||||
|
if len(username) > 20 {
|
||||||
|
username = username[:20]
|
||||||
|
}
|
||||||
|
|
||||||
|
mu.Lock()
|
||||||
|
onlineUsers[addr] = username
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
mu.Lock()
|
||||||
|
delete(onlineUsers, addr)
|
||||||
|
mu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
send(conn, fmt.Sprintf("\r\n%sSzia, %s%s%s! Üdv a Teletype BBS-en!%s\r\n", GR, WH, username, GR, R))
|
||||||
|
|
||||||
|
for {
|
||||||
|
send(conn, mainMenu(username))
|
||||||
|
choice, err := readLine(conn)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
c := strings.ToUpper(strings.TrimSpace(choice))
|
||||||
|
|
||||||
|
switch c {
|
||||||
|
case "1":
|
||||||
|
showUzenopal(conn, username)
|
||||||
|
case "2":
|
||||||
|
showWikiList(conn, "blog", BL, "📰 BLOG BEJEGYZÉSEK")
|
||||||
|
case "3":
|
||||||
|
showWikiList(conn, "howto", MG, "📖 HOWTO ÚTMUTATÓK")
|
||||||
|
case "4":
|
||||||
|
showGames(conn)
|
||||||
|
case "5":
|
||||||
|
showOnline(conn, addr)
|
||||||
|
case "6":
|
||||||
|
showSysinfo(conn)
|
||||||
|
case "Q":
|
||||||
|
send(conn, fmt.Sprintf("\r\n%sViszlát, %s! 👋%s\r\n\r\n", RD, username, R))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func visibleLen(s string) int {
|
||||||
|
re := regexp.MustCompile(`\033\[[0-9;]*m`)
|
||||||
|
return utf8.RuneCountInString(re.ReplaceAllString(s, ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func padLine(content string, width int) string {
|
||||||
|
vLen := visibleLen(content)
|
||||||
|
padding := width - vLen
|
||||||
|
if padding < 0 {
|
||||||
|
padding = 0
|
||||||
|
}
|
||||||
|
return content + strings.Repeat(" ", padding)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mainMenu(username string) string {
|
||||||
|
headerLine := strings.Repeat("═", W)
|
||||||
|
|
||||||
|
// Sorok összeállítása pontos hosszal
|
||||||
|
l1 := padLine(fmt.Sprintf(" %sFŐMENÜ%s %s@%s%s", YL, R, GY, username, R), W)
|
||||||
|
l2 := padLine(fmt.Sprintf(" %s[1]%s Üzenőfal", GR, R), W/2) + padLine(fmt.Sprintf(" %s[2]%s Blog bejegyzések", BL, R), W/2)
|
||||||
|
l3 := padLine(fmt.Sprintf(" %s[3]%s HowTo útmutatók", MG, R), W/2) + padLine(fmt.Sprintf(" %s[4]%s Játékkatalógus", YL, R), W/2)
|
||||||
|
l4 := padLine(fmt.Sprintf(" %s[5]%s Online felhasználók", CY, R), W/2) + padLine(fmt.Sprintf(" %s[6]%s Rendszerinfo", GY, R), W/2)
|
||||||
|
l5 := padLine(fmt.Sprintf(" %s[Q]%s Kilépés", RD, R), 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\nVálasztá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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Telnet I/O ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func send(conn net.Conn, text string) {
|
||||||
|
// Minden \n-t \r\n-re cserélünk, ha még nem az, hogy Telnet-en ne essen szét az ASCII art
|
||||||
|
normalized := strings.ReplaceAll(text, "\r\n", "\n")
|
||||||
|
normalized = strings.ReplaceAll(normalized, "\n", "\r\n")
|
||||||
|
conn.Write([]byte(normalized))
|
||||||
|
}
|
||||||
|
|
||||||
|
func readLine(conn net.Conn) (string, error) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for {
|
||||||
|
b := make([]byte, 1)
|
||||||
|
_, err := conn.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
ch := b[0]
|
||||||
|
|
||||||
|
// Telnet IAC (Interpret As Command)
|
||||||
|
if ch == 255 {
|
||||||
|
cmd := make([]byte, 2)
|
||||||
|
_, err := conn.Read(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if cmd[0] == 250 { // SB
|
||||||
|
for {
|
||||||
|
tmp := make([]byte, 1)
|
||||||
|
_, err := conn.Read(tmp)
|
||||||
|
if err != nil || tmp[0] == 240 { // SE
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enter billentyű (Telnetnél \r\n vagy csak \r)
|
||||||
|
if ch == '\r' || ch == '\n' {
|
||||||
|
res := buf.String()
|
||||||
|
send(conn, "\r\n") // Visszhangozzuk a sortörést
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backspace (8=BS, 127=DEL)
|
||||||
|
if ch == 8 || ch == 127 {
|
||||||
|
if buf.Len() > 0 {
|
||||||
|
curr := buf.Bytes()
|
||||||
|
buf.Reset()
|
||||||
|
buf.Write(curr[:len(curr)-1])
|
||||||
|
send(conn, "\b \b")
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Csak a látható karaktereket gyűjtjük
|
||||||
|
if ch >= 32 && ch < 127 {
|
||||||
|
buf.WriteByte(ch)
|
||||||
|
send(conn, string(ch))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pause(conn net.Conn) {
|
||||||
|
send(conn, fmt.Sprintf("\r\n%s [ Nyomj ENTER-t... ]%s ", GY, R))
|
||||||
|
readLine(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── API hívások ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func 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 wikiToken != "" {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+wikiToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchWikiList(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 := 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 fetchWikiContent(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 := 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
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Szöveg segédek ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func boxHeader(title string, color string) string {
|
||||||
|
line := strings.Repeat("═", W)
|
||||||
|
titleLen := utf8.RuneCountInString(title)
|
||||||
|
padding := (W - 2 - titleLen) / 2
|
||||||
|
if padding < 0 {
|
||||||
|
padding = 0
|
||||||
|
}
|
||||||
|
inner := strings.Repeat(" ", padding) + title + strings.Repeat(" ", W-2-titleLen-padding)
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"\n%s%s%s\n%s║%s %s%s%s %s║%s\n%s%s%s\n",
|
||||||
|
color, line, R,
|
||||||
|
color, R, B, inner, R, color, R,
|
||||||
|
color, line, R,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hr(char string, color string) string {
|
||||||
|
return fmt.Sprintf("%s%s%s", color, strings.Repeat(char, W), R)
|
||||||
|
}
|
||||||
|
|
||||||
|
func fmtDate(s string) string {
|
||||||
|
t, err := time.Parse(time.RFC3339, strings.Replace(s, "Z", "+00:00", 1))
|
||||||
|
if err != nil {
|
||||||
|
if len(s) >= 10 {
|
||||||
|
return s[:10]
|
||||||
|
}
|
||||||
|
return "–"
|
||||||
|
}
|
||||||
|
return t.Format("2006-01-02")
|
||||||
|
}
|
||||||
|
|
||||||
|
func stripMD(text string) string {
|
||||||
|
reImg := regexp.MustCompile(`!\[.*?\]\(.*?\)|\[(.*?)\]\(.*?\)|#{1,6}\s*|[*_` + "`" + `~>|]`)
|
||||||
|
text = reImg.ReplaceAllStringFunc(text, func(s string) string {
|
||||||
|
if strings.HasPrefix(s, "[") {
|
||||||
|
sub := regexp.MustCompile(`\[(.*?)\]\(.*?\)` )
|
||||||
|
matches := sub.FindStringSubmatch(s)
|
||||||
|
if len(matches) > 1 {
|
||||||
|
return matches[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(s, "!") || strings.HasPrefix(s, "#") || strings.ContainsAny(s, "*_`~>|") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
})
|
||||||
|
return strings.TrimSpace(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
func wrapped(text string, indent int, maxChars int) string {
|
||||||
|
if len(text) > maxChars {
|
||||||
|
text = text[:maxChars]
|
||||||
|
}
|
||||||
|
text = stripMD(text)
|
||||||
|
prefix := strings.Repeat(" ", indent)
|
||||||
|
var lines []string
|
||||||
|
|
||||||
|
paras := strings.Split(text, "\n")
|
||||||
|
for _, para := range paras {
|
||||||
|
para = strings.TrimSpace(para)
|
||||||
|
if para == "" {
|
||||||
|
lines = append(lines, "")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
words := strings.Fields(para)
|
||||||
|
if len(words) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLine := prefix + words[0]
|
||||||
|
for _, word := range words[1:] {
|
||||||
|
if len(currentLine)+1+len(word) > W {
|
||||||
|
lines = append(lines, currentLine)
|
||||||
|
currentLine = prefix + word
|
||||||
|
} else {
|
||||||
|
currentLine += " " + word
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lines = append(lines, currentLine)
|
||||||
|
}
|
||||||
|
return strings.Join(lines, "\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Menük ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func showUzenopal(conn net.Conn, username string) {
|
||||||
|
send(conn, boxHeader("📋 ÜZENŐFAL", GR))
|
||||||
|
mu.Lock()
|
||||||
|
snap := make([]Message, len(messages))
|
||||||
|
copy(snap, messages)
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
if len(snap) == 0 {
|
||||||
|
send(conn, fmt.Sprintf(" %s(Még nincs üzenet — legyél az első!)%s\r\n", GY, R))
|
||||||
|
} else {
|
||||||
|
start := 0
|
||||||
|
if len(snap) > 30 {
|
||||||
|
start = len(snap) - 30
|
||||||
|
}
|
||||||
|
for i, msg := range snap[start:] {
|
||||||
|
send(conn, 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send(conn, fmt.Sprintf("\r\n%s[N]%s Új üzenet írása %s[ENTER]%s Vissza → ", GY, R, GY, R))
|
||||||
|
choice, _ := readLine(conn)
|
||||||
|
if strings.ToUpper(strings.TrimSpace(choice)) == "N" {
|
||||||
|
send(conn, fmt.Sprintf("\r\n%sÜzenet szövege:%s ", WH, R))
|
||||||
|
msgText, _ := readLine(conn)
|
||||||
|
msgText = strings.TrimSpace(msgText)
|
||||||
|
if msgText != "" {
|
||||||
|
if len(msgText) > 200 {
|
||||||
|
msgText = msgText[:200]
|
||||||
|
}
|
||||||
|
ts := time.Now().Format("01-02 15:04")
|
||||||
|
mu.Lock()
|
||||||
|
messages = append(messages, Message{User: username, Timestamp: ts, Text: msgText})
|
||||||
|
mu.Unlock()
|
||||||
|
send(conn, fmt.Sprintf("\r\n%s✓ Elküldve!%s\r\n", GR, R))
|
||||||
|
} else {
|
||||||
|
send(conn, fmt.Sprintf("\r\n%s(Üres – nem küldtük el)%s\r\n", GY, R))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func showWikiList(conn net.Conn, tag string, color string, title string) {
|
||||||
|
send(conn, boxHeader(title, color))
|
||||||
|
send(conn, fmt.Sprintf(" %sTöltés folyamatban...%s\r\n", GY, R))
|
||||||
|
|
||||||
|
pages, err := fetchWikiList(tag)
|
||||||
|
if err != nil {
|
||||||
|
send(conn, fmt.Sprintf("\r\n%sKapcsolati hiba: %v%s\r\n", RD, err, R))
|
||||||
|
pause(conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear "Töltés" line
|
||||||
|
send(conn, "\r\033[A\033[2K")
|
||||||
|
|
||||||
|
if len(pages) == 0 {
|
||||||
|
send(conn, fmt.Sprintf(" %sNincs találat a '%s' tag-re.%s\r\n", GY, tag, R))
|
||||||
|
pause(conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
maxIdx := 25
|
||||||
|
if len(pages) < maxIdx {
|
||||||
|
maxIdx = len(pages)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, p := range pages[:maxIdx] {
|
||||||
|
date := fmtDate(p.CreatedAt)
|
||||||
|
if date == "–" {
|
||||||
|
date = 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]
|
||||||
|
}
|
||||||
|
send(conn, fmt.Sprintf(" %s%2d%s %s%s%s\r\n", color, i+1, R, WH, titleP, R))
|
||||||
|
if desc != "" {
|
||||||
|
send(conn, fmt.Sprintf(" %s%s %s%s%s\r\n", GY, date, DIM, desc, R))
|
||||||
|
} else {
|
||||||
|
send(conn, fmt.Sprintf(" %s%s%s\r\n", GY, date, R))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
send(conn, fmt.Sprintf("\r\n%sSzám a tartalom megnyitásához, ENTER a visszalépéshez:%s ", GY, R))
|
||||||
|
choice, _ := readLine(conn)
|
||||||
|
choice = strings.TrimSpace(choice)
|
||||||
|
idx, err := strconv.Atoi(choice)
|
||||||
|
if err != nil || idx < 1 || idx > len(pages) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
page := pages[idx-1]
|
||||||
|
send(conn, fmt.Sprintf("\r\n%sTartalom betöltése...%s", GY, R))
|
||||||
|
content, err := fetchWikiContent(page.ID)
|
||||||
|
if err != nil {
|
||||||
|
send(conn, fmt.Sprintf("\r\n%sHiba: %v%s\r\n", RD, err, R))
|
||||||
|
pause(conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
send(conn, "\r\033[A\033[2K")
|
||||||
|
send(conn, fmt.Sprintf("\r\n%s\r\n", hr("═", color)))
|
||||||
|
send(conn, fmt.Sprintf(" %s%s%s%s\r\n", WH, B, page.Title, R))
|
||||||
|
url := fmt.Sprintf("%s/%s/%s", WikiJSBaseURL, page.Locale, page.Path)
|
||||||
|
send(conn, fmt.Sprintf(" %s%s %s%s%s\r\n", GY, fmtDate(page.CreatedAt), DIM, url, R))
|
||||||
|
send(conn, fmt.Sprintf("%s\r\n\r\n", hr("─", GY)))
|
||||||
|
|
||||||
|
body := wrapped(content, 2, 5000)
|
||||||
|
for _, line := range strings.Split(body, "\r\n") {
|
||||||
|
send(conn, line+"\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
send(conn, fmt.Sprintf("\r\n%s\r\n", hr("─", GY)))
|
||||||
|
pause(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func showGames(conn net.Conn) {
|
||||||
|
send(conn, boxHeader("🎮 JÁTÉKKATALÓGUS", YL))
|
||||||
|
send(conn, fmt.Sprintf(" %sTöltés folyamatban...%s\r\n", GY, R))
|
||||||
|
|
||||||
|
softwares, err := fetchGames()
|
||||||
|
if err != nil {
|
||||||
|
send(conn, fmt.Sprintf("\r\n%sKapcsolati hiba: %v%s\r\n", RD, err, R))
|
||||||
|
pause(conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
send(conn, "\r\033[A\033[2K")
|
||||||
|
|
||||||
|
if len(softwares) == 0 {
|
||||||
|
send(conn, fmt.Sprintf(" %sNincs elérhető játék.%s\r\n", GY, R))
|
||||||
|
pause(conn)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
base := "https://games.teletype.hu"
|
||||||
|
|
||||||
|
for i, entry := range softwares {
|
||||||
|
sw := entry.Software
|
||||||
|
lr := entry.LatestRelease
|
||||||
|
|
||||||
|
send(conn, fmt.Sprintf("\r\n %s%s%s\r\n", YL, strings.Repeat("─", W-4), R))
|
||||||
|
send(conn, 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 := wrapped(sw.Desc, 7, 1000)
|
||||||
|
for _, line := range strings.Split(wrappedDesc, "\r\n") {
|
||||||
|
send(conn, 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, " ")
|
||||||
|
}
|
||||||
|
send(conn, fmt.Sprintf(" %sLegújabb: v%s%s %s\r\n", GY, lr.Version, R, badgeStr))
|
||||||
|
|
||||||
|
if lr.HTMLFolderPath != "" {
|
||||||
|
url := base + lr.HTMLFolderPath
|
||||||
|
send(conn, fmt.Sprintf(" %s▶ %s%s\r\n", DIM, url, R))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
send(conn, fmt.Sprintf(" %s%d verzió elérhető%s\r\n", GY, len(entry.Releases), R))
|
||||||
|
}
|
||||||
|
|
||||||
|
send(conn, fmt.Sprintf("\r\n%s\r\n", hr("─", YL)))
|
||||||
|
send(conn, fmt.Sprintf(" %sTeljes katalógus: %s%s\r\n", GY, base, R))
|
||||||
|
pause(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func showOnline(conn net.Conn, myAddr string) {
|
||||||
|
send(conn, boxHeader("👥 ONLINE FELHASZNÁLÓK", CY))
|
||||||
|
mu.Lock()
|
||||||
|
snap := make(map[string]string)
|
||||||
|
for k, v := range onlineUsers {
|
||||||
|
snap[k] = v
|
||||||
|
}
|
||||||
|
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 == myAddr {
|
||||||
|
marker = fmt.Sprintf(" %s← te%s", GR, R)
|
||||||
|
}
|
||||||
|
send(conn, fmt.Sprintf(" %s•%s %s%s%s%s\r\n", CY, R, WH, user, R, marker))
|
||||||
|
}
|
||||||
|
send(conn, fmt.Sprintf("\r\n %sÖsszesen: %s%d%s fő online%s\r\n", GY, WH, len(snap), GY, R))
|
||||||
|
pause(conn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func showSysinfo(conn net.Conn) {
|
||||||
|
send(conn, boxHeader("ℹ RENDSZERINFO", GY))
|
||||||
|
mu.Lock()
|
||||||
|
uc := len(onlineUsers)
|
||||||
|
mc := len(messages)
|
||||||
|
mu.Unlock()
|
||||||
|
|
||||||
|
now := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
rows := [][]string{
|
||||||
|
{"Szerver ideje", now},
|
||||||
|
{"Online userek", strconv.Itoa(uc)},
|
||||||
|
{"Üzenetek száma", strconv.Itoa(mc)},
|
||||||
|
{"Wiki URL", WikiJSBaseURL},
|
||||||
|
{"Games API", GamesAPIURL},
|
||||||
|
{"Token beállítva", func() string {
|
||||||
|
if wikiToken != "" {
|
||||||
|
return "igen"
|
||||||
|
}
|
||||||
|
return "nem"
|
||||||
|
}()},
|
||||||
|
{"Platform", "Go BBS v2.0"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, row := range rows {
|
||||||
|
send(conn, fmt.Sprintf(" %s%-18s%s %s%s%s\r\n", GY, row[0], R, WH, row[1], R))
|
||||||
|
}
|
||||||
|
pause(conn)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user