summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGab <24553253+gabrix73@users.noreply.github.com>2025-12-01 23:05:55 +0100
committerGitHub <noreply@github.com>2025-12-01 23:05:55 +0100
commit8cd481b0c47b0a137b58c997ba0340c635edc92a (patch)
treed37cc8884168d4067b7cc4c32845a4e79127e2b0
parentb103c1add8b6fae5eb1a1296ce1837433cdbaa72 (diff)
downloadonion-newsreader-8cd481b0c47b0a137b58c997ba0340c635edc92a.tar.gz
onion-newsreader-8cd481b0c47b0a137b58c997ba0340c635edc92a.tar.xz
onion-newsreader-8cd481b0c47b0a137b58c997ba0340c635edc92a.zip
Update onion-newsreader.go
-rw-r--r--onion-newsreader.go4282
1 files changed, 789 insertions, 3493 deletions
diff --git a/onion-newsreader.go b/onion-newsreader.go
index 836b9e4..c0b0a56 100644
--- a/onion-newsreader.go
+++ b/onion-newsreader.go
@@ -5,7 +5,6 @@ import (
"context"
"crypto/rand"
"fmt"
- "html"
"html/template"
"io"
"log"
@@ -20,60 +19,48 @@ import (
"time"
)
-type NewsgroupInfo struct {
- Name string
- High int
- Low int
- Status string
- ArticleCount int
-}
-
-type ArticleOverview struct {
- Number int
- Subject string
- From string
- Date string
- MessageID string
- References string
- ByteCount int
- LineCount int
- ParsedDate time.Time
-}
+const (
+ M2UsenetURL = "http://itcxzfm2h36hfj6j7qxksyfm4ipp3co4rkl62sgge7hp6u77lbretiyd.onion:8880"
+ TorProxyAddr = "127.0.0.1:9050"
+ PrimaryNNTP = "peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion:119"
+ FallbackNNTP = "news.tcpreset.net:119"
+)
-type ThreadNode struct {
- Article *ArticleOverview
- Children []*ThreadNode
- Level int
- IsRoot bool
+type NewsgroupInfo struct {
+ Name string
+ High int
+ Low int
+ Status string
}
-type Thread struct {
- Root *ThreadNode
- ArticleCount int
- LastDate time.Time
+type ArticleInfo struct {
+ Number int
Subject string
+ From string
+ Date string
+ MessageID string
+ References string // References header for threading
+ ParentID string // Direct parent Message-ID
+ ReplySubject string // For m2usenet reply link
+ ReplyRef string // For m2usenet reply link
+ Depth int // Threading depth (0 = root)
+ Children []*ArticleInfo // Child posts (replies)
}
type NNTPClient struct {
- conn net.Conn
- reader *bufio.Reader
- mutex sync.Mutex
- lastUsed time.Time
- isConnected bool
- isConnecting bool
- connectionChan chan struct{}
- backoffCount int
- maxBackoff time.Duration
+ conn net.Conn
+ reader *bufio.Reader
+ mutex sync.Mutex
+ isConnected bool
connectedServer string
- isOnionService bool
+ lastUsed time.Time
}
type NewsServer struct {
- client *NNTPClient
- groups map[string]*NewsgroupInfo
- mutex sync.RWMutex
- session string
- features map[string]bool
+ client *NNTPClient
+ groups map[string]*NewsgroupInfo
+ mutex sync.RWMutex
+ session string
downloadStatus struct {
IsDownloading bool
StartTime time.Time
@@ -85,472 +72,172 @@ type NewsServer struct {
}
func NewNNTPClient() *NNTPClient {
- return &NNTPClient{
- connectionChan: make(chan struct{}, 1),
- maxBackoff: 5 * time.Minute,
- }
+ return &NNTPClient{}
}
func NewNewsServer() *NewsServer {
sessionBytes := make([]byte, 8)
rand.Read(sessionBytes)
-
return &NewsServer{
client: NewNNTPClient(),
groups: make(map[string]*NewsgroupInfo),
session: fmt.Sprintf("%x", sessionBytes),
- features: map[string]bool{
- "MODE_READER": true,
- "XOVER": true,
- "OVER": true,
- "HDR": true,
- "LIST_ACTIVE": true,
- "LIST_NEWSGROUPS": true,
- },
}
}
-func (c *NNTPClient) connectInternal() error {
- proxyAddr := "127.0.0.1:9050"
-
- targetConfigs := []struct {
- addr string
- isOnion bool
- description string
- }{
- {
- addr: "peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion:119",
- isOnion: true,
- description: "Primary NNTP Onion Service",
- },
- {
- addr: "news.tcpreset.net:119",
- isOnion: false,
- description: "TCPReset NNTP Server (via Tor)",
- },
- }
-
- var lastErr error
- for i, config := range targetConfigs {
- log.Printf("๐Ÿ”— Attempt %d/%d: %s - %s", i+1, len(targetConfigs), config.description, config.addr)
-
- var conn net.Conn
- var err error
-
- if config.isOnion {
- conn, err = c.connectViaSocks5(proxyAddr, config.addr)
- } else {
- conn, err = c.connectViaSocks5(proxyAddr, config.addr)
- }
-
- if err != nil {
- lastErr = fmt.Errorf("%s failed: %v", config.description, err)
- log.Printf("โŒ %s", lastErr)
- continue
- }
-
- log.Printf("โœ… Successfully connected to %s", config.description)
- c.conn = conn
- c.reader = bufio.NewReader(c.conn)
- c.isConnected = true
- c.lastUsed = time.Now()
- c.connectedServer = config.description
- c.isOnionService = config.isOnion
-
- log.Printf("๐Ÿ“ฅ Reading NNTP welcome message from %s...", config.description)
- c.conn.SetReadDeadline(time.Now().Add(60 * time.Second))
- welcome, err := c.reader.ReadString('\n')
- c.conn.SetReadDeadline(time.Time{})
-
- if err != nil {
- c.isConnected = false
- lastErr = fmt.Errorf("failed to read welcome from %s: %v", config.description, err)
- log.Printf("โŒ %s", lastErr)
- c.conn.Close()
- c.connectedServer = ""
- c.isOnionService = false
- continue
- }
-
- welcome = strings.TrimSpace(welcome)
- log.Printf("๐Ÿ“ฅ Welcome received from %s: %s", config.description, welcome)
+// ============= SOCKS5 / Tor Connection =============
- log.Printf("๐Ÿ“ค Sending MODE READER command to %s...", config.description)
- return c.modeReaderInternal()
- }
-
- return fmt.Errorf("failed to connect to any NNTP server, last error: %v", lastErr)
-}
-
-func (c *NNTPClient) connectViaSocks5(proxyAddr, targetAddr string) (net.Conn, error) {
- proxyConn, err := net.DialTimeout("tcp", proxyAddr, 30*time.Second)
+func (c *NNTPClient) connectViaSocks5(targetAddr string) (net.Conn, error) {
+ proxyConn, err := net.DialTimeout("tcp", TorProxyAddr, 30*time.Second)
if err != nil {
- return nil, fmt.Errorf("failed to connect to Tor proxy: %v", err)
+ return nil, fmt.Errorf("tor proxy unreachable: %v", err)
}
deadline := 60 * time.Second
if strings.Contains(targetAddr, ".onion") {
deadline = 180 * time.Second
}
-
proxyConn.SetDeadline(time.Now().Add(deadline))
defer proxyConn.SetDeadline(time.Time{})
- log.Printf("๐Ÿ”„ Performing SOCKS5 handshake for %s...", targetAddr)
- if err := c.socks5Handshake(proxyConn, targetAddr); err != nil {
+ if _, err := proxyConn.Write([]byte{0x05, 0x01, 0x00}); err != nil {
proxyConn.Close()
- return nil, fmt.Errorf("SOCKS5 handshake failed: %v", err)
- }
-
- log.Printf("โœ… SOCKS5 connection established to %s", targetAddr)
- return proxyConn, nil
-}
-
-func (c *NNTPClient) socks5Handshake(conn net.Conn, target string) error {
- log.Printf("๐Ÿ”„ SOCKS5: Starting authentication...")
- authReq := []byte{0x05, 0x01, 0x00}
- if _, err := conn.Write(authReq); err != nil {
- return fmt.Errorf("auth request failed: %v", err)
+ return nil, fmt.Errorf("socks5 auth failed: %v", err)
}
authResp := make([]byte, 2)
- if _, err := io.ReadFull(conn, authResp); err != nil {
- return fmt.Errorf("auth response read failed: %v", err)
+ if _, err := io.ReadFull(proxyConn, authResp); err != nil {
+ proxyConn.Close()
+ return nil, fmt.Errorf("socks5 auth response failed: %v", err)
}
-
if authResp[0] != 0x05 || authResp[1] != 0x00 {
- return fmt.Errorf("SOCKS5 authentication failed")
+ proxyConn.Close()
+ return nil, fmt.Errorf("socks5 auth rejected")
}
- log.Printf("โœ… SOCKS5: Authentication successful")
-
- parts := strings.Split(target, ":")
+ parts := strings.Split(targetAddr, ":")
host := parts[0]
port, _ := strconv.Atoi(parts[1])
- log.Printf("๐Ÿ”„ SOCKS5: Connecting to %s (hostname length: %d)", target, len(host))
- req := []byte{0x05, 0x01, 0x00, 0x03}
- req = append(req, byte(len(host)))
+ req := []byte{0x05, 0x01, 0x00, 0x03, byte(len(host))}
req = append(req, []byte(host)...)
req = append(req, byte(port>>8), byte(port&0xff))
- if _, err := conn.Write(req); err != nil {
- return fmt.Errorf("connect request failed: %v", err)
+ if _, err := proxyConn.Write(req); err != nil {
+ proxyConn.Close()
+ return nil, fmt.Errorf("socks5 connect failed: %v", err)
}
- log.Printf("๐Ÿ”„ SOCKS5: Waiting for connect response...")
resp := make([]byte, 4)
- if _, err := io.ReadFull(conn, resp); err != nil {
- return fmt.Errorf("connect response read failed: %v", err)
+ if _, err := io.ReadFull(proxyConn, resp); err != nil {
+ proxyConn.Close()
+ return nil, fmt.Errorf("socks5 response failed: %v", err)
}
- if resp[0] != 0x05 {
- return fmt.Errorf("SOCKS5 version mismatch")
- }
-
if resp[1] != 0x00 {
- errorMsg := map[byte]string{
- 0x01: "general SOCKS server failure",
- 0x02: "connection not allowed by ruleset",
- 0x03: "network unreachable",
- 0x04: "host unreachable",
- 0x05: "connection refused",
- 0x06: "TTL expired",
- 0x07: "command not supported",
- 0x08: "address type not supported",
+ proxyConn.Close()
+ errCodes := map[byte]string{
+ 0x01: "general failure", 0x02: "not allowed", 0x03: "network unreachable",
+ 0x04: "host unreachable", 0x05: "connection refused", 0x06: "TTL expired",
}
- if msg, ok := errorMsg[resp[1]]; ok {
- return fmt.Errorf("SOCKS5 connect failed: %s (code %d)", msg, resp[1])
+ if msg, ok := errCodes[resp[1]]; ok {
+ return nil, fmt.Errorf("socks5: %s", msg)
}
- return fmt.Errorf("SOCKS5 connect failed: error code %d", resp[1])
+ return nil, fmt.Errorf("socks5 error: %d", resp[1])
}
switch resp[3] {
case 0x01:
- addr := make([]byte, 6)
- io.ReadFull(conn, addr)
+ io.ReadFull(proxyConn, make([]byte, 6))
case 0x03:
- domainLen := make([]byte, 1)
- io.ReadFull(conn, domainLen)
- domain := make([]byte, domainLen[0]+2)
- io.ReadFull(conn, domain)
+ lenByte := make([]byte, 1)
+ io.ReadFull(proxyConn, lenByte)
+ io.ReadFull(proxyConn, make([]byte, lenByte[0]+2))
case 0x04:
- addr := make([]byte, 18)
- io.ReadFull(conn, addr)
+ io.ReadFull(proxyConn, make([]byte, 18))
}
- return nil
-}
-
-func (c *NNTPClient) ensureConnected() error {
- c.mutex.Lock()
- defer c.mutex.Unlock()
-
- if !c.isConnected || c.conn == nil || time.Since(c.lastUsed) > 5*time.Minute {
- log.Printf("๐Ÿ”„ Connection check: reconnecting (connected=%v, lastUsed=%v ago)",
- c.isConnected, time.Since(c.lastUsed))
-
- if c.conn != nil {
- c.conn.Close()
- }
- c.isConnected = false
- c.connectedServer = ""
- c.isOnionService = false
-
- if err := c.testOnionReachability(); err != nil {
- return fmt.Errorf("onion service unreachable: %v", err)
- }
-
- if err := c.connectInternal(); err != nil {
- return fmt.Errorf("failed to reconnect: %v", err)
- }
- }
-
- c.lastUsed = time.Now()
- return nil
+ return proxyConn, nil
}
-func (c *NNTPClient) testOnionReachability() error {
- log.Printf("๐Ÿงช Testing NNTP server reachability...")
-
- proxyAddr := "127.0.0.1:9050"
- testConfigs := []struct {
- addr string
- isOnion bool
- description string
+func (c *NNTPClient) connect() error {
+ targets := []struct {
+ addr string
+ name string
}{
- {
- addr: "peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion:119",
- isOnion: true,
- description: "Primary NNTP Onion Service",
- },
- {
- addr: "news.tcpreset.net:119",
- isOnion: false,
- description: "TCPReset NNTP Server",
- },
+ {PrimaryNNTP, "Onion Service"},
+ {FallbackNNTP, "Fallback (via Tor)"},
}
-
- for _, config := range testConfigs {
- log.Printf("๐Ÿงช Testing %s (%s)...", config.description, config.addr)
-
- proxyConn, err := net.DialTimeout("tcp", proxyAddr, 10*time.Second)
+
+ var lastErr error
+ for _, target := range targets {
+ log.Printf("๐Ÿ”— Connecting to %s...", target.name)
+
+ conn, err := c.connectViaSocks5(target.addr)
if err != nil {
- log.Printf("โš ๏ธ Tor proxy unreachable: %v", err)
+ lastErr = err
+ log.Printf("โŒ %s failed: %v", target.name, err)
continue
}
- defer proxyConn.Close()
-
- deadline := 30 * time.Second
- if config.isOnion {
- deadline = 60 * time.Second
- }
- proxyConn.SetDeadline(time.Now().Add(deadline))
-
- if err := c.quickSocks5Test(proxyConn, config.addr); err != nil {
- log.Printf("โš ๏ธ %s failed: %v", config.description, err)
- continue
- }
-
- log.Printf("โœ… %s appears reachable", config.description)
- return nil
- }
-
- return fmt.Errorf("no NNTP servers are reachable (tried onion service and public servers)")
-}
-func (c *NNTPClient) quickSocks5Test(conn net.Conn, target string) error {
- authReq := []byte{0x05, 0x01, 0x00}
- if _, err := conn.Write(authReq); err != nil {
- return fmt.Errorf("auth request failed: %v", err)
- }
-
- authResp := make([]byte, 2)
- if _, err := io.ReadFull(conn, authResp); err != nil {
- return fmt.Errorf("auth response read failed: %v", err)
- }
-
- if authResp[0] != 0x05 || authResp[1] != 0x00 {
- return fmt.Errorf("SOCKS5 authentication failed")
- }
-
- parts := strings.Split(target, ":")
- host := parts[0]
- port, _ := strconv.Atoi(parts[1])
-
- req := []byte{0x05, 0x01, 0x00, 0x03}
- req = append(req, byte(len(host)))
- req = append(req, []byte(host)...)
- req = append(req, byte(port>>8), byte(port&0xff))
-
- if _, err := conn.Write(req); err != nil {
- return fmt.Errorf("connect request failed: %v", err)
- }
-
- resp := make([]byte, 4)
- if _, err := io.ReadFull(conn, resp); err != nil {
- return fmt.Errorf("connect response read failed: %v", err)
- }
+ c.conn = conn
+ c.reader = bufio.NewReader(conn)
- if resp[0] != 0x05 {
- return fmt.Errorf("SOCKS5 version mismatch")
- }
-
- if resp[1] != 0x00 {
- errorMsg := map[byte]string{
- 0x01: "general SOCKS server failure",
- 0x02: "connection not allowed by ruleset",
- 0x03: "network unreachable",
- 0x04: "host unreachable",
- 0x05: "connection refused",
- 0x06: "TTL expired",
- 0x07: "command not supported",
- 0x08: "address type not supported",
- }
- if msg, ok := errorMsg[resp[1]]; ok {
- return fmt.Errorf("SOCKS5 connect failed: %s", msg)
+ conn.SetReadDeadline(time.Now().Add(60 * time.Second))
+ welcome, err := c.reader.ReadString('\n')
+ conn.SetReadDeadline(time.Time{})
+ if err != nil {
+ conn.Close()
+ lastErr = err
+ continue
}
- return fmt.Errorf("SOCKS5 connect failed: error code %d", resp[1])
- }
-
- switch resp[3] {
- case 0x01:
- addr := make([]byte, 6)
- io.ReadFull(conn, addr)
- case 0x03:
- domainLen := make([]byte, 1)
- io.ReadFull(conn, domainLen)
- domain := make([]byte, domainLen[0]+2)
- io.ReadFull(conn, domain)
- case 0x04:
- addr := make([]byte, 18)
- io.ReadFull(conn, addr)
- }
-
- return nil
-}
-
-func (c *NNTPClient) modeReaderInternal() error {
- if err := c.sendCommand("MODE READER"); err != nil {
- return fmt.Errorf("failed to send MODE READER: %v", err)
- }
-
- response, err := c.readResponse()
- if err != nil {
- return fmt.Errorf("failed to read MODE READER response: %v", err)
- }
-
- if !strings.HasPrefix(response, "200") && !strings.HasPrefix(response, "201") {
- return fmt.Errorf("MODE READER failed: %s", response)
- }
+ log.Printf("๐Ÿ“ฅ Welcome: %s", strings.TrimSpace(welcome))
- log.Printf("โœ… MODE READER successful: %s", response)
- return nil
-}
-
-func (c *NNTPClient) sendCommand(command string) error {
- if c.conn == nil {
- return fmt.Errorf("not connected")
- }
+ if _, err := conn.Write([]byte("MODE READER\r\n")); err != nil {
+ conn.Close()
+ lastErr = err
+ continue
+ }
- log.Printf("๐Ÿ“ค Sending: %s", command)
- _, err := c.conn.Write([]byte(command + "\r\n"))
- return err
-}
+ modeResp, err := c.reader.ReadString('\n')
+ if err != nil {
+ conn.Close()
+ lastErr = err
+ continue
+ }
-func (c *NNTPClient) readResponse() (string, error) {
- if c.reader == nil {
- return "", fmt.Errorf("not connected")
- }
+ if !strings.HasPrefix(modeResp, "200") && !strings.HasPrefix(modeResp, "201") {
+ conn.Close()
+ lastErr = fmt.Errorf("MODE READER failed: %s", modeResp)
+ continue
+ }
- response, err := c.reader.ReadString('\n')
- if err != nil {
- return "", err
+ c.isConnected = true
+ c.connectedServer = target.name
+ c.lastUsed = time.Now()
+ log.Printf("โœ… Connected to %s", target.name)
+ return nil
}
- response = strings.TrimSpace(response)
- log.Printf("๐Ÿ“ฅ Received: %s", response)
- return response, nil
+ return fmt.Errorf("all servers failed: %v", lastErr)
}
-func (c *NNTPClient) HealthCheck() error {
- log.Printf("๐Ÿงช Performing NNTP health check...")
+func (c *NNTPClient) ensureConnected() error {
c.mutex.Lock()
defer c.mutex.Unlock()
- if !c.isConnected || c.conn == nil {
- return fmt.Errorf("connection is nil or not connected")
- }
-
- c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
- c.conn.SetReadDeadline(time.Now().Add(10 * time.Second))
- defer c.conn.SetWriteDeadline(time.Time{})
- defer c.conn.SetReadDeadline(time.Time{})
-
- // Try DATE command first (single line response, safer)
- log.Printf("๐Ÿ“ค Health check: sending DATE command")
- if err := c.sendCommand("DATE"); err != nil {
- c.isConnected = false
- return fmt.Errorf("health check send failed: %v", err)
- }
-
- response, err := c.readResponse()
- if err != nil {
- c.isConnected = false
- return fmt.Errorf("health check read failed: %v", err)
- }
-
- log.Printf("๐Ÿ“ฅ Health check: received: %s", response)
-
- // DATE should return "111 YYYYMMDDhhmmss" - single line response
- if strings.HasPrefix(response, "111") {
- log.Printf("โœ… Health check successful")
- return nil
- }
-
- // If DATE failed, try HELP but consume multiline response properly
- log.Printf("๐Ÿ“ค Health check: DATE failed, trying HELP command")
- if err := c.sendCommand("HELP"); err != nil {
- c.isConnected = false
- return fmt.Errorf("health check HELP send failed: %v", err)
- }
-
- helpResponse, err := c.readResponse()
- if err != nil {
- c.isConnected = false
- return fmt.Errorf("health check HELP read failed: %v", err)
- }
-
- log.Printf("๐Ÿ“ฅ Health check: HELP received: %s", helpResponse)
-
- // HELP command returns multiline response - consume all lines until "."
- if strings.HasPrefix(helpResponse, "100") {
- log.Printf("๐Ÿ“ฅ Health check: reading multiline HELP response")
- for {
- line, err := c.reader.ReadString('\n')
- if err != nil {
- c.isConnected = false
- return fmt.Errorf("health check multiline read failed: %v", err)
- }
-
- line = strings.TrimSpace(line)
- log.Printf("๐Ÿ“ฅ Health check help: %s", line)
-
- if line == "." {
- break
- }
+ if !c.isConnected || c.conn == nil || time.Since(c.lastUsed) > 5*time.Minute {
+ if c.conn != nil {
+ c.conn.Close()
}
- log.Printf("โœ… Health check successful")
- return nil
+ c.isConnected = false
+ return c.connect()
}
-
- c.isConnected = false
- return fmt.Errorf("health check failed: unexpected response: %s", helpResponse)
+ c.lastUsed = time.Now()
+ return nil
}
func (c *NNTPClient) ListGroups() (map[string]*NewsgroupInfo, error) {
- log.Printf("Downloading active file with retry logic...")
-
if err := c.ensureConnected(); err != nil {
return nil, err
}
@@ -558,45 +245,41 @@ func (c *NNTPClient) ListGroups() (map[string]*NewsgroupInfo, error) {
c.mutex.Lock()
defer c.mutex.Unlock()
- log.Printf("๐Ÿ“ค Requesting group list...")
- if err := c.sendCommand("LIST"); err != nil {
- return nil, fmt.Errorf("failed to send LIST command: %v", err)
+ log.Printf("๐Ÿ“ค Sending LIST command...")
+ if _, err := c.conn.Write([]byte("LIST\r\n")); err != nil {
+ return nil, err
}
- response, err := c.readResponse()
+ resp, err := c.reader.ReadString('\n')
if err != nil {
- return nil, fmt.Errorf("failed to read LIST response: %v", err)
+ return nil, err
}
-
- if !strings.HasPrefix(response, "215") {
- return nil, fmt.Errorf("LIST command failed: %s", response)
+ if !strings.HasPrefix(resp, "215") {
+ return nil, fmt.Errorf("LIST failed: %s", resp)
}
- log.Printf("๐Ÿ“ฅ Reading group data...")
groups := make(map[string]*NewsgroupInfo)
lineCount := 0
for {
line, err := c.reader.ReadString('\n')
if err != nil {
- return nil, fmt.Errorf("error reading group data: %v", err)
+ return nil, fmt.Errorf("error reading groups at line %d: %v", lineCount, err)
}
-
line = strings.TrimSpace(line)
if line == "." {
break
}
lineCount++
- if lineCount%1000 == 0 {
- log.Printf("๐Ÿ“Š Processed %d groups...", lineCount)
+ if lineCount%5000 == 0 {
+ log.Printf("๐Ÿ“Š Read %d groups...", lineCount)
}
parts := strings.Fields(line)
if len(parts) >= 4 {
high, _ := strconv.Atoi(parts[1])
low, _ := strconv.Atoi(parts[2])
-
groups[parts[0]] = &NewsgroupInfo{
Name: parts[0],
High: high,
@@ -606,3348 +289,961 @@ func (c *NNTPClient) ListGroups() (map[string]*NewsgroupInfo, error) {
}
}
- log.Printf("โœ… Successfully loaded %d newsgroups", len(groups))
+ log.Printf("โœ… Loaded %d groups total", len(groups))
return groups, nil
}
-func (c *NNTPClient) SelectGroup(groupName string) (int, int, int, error) {
+func (c *NNTPClient) SelectGroup(name string) (int, int, int, error) {
c.mutex.Lock()
defer c.mutex.Unlock()
- if !c.isConnected || c.conn == nil {
+ if !c.isConnected {
return 0, 0, 0, fmt.Errorf("not connected")
}
- log.Printf("๐Ÿ“ค Selecting group: %s", groupName)
- if err := c.sendCommand("GROUP " + groupName); err != nil {
- return 0, 0, 0, fmt.Errorf("failed to send GROUP command: %v", err)
+ if _, err := c.conn.Write([]byte("GROUP " + name + "\r\n")); err != nil {
+ return 0, 0, 0, err
}
- response, err := c.readResponse()
+ resp, err := c.reader.ReadString('\n')
if err != nil {
- return 0, 0, 0, fmt.Errorf("failed to read GROUP response: %v", err)
+ return 0, 0, 0, err
}
- if !strings.HasPrefix(response, "211") {
- return 0, 0, 0, fmt.Errorf("GROUP command failed: %s", response)
+ if !strings.HasPrefix(resp, "211") {
+ return 0, 0, 0, fmt.Errorf("GROUP failed: %s", resp)
}
- // Parse response: 211 count low high group
- parts := strings.Fields(response)
- if len(parts) < 5 {
- return 0, 0, 0, fmt.Errorf("invalid GROUP response format: %s", response)
+ parts := strings.Fields(resp)
+ if len(parts) < 4 {
+ return 0, 0, 0, fmt.Errorf("invalid response")
}
count, _ := strconv.Atoi(parts[1])
low, _ := strconv.Atoi(parts[2])
high, _ := strconv.Atoi(parts[3])
-
- log.Printf("โœ… Group selected: %s (count=%d, range=%d-%d)", groupName, count, low, high)
return count, low, high, nil
}
-func (c *NNTPClient) GetArticleOverview(low, high int, maxArticles int) ([]*ArticleOverview, error) {
+func (c *NNTPClient) GetRecentArticles(low, high, max int) ([]*ArticleInfo, error) {
c.mutex.Lock()
defer c.mutex.Unlock()
- if !c.isConnected || c.conn == nil {
+ if !c.isConnected {
return nil, fmt.Errorf("not connected")
}
- // Limit range to prevent huge downloads
- if high-low+1 > maxArticles {
- low = high - maxArticles + 1
+ if high-low+1 > max {
+ low = high - max + 1
}
- log.Printf("๐Ÿ“ค Getting article overview for range %d-%d", low, high)
- if err := c.sendCommand(fmt.Sprintf("XOVER %d-%d", low, high)); err != nil {
- return nil, fmt.Errorf("failed to send XOVER command: %v", err)
+ cmd := fmt.Sprintf("XOVER %d-%d\r\n", low, high)
+ if _, err := c.conn.Write([]byte(cmd)); err != nil {
+ return nil, err
}
- response, err := c.readResponse()
+ resp, err := c.reader.ReadString('\n')
if err != nil {
- return nil, fmt.Errorf("failed to read XOVER response: %v", err)
+ return nil, err
}
- if !strings.HasPrefix(response, "224") {
- return nil, fmt.Errorf("XOVER command failed: %s", response)
+ if !strings.HasPrefix(resp, "224") {
+ return nil, fmt.Errorf("XOVER failed: %s", resp)
}
- var articles []*ArticleOverview
- log.Printf("๐Ÿ“ฅ Reading article overview data...")
-
+ var articles []*ArticleInfo
for {
line, err := c.reader.ReadString('\n')
if err != nil {
- return nil, fmt.Errorf("error reading overview data: %v", err)
+ return nil, err
}
-
line = strings.TrimSpace(line)
if line == "." {
break
}
- article := c.parseOverviewLine(line)
- if article != nil {
- articles = append(articles, article)
- }
- }
-
- log.Printf("โœ… Retrieved %d article overviews", len(articles))
- return articles, nil
-}
-
-func (c *NNTPClient) parseOverviewLine(line string) *ArticleOverview {
- // XOVER format: number<tab>subject<tab>from<tab>date<tab>message-id<tab>references<tab>byte count<tab>line count
- parts := strings.Split(line, "\t")
- if len(parts) < 8 {
- return nil
- }
-
- number, _ := strconv.Atoi(parts[0])
- byteCount, _ := strconv.Atoi(parts[6])
- lineCount, _ := strconv.Atoi(parts[7])
-
- // Parse the date for threading
- parsedDate := time.Now() // fallback
- if dateStr := strings.TrimSpace(parts[3]); dateStr != "" {
- // Try common date formats
- formats := []string{
- time.RFC1123Z,
- time.RFC1123,
- "Mon, 2 Jan 2006 15:04:05 -0700",
- "2 Jan 2006 15:04:05 -0700",
- "Mon, 2 Jan 2006 15:04:05 MST",
- }
-
- for _, format := range formats {
- if parsed, err := time.Parse(format, dateStr); err == nil {
- parsedDate = parsed
- break
+ parts := strings.Split(line, "\t")
+ if len(parts) >= 5 {
+ num, _ := strconv.Atoi(parts[0])
+ date := ""
+ if len(parts) > 3 {
+ date = parts[3]
+ }
+ // XOVER format: num subject from date message-id references bytes lines
+ refs := ""
+ if len(parts) > 5 {
+ refs = strings.TrimSpace(parts[5])
}
+ // Extract parent ID (last message-id in references)
+ parentID := ""
+ if refs != "" {
+ // References contains space-separated message-ids
+ refParts := strings.Fields(refs)
+ if len(refParts) > 0 {
+ parentID = refParts[len(refParts)-1]
+ }
+ }
+ articles = append(articles, &ArticleInfo{
+ Number: num,
+ Subject: parts[1],
+ From: parts[2],
+ Date: date,
+ MessageID: strings.TrimSpace(parts[4]),
+ References: refs,
+ ParentID: parentID,
+ })
}
}
- return &ArticleOverview{
- Number: number,
- Subject: parts[1],
- From: parts[2],
- Date: parts[3],
- MessageID: strings.TrimSpace(parts[4]),
- References: strings.TrimSpace(parts[5]),
- ByteCount: byteCount,
- LineCount: lineCount,
- ParsedDate: parsedDate,
- }
+ return articles, nil
}
-func (c *NNTPClient) GetArticle(groupName string, articleNumber int) (map[string]string, []string, error) {
+func (c *NNTPClient) GetArticle(articleNum int) (map[string]string, string, error) {
c.mutex.Lock()
defer c.mutex.Unlock()
- if !c.isConnected || c.conn == nil {
- return nil, nil, fmt.Errorf("not connected")
+ if !c.isConnected {
+ return nil, "", fmt.Errorf("not connected")
}
- log.Printf("๐Ÿ“ค Getting article %d from group %s", articleNumber, groupName)
- if err := c.sendCommand(fmt.Sprintf("ARTICLE %d", articleNumber)); err != nil {
- return nil, nil, fmt.Errorf("failed to send ARTICLE command: %v", err)
+ cmd := fmt.Sprintf("ARTICLE %d\r\n", articleNum)
+ if _, err := c.conn.Write([]byte(cmd)); err != nil {
+ return nil, "", err
}
- response, err := c.readResponse()
+ resp, err := c.reader.ReadString('\n')
if err != nil {
- return nil, nil, fmt.Errorf("failed to read ARTICLE response: %v", err)
+ return nil, "", err
}
- if !strings.HasPrefix(response, "220") {
- return nil, nil, fmt.Errorf("ARTICLE command failed: %s", response)
+ if !strings.HasPrefix(resp, "220") {
+ return nil, "", fmt.Errorf("ARTICLE failed: %s", resp)
}
- // Read article content
headers := make(map[string]string)
- var body []string
+ var bodyLines []string
inHeaders := true
-
- log.Printf("๐Ÿ“ฅ Reading article content...")
+ currentHeader := ""
+
for {
line, err := c.reader.ReadString('\n')
if err != nil {
- return nil, nil, fmt.Errorf("error reading article data: %v", err)
+ return nil, "", err
}
+ line = strings.TrimRight(line, "\r\n")
- line = strings.TrimSuffix(line, "\r\n")
- line = strings.TrimSuffix(line, "\n")
-
if line == "." {
break
}
+ // Handle dot-stuffing
+ if strings.HasPrefix(line, "..") {
+ line = line[1:]
+ }
+
if inHeaders {
if line == "" {
inHeaders = false
continue
}
-
- // Parse header line
- if strings.Contains(line, ":") {
- parts := strings.SplitN(line, ":", 2)
- if len(parts) == 2 {
- key := strings.TrimSpace(parts[0])
- value := strings.TrimSpace(parts[1])
-
- // Handle multi-line headers
- if existing, exists := headers[key]; exists {
- headers[key] = existing + " " + value
- } else {
- headers[key] = value
- }
- }
+
+ // Continuation of previous header
+ if (strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t")) && currentHeader != "" {
+ headers[currentHeader] += " " + strings.TrimSpace(line)
+ continue
+ }
+
+ // New header
+ if idx := strings.Index(line, ":"); idx > 0 {
+ currentHeader = line[:idx]
+ headers[currentHeader] = strings.TrimSpace(line[idx+1:])
}
} else {
- body = append(body, line)
+ bodyLines = append(bodyLines, line)
}
}
- log.Printf("โœ… Retrieved article %d: %d headers, %d body lines", articleNumber, len(headers), len(body))
- return headers, body, nil
+ return headers, strings.Join(bodyLines, "\n"), nil
}
-func (c *NNTPClient) GetConnectionInfo() (string, bool) {
+func (c *NNTPClient) Close() {
c.mutex.Lock()
defer c.mutex.Unlock()
- return c.connectedServer, c.isOnionService
-}
-
-func (c *NNTPClient) Close() error {
- c.mutex.Lock()
- defer c.mutex.Unlock()
-
- if c.conn != nil {
- log.Printf("๐Ÿ“ค Sending: QUIT")
- c.sendCommand("QUIT")
- c.conn.Close()
- c.conn = nil
- c.reader = nil
- }
- c.isConnected = false
- c.connectedServer = ""
- c.isOnionService = false
- return nil
-}
-
-func (c *NNTPClient) disconnect() {
if c.conn != nil {
+ c.conn.Write([]byte("QUIT\r\n"))
c.conn.Close()
}
- c.conn = nil
- c.reader = nil
c.isConnected = false
- c.connectedServer = ""
- c.isOnionService = false
}
-// Threading functions
-func buildThreads(articles []*ArticleOverview) []*Thread {
- log.Printf("๐Ÿงต Building threads from %d articles...", len(articles))
-
- // Maps for fast lookup
- articlesByMessageID := make(map[string]*ArticleOverview)
- articlesBySubject := make(map[string][]*ArticleOverview)
-
- // Index articles
- for _, article := range articles {
- if article.MessageID != "" {
- articlesByMessageID[article.MessageID] = article
- }
-
- // Normalize subject for threading
- normalizedSubject := normalizeSubject(article.Subject)
- articlesBySubject[normalizedSubject] = append(articlesBySubject[normalizedSubject], article)
- }
-
- // Build thread trees
- threadRoots := make(map[string]*ThreadNode)
- processedArticles := make(map[string]bool)
-
- for _, article := range articles {
- if processedArticles[article.MessageID] {
- continue
- }
-
- // Find root of this thread
- root := findOrCreateThreadRoot(article, articlesByMessageID, threadRoots)
- buildThreadTree(root, article, articlesByMessageID, processedArticles)
- }
-
- // Convert to Thread structs and sort
- var threads []*Thread
- for _, root := range threadRoots {
- thread := &Thread{
- Root: root,
- ArticleCount: countArticlesInThread(root),
- LastDate: findLatestDateInThread(root),
- Subject: root.Article.Subject,
- }
- threads = append(threads, thread)
- }
-
- // Sort threads by last activity
- sort.Slice(threads, func(i, j int) bool {
- return threads[i].LastDate.After(threads[j].LastDate)
- })
-
- log.Printf("โœ… Built %d threads from %d articles", len(threads), len(articles))
- return threads
-}
+// ============= Server Handlers =============
-func normalizeSubject(subject string) string {
- // Remove Re:, Fwd:, etc.
- subject = strings.TrimSpace(subject)
-
- // Common prefixes to remove
- prefixes := []string{
- "Re:", "RE:", "re:", "Fwd:", "FWD:", "fwd:",
- "Fw:", "FW:", "fw:", "AW:", "aw:", "Antw:",
- }
-
- for {
- trimmed := false
- for _, prefix := range prefixes {
- if strings.HasPrefix(subject, prefix) {
- subject = strings.TrimSpace(subject[len(prefix):])
- trimmed = true
- break
- }
- }
- if !trimmed {
- break
- }
+func (s *NewsServer) testTorProxy() error {
+ conn, err := net.DialTimeout("tcp", TorProxyAddr, 10*time.Second)
+ if err != nil {
+ return err
}
-
- return strings.ToLower(subject)
-}
+ defer conn.Close()
-func findOrCreateThreadRoot(article *ArticleOverview, articlesByMessageID map[string]*ArticleOverview, threadRoots map[string]*ThreadNode) *ThreadNode {
- // If no references, this is a root
- if article.References == "" {
- normalizedSubject := normalizeSubject(article.Subject)
- if root, exists := threadRoots[normalizedSubject]; exists {
- return root
- }
-
- root := &ThreadNode{
- Article: article,
- Level: 0,
- IsRoot: true,
- }
- threadRoots[normalizedSubject] = root
- return root
- }
-
- // Find the root by following references
- references := parseReferences(article.References)
- if len(references) == 0 {
- normalizedSubject := normalizeSubject(article.Subject)
- if root, exists := threadRoots[normalizedSubject]; exists {
- return root
- }
-
- root := &ThreadNode{
- Article: article,
- Level: 0,
- IsRoot: true,
- }
- threadRoots[normalizedSubject] = root
- return root
- }
-
- // The first reference is usually the thread root
- rootMessageID := references[0]
- if rootArticle, exists := articlesByMessageID[rootMessageID]; exists {
- normalizedSubject := normalizeSubject(rootArticle.Subject)
- if root, exists := threadRoots[normalizedSubject]; exists {
- return root
- }
-
- root := &ThreadNode{
- Article: rootArticle,
- Level: 0,
- IsRoot: true,
- }
- threadRoots[normalizedSubject] = root
- return root
- }
-
- // Fallback: use subject-based threading
- normalizedSubject := normalizeSubject(article.Subject)
- if root, exists := threadRoots[normalizedSubject]; exists {
- return root
- }
-
- root := &ThreadNode{
- Article: article,
- Level: 0,
- IsRoot: true,
+ conn.SetDeadline(time.Now().Add(10 * time.Second))
+ if _, err := conn.Write([]byte{0x05, 0x01, 0x00}); err != nil {
+ return err
}
- threadRoots[normalizedSubject] = root
- return root
-}
-func buildThreadTree(root *ThreadNode, article *ArticleOverview, articlesByMessageID map[string]*ArticleOverview, processedArticles map[string]bool) {
- if processedArticles[article.MessageID] {
- return
- }
-
- processedArticles[article.MessageID] = true
-
- // If this is the root article, we're done
- if root.Article.MessageID == article.MessageID {
- return
- }
-
- // Find where to insert this article in the tree
- insertLocation := findInsertLocation(root, article, articlesByMessageID)
-
- newNode := &ThreadNode{
- Article: article,
- Level: insertLocation.Level + 1,
- IsRoot: false,
+ resp := make([]byte, 2)
+ if _, err := io.ReadFull(conn, resp); err != nil {
+ return err
}
-
- insertLocation.Children = append(insertLocation.Children, newNode)
-
- // Sort children by date
- sort.Slice(insertLocation.Children, func(i, j int) bool {
- return insertLocation.Children[i].Article.ParsedDate.Before(insertLocation.Children[j].Article.ParsedDate)
- })
-}
-func findInsertLocation(root *ThreadNode, article *ArticleOverview, articlesByMessageID map[string]*ArticleOverview) *ThreadNode {
- references := parseReferences(article.References)
- if len(references) == 0 {
- return root
- }
-
- // Find the parent by looking for the last reference that exists in our tree
- var parent *ThreadNode = root
-
- for i := len(references) - 1; i >= 0; i-- {
- parentMessageID := references[i]
- if foundParent := findNodeByMessageID(root, parentMessageID); foundParent != nil {
- parent = foundParent
- break
- }
+ if resp[0] != 0x05 || resp[1] != 0x00 {
+ return fmt.Errorf("socks5 failed")
}
-
- return parent
-}
-func findNodeByMessageID(node *ThreadNode, messageID string) *ThreadNode {
- if node.Article.MessageID == messageID {
- return node
- }
-
- for _, child := range node.Children {
- if found := findNodeByMessageID(child, messageID); found != nil {
- return found
- }
- }
-
return nil
}
-func parseReferences(references string) []string {
- if references == "" {
+func (s *NewsServer) searchGroups(query string) []*NewsgroupInfo {
+ s.mutex.RLock()
+ defer s.mutex.RUnlock()
+
+ if query == "" {
return nil
}
-
- // Split by whitespace and clean up
- var messageIDs []string
- parts := strings.Fields(references)
-
- for _, part := range parts {
- part = strings.TrimSpace(part)
- if strings.HasPrefix(part, "<") && strings.HasSuffix(part, ">") {
- messageIDs = append(messageIDs, part)
- }
- }
-
- return messageIDs
-}
-func countArticlesInThread(node *ThreadNode) int {
- count := 1
- for _, child := range node.Children {
- count += countArticlesInThread(child)
+ pattern := strings.ToLower(query)
+ pattern = strings.ReplaceAll(pattern, ".", "\\.")
+ pattern = strings.ReplaceAll(pattern, "*", ".*")
+ pattern = strings.ReplaceAll(pattern, "?", ".")
+
+ if !strings.Contains(query, "*") && !strings.Contains(query, "?") {
+ pattern = ".*" + pattern + ".*"
}
- return count
-}
-func findLatestDateInThread(node *ThreadNode) time.Time {
- latest := node.Article.ParsedDate
-
- for _, child := range node.Children {
- childLatest := findLatestDateInThread(child)
- if childLatest.After(latest) {
- latest = childLatest
+ regex, err := regexp.Compile("^" + pattern + "$")
+ if err != nil {
+ var results []*NewsgroupInfo
+ queryLower := strings.ToLower(query)
+ for _, g := range s.groups {
+ if strings.Contains(strings.ToLower(g.Name), queryLower) {
+ results = append(results, g)
+ }
}
+ return results
}
-
- return latest
-}
-func flattenThread(node *ThreadNode) []*ArticleOverview {
- var articles []*ArticleOverview
- articles = append(articles, node.Article)
-
- for _, child := range node.Children {
- articles = append(articles, flattenThread(child)...)
+ var results []*NewsgroupInfo
+ for _, g := range s.groups {
+ if regex.MatchString(strings.ToLower(g.Name)) {
+ results = append(results, g)
+ }
}
-
- return articles
-}
-
-func (s *NewsServer) GetHealthStatus() map[string]interface{} {
- s.mutex.RLock()
- defer s.mutex.RUnlock()
- status := map[string]interface{}{
- "session": s.session,
- "timestamp": time.Now().UTC().Format("2006-01-02 15:04:05 UTC"),
- "features": s.features,
- "target": "peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion",
- "fallback": "news.tcpreset.net",
- "cached_groups": len(s.groups),
- }
+ sort.Slice(results, func(i, j int) bool {
+ return results[i].Name < results[j].Name
+ })
- if connectedServer, isOnion := s.client.GetConnectionInfo(); connectedServer != "" {
- status["connected_server"] = connectedServer
- status["is_onion_service"] = isOnion
- }
+ return results
+}
- if err := s.testTorProxy(); err != nil {
- status["overall"] = "degraded"
- status["tor_proxy"] = "failed"
- status["tor_error"] = err.Error()
- status["nntp_status"] = "untested"
- status["message"] = "Tor proxy is not reachable - check if Tor is running"
- return status
- }
-
- status["tor_proxy"] = "ok"
-
- if err := s.client.HealthCheck(); err != nil {
- status["overall"] = "partial"
- status["nntp_status"] = "disconnected"
- status["nntp_error"] = err.Error()
- status["http_status"] = "ok"
-
- if strings.Contains(err.Error(), "TTL expired") {
- status["message"] = "Onion service appears to be down or unreachable"
- } else if strings.Contains(err.Error(), "connection reset") {
- status["message"] = "Connection to onion service was reset"
- } else if strings.Contains(err.Error(), "timeout") {
- status["message"] = "Connection to onion service timed out"
- } else {
- status["message"] = "HTTP server running, NNTP connection will retry automatically"
- }
- } else {
- status["overall"] = "healthy"
- status["nntp_status"] = "connected"
- status["http_status"] = "ok"
- status["message"] = "All systems operational"
- }
+func (s *NewsServer) handleHome(w http.ResponseWriter, r *http.Request) {
+ s.mutex.RLock()
+ groupCount := len(s.groups)
+ s.mutex.RUnlock()
- return status
-}
+ torOK := s.testTorProxy() == nil
-func (s *NewsServer) testTorProxy() error {
- log.Printf("๐Ÿงช Testing Tor proxy connectivity...")
-
- conn, err := net.DialTimeout("tcp", "127.0.0.1:9050", 10*time.Second)
- if err != nil {
- return fmt.Errorf("Tor proxy not reachable: %v", err)
- }
- defer conn.Close()
-
- conn.SetDeadline(time.Now().Add(10 * time.Second))
-
- _, err = conn.Write([]byte{0x05, 0x01, 0x00})
- if err != nil {
- return fmt.Errorf("SOCKS5 handshake failed: %v", err)
- }
-
- resp := make([]byte, 2)
- _, err = io.ReadFull(conn, resp)
- if err != nil {
- return fmt.Errorf("SOCKS5 response failed: %v", err)
- }
-
- if resp[0] != 0x05 || resp[1] != 0x00 {
- return fmt.Errorf("SOCKS5 authentication failed: got %d,%d", resp[0], resp[1])
- }
-
- log.Printf("โœ… Tor proxy is reachable and responding")
- return nil
+ tmpl := template.Must(template.New("home").Parse(homeTemplate))
+ tmpl.Execute(w, map[string]interface{}{
+ "GroupCount": groupCount,
+ "TorOK": torOK,
+ "Session": s.session,
+ "M2UsenetURL": M2UsenetURL,
+ })
}
-func (s *NewsServer) downloadActiveFile() error {
+func (s *NewsServer) handleDownload(w http.ResponseWriter, r *http.Request) {
s.downloadStatus.mutex.Lock()
s.downloadStatus.IsDownloading = true
s.downloadStatus.StartTime = time.Now()
s.downloadStatus.LastError = ""
s.downloadStatus.Completed = false
+ s.downloadStatus.GroupCount = 0
s.downloadStatus.mutex.Unlock()
-
- log.Printf("๐Ÿ“ฅ Starting active file download with timeout...")
-
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
- defer cancel()
-
- resultChan := make(chan error, 1)
-
+
go func() {
- groups, err := s.client.ListGroups()
- if err != nil {
- resultChan <- err
- return
- }
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
+ defer cancel()
+
+ done := make(chan error, 1)
+ go func() {
+ groups, err := s.client.ListGroups()
+ if err != nil {
+ done <- err
+ return
+ }
+ s.mutex.Lock()
+ s.groups = groups
+ s.mutex.Unlock()
- s.mutex.Lock()
- s.groups = groups
- s.mutex.Unlock()
-
- s.downloadStatus.mutex.Lock()
- s.downloadStatus.GroupCount = len(groups)
- s.downloadStatus.mutex.Unlock()
-
- resultChan <- nil
- }()
-
- select {
- case err := <-resultChan:
- s.downloadStatus.mutex.Lock()
- s.downloadStatus.IsDownloading = false
- s.downloadStatus.mutex.Unlock()
-
- if err != nil {
- log.Printf("โŒ Active file download failed: %v", err)
s.downloadStatus.mutex.Lock()
- s.downloadStatus.LastError = err.Error()
+ s.downloadStatus.GroupCount = len(groups)
+ s.downloadStatus.mutex.Unlock()
+ done <- nil
+ }()
+
+ select {
+ case err := <-done:
+ s.downloadStatus.mutex.Lock()
+ s.downloadStatus.IsDownloading = false
+ if err != nil {
+ s.downloadStatus.LastError = err.Error()
+ } else {
+ s.downloadStatus.Completed = true
+ }
+ s.downloadStatus.mutex.Unlock()
+ case <-ctx.Done():
+ s.downloadStatus.mutex.Lock()
+ s.downloadStatus.IsDownloading = false
+ s.downloadStatus.LastError = "timeout after 10 minutes"
s.downloadStatus.mutex.Unlock()
- return err
}
- log.Printf("โœ… Active file download completed successfully")
- s.downloadStatus.mutex.Lock()
- s.downloadStatus.Completed = true
- s.downloadStatus.mutex.Unlock()
- return nil
- case <-ctx.Done():
- log.Printf("โฐ Active file download timed out after 5 minutes")
- s.downloadStatus.mutex.Lock()
- s.downloadStatus.IsDownloading = false
- s.downloadStatus.LastError = "Download timed out after 5 minutes"
- s.downloadStatus.mutex.Unlock()
- return fmt.Errorf("download timed out - onion service connection taking too long")
- }
+ }()
+
+ tmpl := template.Must(template.New("download").Parse(downloadingTemplate))
+ tmpl.Execute(w, nil)
}
func (s *NewsServer) handleDownloadStatus(w http.ResponseWriter, r *http.Request) {
s.downloadStatus.mutex.RLock()
- isDownloading := s.downloadStatus.IsDownloading
- startTime := s.downloadStatus.StartTime
- lastError := s.downloadStatus.LastError
- completed := s.downloadStatus.Completed
- groupCount := s.downloadStatus.GroupCount
- s.downloadStatus.mutex.RUnlock()
-
+ defer s.downloadStatus.mutex.RUnlock()
+
w.Header().Set("Content-Type", "application/json")
-
- status := map[string]interface{}{
- "is_downloading": isDownloading,
- "completed": completed,
- "group_count": groupCount,
- "timestamp": time.Now().Unix(),
+ fmt.Fprintf(w, `{"downloading":%t,"completed":%t,"groups":%d,"error":"%s","elapsed":%d}`,
+ s.downloadStatus.IsDownloading,
+ s.downloadStatus.Completed,
+ s.downloadStatus.GroupCount,
+ s.downloadStatus.LastError,
+ int(time.Since(s.downloadStatus.StartTime).Seconds()))
+}
+
+func (s *NewsServer) handleSearch(w http.ResponseWriter, r *http.Request) {
+ query := strings.TrimSpace(r.URL.Query().Get("q"))
+
+ var results []*NewsgroupInfo
+ if query != "" {
+ results = s.searchGroups(query)
}
-
- if !startTime.IsZero() {
- status["start_time"] = startTime.Unix()
- status["elapsed_seconds"] = int(time.Since(startTime).Seconds())
+
+ truncated := false
+ if len(results) > 500 {
+ results = results[:500]
+ truncated = true
}
-
- if lastError != "" {
- status["last_error"] = lastError
+
+ s.mutex.RLock()
+ totalGroups := len(s.groups)
+ s.mutex.RUnlock()
+
+ tmpl := template.Must(template.New("search").Parse(searchTemplate))
+ tmpl.Execute(w, map[string]interface{}{
+ "Query": query,
+ "Results": results,
+ "ResultCount": len(results),
+ "TotalGroups": totalGroups,
+ "Truncated": truncated,
+ "M2UsenetURL": M2UsenetURL,
+ })
+}
+
+// buildThreads organizes articles into a threaded structure
+func buildThreads(articles []*ArticleInfo) []*ArticleInfo {
+ if len(articles) == 0 {
+ return articles
}
-
- if isDownloading {
- status["message"] = "Download in progress..."
- } else if completed {
- status["message"] = "Download completed successfully"
- } else if lastError != "" {
- status["message"] = "Download failed"
- } else {
- status["message"] = "No download in progress"
+
+ // Create map of MessageID -> Article
+ byMsgID := make(map[string]*ArticleInfo)
+ for _, art := range articles {
+ msgID := art.MessageID
+ // Normalize message-id (ensure <> brackets)
+ if !strings.HasPrefix(msgID, "<") {
+ msgID = "<" + msgID
+ }
+ if !strings.HasSuffix(msgID, ">") {
+ msgID = msgID + ">"
+ }
+ byMsgID[msgID] = art
}
-
- w.Write([]byte(fmt.Sprintf(`{
- "is_downloading": %t,
- "completed": %t,
- "group_count": %d,
- "message": "%s",
- "timestamp": %d
- %s
- }`,
- isDownloading,
- completed,
- groupCount,
- status["message"],
- status["timestamp"],
- func() string {
- extras := ""
- if !startTime.IsZero() {
- extras += fmt.Sprintf(`,
- "elapsed_seconds": %d`, int(time.Since(startTime).Seconds()))
+
+ // Build parent-child relationships
+ var roots []*ArticleInfo
+ for _, art := range articles {
+ parentID := art.ParentID
+ // Normalize parent ID
+ if parentID != "" {
+ if !strings.HasPrefix(parentID, "<") {
+ parentID = "<" + parentID
}
- if lastError != "" {
- extras += fmt.Sprintf(`,
- "last_error": "%s"`, lastError)
+ if !strings.HasSuffix(parentID, ">") {
+ parentID = parentID + ">"
}
- return extras
- }(),
- )))
-}
+ }
-func (s *NewsServer) handleActive(w http.ResponseWriter, r *http.Request) {
- if r.URL.Query().Get("refresh") == "1" || r.Method == "POST" {
- log.Printf("๐Ÿ“ฅ Active file download requested...")
-
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
- processingHTML := `<!DOCTYPE html>
-<html>
-<head>
- <title>โณ Downloading... - Onion Newsreader</title>
- <meta charset="utf-8">
- <style>
- body { font-family: monospace; background: #0a0a0a; color: #ffaa00; padding: 20px; }
- .container { max-width: 800px; margin: 0 auto; text-align: center; }
- .spinner { animation: spin 1s linear infinite; display: inline-block; font-size: 2em; }
- @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
- .processing-box { background: #1a1a1a; padding: 30px; border-radius: 8px; border: 2px solid #ffaa00; margin: 20px 0; }
- .back-link { color: #00aaff; text-decoration: none; }
- .back-link:hover { color: #00ff00; }
- .status-update { margin: 15px 0; padding: 10px; background: #2a1a00; border-radius: 4px; }
- .success { color: #00ff00; }
- .error { color: #ff8800; }
- .progress { margin: 10px 0; font-size: 0.9em; color: #888; }
- .app-footer {
- margin-top: 40px;
- padding: 25px 0;
- border-top: 2px solid #333;
- background: #1a1a1a;
- text-align: center;
- }
- .footer-links {
- display: flex;
- justify-content: center;
- gap: 25px;
- flex-wrap: wrap;
- }
- .footer-links a {
- color: #00aaff;
- text-decoration: none;
- font-weight: 500;
- padding: 8px 15px;
- border-radius: 20px;
- background: rgba(0, 170, 255, 0.1);
- transition: all 0.3s ease;
- border: 1px solid rgba(0, 170, 255, 0.2);
- }
- .footer-links a:hover {
- background: #00aaff;
- color: #0a0a0a;
- }
- .footer-links a[href^="monero:"] {
- background: rgba(255, 102, 0, 0.1);
- color: #ff6600;
- border-color: rgba(255, 102, 0, 0.2);
- }
- .footer-links a[href^="monero:"]:hover {
- background: #ff6600;
- color: #0a0a0a;
- }
- .footer-links a[href*="github"] {
- background: rgba(51, 51, 51, 0.1);
- color: #00ff00;
- border-color: rgba(51, 51, 51, 0.2);
- }
- .footer-links a[href*="github"]:hover {
- background: #00ff00;
- color: #0a0a0a;
- }
- </style>
-</head>
-<body>
- <div class="container">
- <div class="processing-box">
- <h1><span class="spinner" id="spinner">๐Ÿ”„</span> <span id="status-title">Downloading Active File</span></h1>
- <p id="status-text"><strong>Connecting to NNTP server via Tor...</strong></p>
- <div class="status-update" id="status-details">
- <p>โฐ <strong>This may take 2-5 minutes</strong> due to onion service connection time</p>
- <p id="progress-text">๐Ÿ” Checking status...</p>
- </div>
- <p>
- <a href="/active" class="back-link">๐Ÿ”„ Check Progress</a> |
- <a href="/" class="back-link">โ† Back to Home</a> |
- <a href="/health" class="back-link">๐Ÿ“Š Health Status</a>
- </p>
- <p class="progress"><em>Started at ` + time.Now().Format("15:04:05") + `</em></p>
- </div>
-
- <footer class="app-footer">
- <div class="footer-links">
- <a href="https://github.com/gabrix73/onion-newsreader.git" target="_blank" rel="noopener">๐Ÿ“ Source Code</a>
- <a href="monero:44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L" onclick="fallbackCopyXMR(event)">โšก Support Project</a>
- <a href="https://www.virebent.art" target="_blank" rel="noopener">๐ŸŒ virebent.art</a>
- </div>
- </footer>
- </div>
- <script>
- let checkCount = 0;
-
- function updateStatus() {
- checkCount++;
- fetch('/download-status')
- .then(response => response.json())
- .then(data => {
- const spinner = document.getElementById('spinner');
- const statusTitle = document.getElementById('status-title');
- const statusText = document.getElementById('status-text');
- const progressText = document.getElementById('progress-text');
-
- if (data.completed) {
- spinner.innerHTML = 'โœ…';
- spinner.className = 'success';
- statusTitle.innerHTML = 'Download Completed!';
- statusText.innerHTML = '<strong>Successfully downloaded ' + data.group_count + ' newsgroups</strong>';
- progressText.innerHTML = '๐ŸŽ‰ Redirecting to active groups page...';
- setTimeout(() => { window.location.href = '/active'; }, 3000);
- } else if (data.last_error) {
- spinner.innerHTML = 'โŒ';
- spinner.className = 'error';
- statusTitle.innerHTML = 'Download Failed';
- statusText.innerHTML = '<strong>Error: ' + data.last_error + '</strong>';
- progressText.innerHTML = '๐Ÿ”„ <a href="/active?refresh=1" style="color: #00aaff;">Try Again</a>';
- } else if (data.is_downloading) {
- statusText.innerHTML = '<strong>Download in progress...</strong>';
- if (data.elapsed_seconds) {
- progressText.innerHTML = 'โฐ Running for ' + data.elapsed_seconds + ' seconds...';
- }
- } else {
- progressText.innerHTML = 'โฐ Waiting for download to start... (check ' + checkCount + ')';
- }
- })
- .catch(error => {
- console.log('Status check failed:', error);
- document.getElementById('progress-text').innerHTML = 'โš ๏ธ Status check failed, will retry...';
- });
- }
-
- updateStatus();
- const interval = setInterval(updateStatus, 3000);
-
- setTimeout(() => {
- clearInterval(interval);
- document.getElementById('progress-text').innerHTML = 'โฐ Status checking stopped - please refresh page manually';
- }, 10 * 60 * 1000);
-
- function fallbackCopyXMR(event) {
- const xmrAddress = "44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L";
-
- if (!navigator.userAgent.includes('Monero')) {
- event.preventDefault();
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(xmrAddress).then(() => {
- alert("โœ… XMR address copied to clipboard!");
- }).catch(() => {
- prompt("Copy XMR address:", xmrAddress);
- });
- } else {
- prompt("Copy XMR address:", xmrAddress);
- }
- }
- }
- </script>
-</body>
-</html>`
- w.Write([]byte(processingHTML))
-
- go func() {
- log.Printf("๐Ÿ”„ Starting background active file download...")
- err := s.downloadActiveFile()
- if err != nil {
- log.Printf("โŒ Background download failed: %v", err)
- } else {
- log.Printf("โœ… Background download completed successfully")
- }
- }()
-
- return
+ if parentID == "" {
+ // No parent - this is a root thread
+ roots = append(roots, art)
+ } else if parent, ok := byMsgID[parentID]; ok {
+ // Parent found in our set - add as child
+ parent.Children = append(parent.Children, art)
+ } else {
+ // Parent not in our set - treat as root
+ roots = append(roots, art)
+ }
}
- s.mutex.RLock()
- groupCount := len(s.groups)
- s.mutex.RUnlock()
+ // Sort roots by article number (newest first)
+ sort.Slice(roots, func(i, j int) bool {
+ return roots[i].Number > roots[j].Number
+ })
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ // Flatten the tree with depth information
+ var result []*ArticleInfo
+ var flatten func(art *ArticleInfo, depth int)
+ flatten = func(art *ArticleInfo, depth int) {
+ art.Depth = depth
+ result = append(result, art)
+ // Sort children by article number (oldest first within thread)
+ sort.Slice(art.Children, func(i, j int) bool {
+ return art.Children[i].Number < art.Children[j].Number
+ })
+ for _, child := range art.Children {
+ flatten(child, depth+1)
+ }
+ }
- activeHTML := `<!DOCTYPE html>
-<html>
-<head>
- <title>๐Ÿ“‹ Active Groups - Onion Newsreader</title>
- <meta charset="utf-8">
- <style>
- body { font-family: monospace; background: #0a0a0a; color: #00ff00; padding: 20px; }
- .container { max-width: 1000px; margin: 0 auto; }
- .header { margin-bottom: 20px; }
- .download-section { background: #1a1a1a; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
- .download-button { background: #333; color: #00ff00; border: none; padding: 12px 20px; border-radius: 4px; cursor: pointer; }
- .download-button:hover { background: #555; }
- .group-list { background: #1a1a1a; padding: 20px; border-radius: 8px; }
- .group-item { padding: 8px; margin: 3px 0; background: #0a0a0a; border-radius: 3px; }
- .group-name { color: #00aaff; }
- .group-stats { color: #888; font-size: 0.9em; }
- .back-link { color: #00aaff; text-decoration: none; }
- .back-link:hover { color: #00ff00; }
- .warning { background: #2a1a00; border: 1px solid #ffaa00; color: #ffaa00; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
- .success { background: #002a1a; border: 1px solid #00aa00; color: #00aa00; padding: 15px; border-radius: 8px; margin-bottom: 20px; }
- .app-footer {
- margin-top: 40px;
- padding: 25px 0;
- border-top: 2px solid #333;
- background: #1a1a1a;
- text-align: center;
- }
- .footer-links {
- display: flex;
- justify-content: center;
- gap: 25px;
- flex-wrap: wrap;
- }
- .footer-links a {
- color: #00aaff;
- text-decoration: none;
- font-weight: 500;
- padding: 8px 15px;
- border-radius: 20px;
- background: rgba(0, 170, 255, 0.1);
- transition: all 0.3s ease;
- border: 1px solid rgba(0, 170, 255, 0.2);
- }
- .footer-links a:hover {
- background: #00aaff;
- color: #0a0a0a;
- }
- .footer-links a[href^="monero:"] {
- background: rgba(255, 102, 0, 0.1);
- color: #ff6600;
- border-color: rgba(255, 102, 0, 0.2);
- }
- .footer-links a[href^="monero:"]:hover {
- background: #ff6600;
- color: #0a0a0a;
- }
- .footer-links a[href*="github"] {
- background: rgba(51, 51, 51, 0.1);
- color: #00ff00;
- border-color: rgba(51, 51, 51, 0.2);
- }
- .footer-links a[href*="github"]:hover {
- background: #00ff00;
- color: #0a0a0a;
- }
- </style>
-</head>
-<body>
- <div class="container">
- <div class="header">
- <h1>๐Ÿ“‹ Active Newsgroups</h1>
- <p><a href="/" class="back-link">โ† Back to Home</a> | <a href="/health" class="back-link">๐Ÿ“Š Health Status</a></p>
- </div>
+ for _, root := range roots {
+ flatten(root, 0)
+ }
- {{if eq .GroupCount 0}}
- <div class="warning">
- <h3>โš ๏ธ No Groups Cached</h3>
- <p>No newsgroups are currently cached. You need to download the active file first.</p>
- <p><em>Note: This may take 2-5 minutes due to onion service connection time.</em></p>
- </div>
- {{else}}
- <div class="success">
- <h3>โœ… Groups Loaded</h3>
- <p>Currently cached: <strong>{{.GroupCount}}</strong> newsgroups</p>
- <p>You can now browse groups or download a fresh list.</p>
- </div>
- {{end}}
+ return result
+}
- <div class="download-section">
- <h3>๐Ÿ“ก Download Active File</h3>
- <p>Current groups in cache: <strong>{{.GroupCount}}</strong></p>
- <form method="post" onsubmit="this.querySelector('button').innerHTML='โณ Starting...'; this.querySelector('button').disabled=true;">
- <button type="submit" class="download-button">๐Ÿ”„ START DOWNLOAD</button>
- </form>
- <p><em>This will download the complete list of available newsgroups from the server.</em></p>
- <p><strong>โฐ Please be patient:</strong> Onion service connections can take 2-5 minutes. The page will show progress.</p>
- </div>
+func (s *NewsServer) handleGroup(w http.ResponseWriter, r *http.Request) {
+ groupName := strings.TrimPrefix(r.URL.Path, "/group/")
+ if groupName == "" {
+ http.Redirect(w, r, "/search", http.StatusFound)
+ return
+ }
- {{if .HasGroups}}
- <div class="group-list">
- <h3>๐Ÿ“Š Available Groups (showing first 100)</h3>
- {{range .Groups}}
- <div class="group-item">
- <div class="group-name">
- <a href="/group/{{.Name}}" style="color: #00aaff; text-decoration: none;">{{.Name}}</a>
- </div>
- <div class="group-stats">Range: {{.Low}}-{{.High}} ({{.Status}})</div>
- </div>
- {{end}}
- </div>
- {{end}}
-
- <footer class="app-footer">
- <div class="footer-links">
- <a href="https://github.com/gabrix73/onion-newsreader.git" target="_blank" rel="noopener">๐Ÿ“ Source Code</a>
- <a href="monero:44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L" onclick="fallbackCopyXMR(event)">โšก Support Project</a>
- <a href="https://www.virebent.art" target="_blank" rel="noopener">๐ŸŒ virebent.art</a>
- </div>
- </footer>
- </div>
-
- <script>
- function fallbackCopyXMR(event) {
- const xmrAddress = "44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L";
-
- if (!navigator.userAgent.includes('Monero')) {
- event.preventDefault();
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(xmrAddress).then(() => {
- alert("โœ… XMR address copied to clipboard!");
- }).catch(() => {
- prompt("Copy XMR address:", xmrAddress);
- });
- } else {
- prompt("Copy XMR address:", xmrAddress);
- }
- }
- }
- </script>
-</body>
-</html>`
+ var articles []*ArticleInfo
+ var connErr string
- var displayGroups []*NewsgroupInfo
- if groupCount > 0 {
- s.mutex.RLock()
- for _, group := range s.groups {
- if len(displayGroups) >= 100 {
- break
+ if err := s.client.ensureConnected(); err != nil {
+ connErr = err.Error()
+ } else {
+ _, low, high, err := s.client.SelectGroup(groupName)
+ if err != nil {
+ connErr = err.Error()
+ } else {
+ articles, err = s.client.GetRecentArticles(low, high, 100) // Get more for threading
+ if err != nil {
+ connErr = err.Error()
}
- displayGroups = append(displayGroups, group)
}
- s.mutex.RUnlock()
-
- sort.Slice(displayGroups, func(i, j int) bool {
- return displayGroups[i].Name < displayGroups[j].Name
- })
}
- tmpl, _ := template.New("active").Parse(activeHTML)
- data := map[string]interface{}{
- "GroupCount": groupCount,
- "Groups": displayGroups,
- "HasGroups": len(displayGroups) > 0,
+ // Compute ReplySubject and ReplyRef for each article
+ for _, art := range articles {
+ // Subject: add "Re: " only if not already present
+ subj := art.Subject
+ if !strings.HasPrefix(strings.ToLower(subj), "re:") {
+ subj = "Re: " + subj
+ }
+ art.ReplySubject = subj
+
+ // MessageID: ensure <> brackets
+ msgID := art.MessageID
+ if msgID != "" {
+ if !strings.HasPrefix(msgID, "<") {
+ msgID = "<" + msgID
+ }
+ if !strings.HasSuffix(msgID, ">") {
+ msgID = msgID + ">"
+ }
+ }
+ art.ReplyRef = msgID
}
- tmpl.Execute(w, data)
+
+ // Build threaded view
+ articles = buildThreads(articles)
+
+ tmpl := template.Must(template.New("group").Parse(groupTemplate))
+ tmpl.Execute(w, map[string]interface{}{
+ "GroupName": groupName,
+ "Articles": articles,
+ "Error": connErr,
+ "M2UsenetURL": M2UsenetURL,
+ })
}
-func (s *NewsServer) handleHealth(w http.ResponseWriter, r *http.Request) {
- status := s.GetHealthStatus()
-
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
-
- healthHTML := `<!DOCTYPE html>
-<html>
-<head>
- <title>๐Ÿ“Š System Health - Onion Newsreader</title>
- <meta charset="utf-8">
- <style>
- body { font-family: monospace; background: #0a0a0a; color: #00ff00; padding: 40px; }
- .status-ok { color: #00ff00; }
- .status-warn { color: #ffaa00; }
- .status-error { color: #ff0000; }
- .status-degraded { color: #ffaa00; }
- .status-partial { color: #ffaa00; }
- .status-connected { color: #00ff00; }
- .status-disconnected { color: #ff8800; }
- .status-failed { color: #ff0000; }
- .container { max-width: 800px; margin: 0 auto; }
- .status-item { margin: 10px 0; padding: 10px; background: #1a1a1a; border-radius: 5px; }
- .diagnostic-section { background: #2a2a1a; padding: 15px; border-radius: 5px; margin: 20px 0; }
- .diagnostic-test { margin: 8px 0; padding: 8px; background: #1a1a1a; border-radius: 3px; font-size: 0.9em; }
- .back-link { color: #00aaff; text-decoration: none; }
- .back-link:hover { color: #00ff00; }
- .test-button { background: #333; color: #00ff00; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; margin: 5px; }
- .test-button:hover { background: #555; }
- .app-footer {
- margin-top: 40px;
- padding: 25px 0;
- border-top: 2px solid #333;
- background: #1a1a1a;
- text-align: center;
- }
- .footer-links {
- display: flex;
- justify-content: center;
- gap: 25px;
- flex-wrap: wrap;
- }
- .footer-links a {
- color: #00aaff;
- text-decoration: none;
- font-weight: 500;
- padding: 8px 15px;
- border-radius: 20px;
- background: rgba(0, 170, 255, 0.1);
- transition: all 0.3s ease;
- border: 1px solid rgba(0, 170, 255, 0.2);
- }
- .footer-links a:hover {
- background: #00aaff;
- color: #0a0a0a;
- }
- .footer-links a[href^="monero:"] {
- background: rgba(255, 102, 0, 0.1);
- color: #ff6600;
- border-color: rgba(255, 102, 0, 0.2);
- }
- .footer-links a[href^="monero:"]:hover {
- background: #ff6600;
- color: #0a0a0a;
- }
- .footer-links a[href*="github"] {
- background: rgba(51, 51, 51, 0.1);
- color: #00ff00;
- border-color: rgba(51, 51, 51, 0.2);
- }
- .footer-links a[href*="github"]:hover {
- background: #00ff00;
- color: #0a0a0a;
- }
- </style>
-</head>
-<body>
- <div class="container">
- <h1>๐Ÿ“Š System Health Status</h1>
-
- <div class="status-item">
- <strong>Overall Status:</strong>
- <span class="status-{{.OverallClass}}">{{.Overall}}</span>
- </div>
-
- {{if .TorProxy}}
- <div class="status-item">
- <strong>Tor Proxy:</strong>
- <span class="status-{{.TorProxyClass}}">{{.TorProxy}}</span>
- {{if .TorError}}
- <br><small style="color: #ff8800;">{{.TorError}}</small>
- {{end}}
- </div>
- {{end}}
-
- <div class="status-item">
- <strong>NNTP Connection:</strong>
- <span class="status-{{.NNTPClass}}">{{.NNTPStatus}}</span>
- {{if .NNTPError}}
- <br><small style="color: #ff8800;">{{.NNTPError}}</small>
- {{end}}
- </div>
-
- <div class="status-item">
- <strong>Target Server:</strong> {{.Target}}
- <br><strong>Fallback Server:</strong> {{.Fallback}} (via Tor)
- {{if .ConnectedServer}}
- <br><strong>Currently Connected:</strong> {{.ConnectedServer}}
- {{if .IsOnionService}}(๐Ÿง… Onion Service){{else}}(๐ŸŒ Via Tor){{end}}
- {{end}}
- </div>
-
- <div class="status-item">
- <strong>Cached Groups:</strong> {{.CachedGroups}}
- </div>
-
- <div class="status-item">
- <strong>Timestamp:</strong> {{.Timestamp}}
- </div>
-
- <div class="status-item">
- <strong>Session:</strong> {{.Session}}
- </div>
-
- {{if .Message}}
- <div class="status-item">
- <strong>Status:</strong> {{.Message}}
- </div>
- {{end}}
-
- <div class="diagnostic-section">
- <h3>๐Ÿ”ง Diagnostic Tools</h3>
- <p>Manual tests you can run to troubleshoot connectivity:</p>
-
- <div class="diagnostic-test">
- <strong>1. Test Tor connectivity (v3 onion):</strong><br>
- <code>curl --socks5 127.0.0.1:9050 http://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion</code>
- </div>
-
- <div class="diagnostic-test">
- <strong>2a. Test primary onion service:</strong><br>
- <code>timeout 120 curl --socks5 127.0.0.1:9050 -v telnet://peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion:119</code>
- </div>
-
- <div class="diagnostic-test">
- <strong>2b. Test fallback NNTP server:</strong><br>
- <code>timeout 60 curl --socks5 127.0.0.1:9050 -v telnet://news.tcpreset.net:119</code>
- </div>
-
- <div class="diagnostic-test">
- <strong>3. Check Tor service:</strong><br>
- <code>systemctl status tor</code> or <code>ps aux | grep tor</code>
- </div>
-
- <div class="diagnostic-test">
- <strong>4. Check Tor logs:</strong><br>
- <code>journalctl -u tor -f</code> or <code>tail -f /var/log/tor/tor.log</code>
- </div>
-
- <p style="margin-top: 15px;"><em>If Tor proxy test fails, Tor is not running or configured correctly.</em></p>
- <p><em>If onion service test fails, the target service may be down.</em></p>
- <p><em>Try the <a href="/test-onion" class="back-link">Interactive Test Page</a> for detailed diagnostics.</em></p>
- </div>
-
- <div class="status-item">
- <strong>Features:</strong>
- <ul>
- <li>โœ… Auto-reconnect enabled</li>
- <li>โœ… MODE READER support</li>
- <li>โœ… Health monitoring</li>
- <li>โœ… Connection retry logic</li>
- <li>โœ… Broken pipe recovery</li>
- <li>โœ… Diagnostic testing</li>
- <li>โœ… Thread-based article grouping</li>
- </ul>
- </div>
-
- <p><a href="/" class="back-link">โ† Back to Home</a> | <a href="/active" class="back-link">Active Groups</a></p>
-
- <footer class="app-footer">
- <div class="footer-links">
- <a href="https://github.com/gabrix73/onion-newsreader.git" target="_blank" rel="noopener">๐Ÿ“ Source Code</a>
- <a href="monero:44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L" onclick="fallbackCopyXMR(event)">โšก Support Project</a>
- <a href="https://www.virebent.art" target="_blank" rel="noopener">๐ŸŒ virebent.art</a>
- </div>
- </footer>
- </div>
-
- <script>
- setTimeout(function() {
- window.location.reload();
- }, 30000);
-
- function fallbackCopyXMR(event) {
- const xmrAddress = "44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L";
-
- if (!navigator.userAgent.includes('Monero') {
- event.preventDefault();
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(xmrAddress).then(() => {
- alert("โœ… XMR address copied to clipboard!");
- }).catch(() => {
- prompt("Copy XMR address:", xmrAddress);
- });
- } else {
- prompt("Copy XMR address:", xmrAddress);
- }
- }
- }
- </script>
-</body>
-</html>`
+func (s *NewsServer) handleArticle(w http.ResponseWriter, r *http.Request) {
+ // URL format: /article/group.name/12345
+ path := strings.TrimPrefix(r.URL.Path, "/article/")
+ parts := strings.Split(path, "/")
- tmpl, _ := template.New("health").Parse(healthHTML)
-
- data := map[string]interface{}{
- "Overall": status["overall"],
- "OverallClass": strings.ToLower(fmt.Sprintf("%v", status["overall"])),
- "NNTPStatus": status["nntp_status"],
- "NNTPClass": strings.ToLower(fmt.Sprintf("%v", status["nntp_status"])),
- "Target": status["target"],
- "Fallback": status["fallback"],
- "CachedGroups": status["cached_groups"],
- "Timestamp": status["timestamp"],
- "Session": status["session"],
- "Message": status["message"],
- }
-
- if connectedServer, exists := status["connected_server"]; exists {
- data["ConnectedServer"] = connectedServer
- }
- if isOnionService, exists := status["is_onion_service"]; exists {
- data["IsOnionService"] = isOnionService
- }
-
- if torProxy, exists := status["tor_proxy"]; exists {
- data["TorProxy"] = torProxy
- data["TorProxyClass"] = strings.ToLower(fmt.Sprintf("%v", torProxy))
+ if len(parts) < 2 {
+ http.Redirect(w, r, "/search", http.StatusFound)
+ return
}
-
- if nntpError, exists := status["nntp_error"]; exists {
- data["NNTPError"] = nntpError
+
+ groupName := strings.Join(parts[:len(parts)-1], "/")
+ articleNum, err := strconv.Atoi(parts[len(parts)-1])
+ if err != nil {
+ http.Error(w, "Invalid article number", http.StatusBadRequest)
+ return
}
-
- if torError, exists := status["tor_error"]; exists {
- data["TorError"] = torError
+
+ var headers map[string]string
+ var body string
+ var connErr string
+
+ if err := s.client.ensureConnected(); err != nil {
+ connErr = err.Error()
+ } else {
+ _, _, _, err := s.client.SelectGroup(groupName)
+ if err != nil {
+ connErr = err.Error()
+ } else {
+ headers, body, err = s.client.GetArticle(articleNum)
+ if err != nil {
+ connErr = err.Error()
+ }
+ }
}
-
- tmpl.Execute(w, data)
-}
-func (s *NewsServer) handleTestOnion(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
-
- testHTML := `<!DOCTYPE html>
-<html>
-<head>
- <title>๐Ÿงช Onion Service Test - Onion Newsreader</title>
- <meta charset="utf-8">
- <style>
- body { font-family: monospace; background: #0a0a0a; color: #00ff00; padding: 20px; }
- .container { max-width: 800px; margin: 0 auto; }
- .test-box { background: #1a1a1a; padding: 20px; border-radius: 8px; margin: 20px 0; border: 1px solid #333; }
- .back-link { color: #00aaff; text-decoration: none; }
- .back-link:hover { color: #00ff00; }
- .test-button { background: #333; color: #00ff00; border: none; padding: 12px 20px; border-radius: 4px; cursor: pointer; margin: 5px; }
- .test-button:hover { background: #555; }
- .result { margin: 15px 0; padding: 15px; border-radius: 5px; }
- .result-success { background: #002a1a; border: 1px solid #00aa00; color: #00aa00; }
- .result-error { background: #2a1a1a; border: 1px solid #aa0000; color: #aa0000; }
- .result-info { background: #1a2a2a; border: 1px solid #0088aa; color: #0088aa; }
- .spinner { animation: spin 1s linear infinite; display: inline-block; }
- @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
- .test-progress { display: none; }
- pre { background: #0a0a0a; padding: 10px; border-radius: 3px; overflow-x: auto; }
- .app-footer {
- margin-top: 40px;
- padding: 25px 0;
- border-top: 2px solid #333;
- background: #1a1a1a;
- text-align: center;
- }
- .footer-links {
- display: flex;
- justify-content: center;
- gap: 25px;
- flex-wrap: wrap;
- }
- .footer-links a {
- color: #00aaff;
- text-decoration: none;
- font-weight: 500;
- padding: 8px 15px;
- border-radius: 20px;
- background: rgba(0, 170, 255, 0.1);
- transition: all 0.3s ease;
- border: 1px solid rgba(0, 170, 255, 0.2);
- }
- .footer-links a:hover {
- background: #00aaff;
- color: #0a0a0a;
- }
- .footer-links a[href^="monero:"] {
- background: rgba(255, 102, 0, 0.1);
- color: #ff6600;
- border-color: rgba(255, 102, 0, 0.2);
- }
- .footer-links a[href^="monero:"]:hover {
- background: #ff6600;
- color: #0a0a0a;
- }
- .footer-links a[href*="github"] {
- background: rgba(51, 51, 51, 0.1);
- color: #00ff00;
- border-color: rgba(51, 51, 51, 0.2);
- }
- .footer-links a[href*="github"]:hover {
- background: #00ff00;
- color: #0a0a0a;
- }
- </style>
-</head>
-<body>
- <div class="container">
- <h1>๐Ÿงช Onion Service Connectivity Test</h1>
- <p><a href="/" class="back-link">โ† Back to Home</a> | <a href="/health" class="back-link">Health Status</a></p>
-
- <div class="test-box">
- <h3>๐Ÿ” Manual Connectivity Tests</h3>
- <p>These tests help diagnose onion service connectivity issues:</p>
-
- <button onclick="testTorProxy()" class="test-button">๐ŸŒ Test Tor Proxy</button>
- <button onclick="testDuckDuckGo()" class="test-button">๐Ÿฆ† Test DuckDuckGo Onion</button>
- <button onclick="testNNTPOnion()" class="test-button">๐Ÿ“ก Test NNTP Onion Service</button>
-
- <div id="test-results"></div>
- </div>
-
- <div class="test-box">
- <h3>๐Ÿ“Š Known Working Test Services (v3 Onion Addresses)</h3>
- <p>These onion services should work if your Tor is configured correctly:</p>
- <ul>
- <li><strong>DuckDuckGo:</strong> <code>duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion</code></li>
- <li><strong>Facebook:</strong> <code>facebookwkhpilnemxj7asaniu7vnjjbiltxjqhye3mhbshg7kx5tfyd.onion</code></li>
- <li><strong>ProPublica:</strong> <code>p53lf57qovyuvwsc6xnrppddxpr23otqjafwze2xypdgc3gvhxoylvvid.onion</code></li>
- </ul>
- <p><em>โš ๏ธ These are v3 onion addresses (56 characters) - v2 addresses (16 chars) are deprecated since 2021</em></p>
- </div>
-
- <div class="test-box">
- <h3>๐ŸŽฏ Target Servers</h3>
- <p><strong>Primary target:</strong> <code>peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion:119</code></p>
- <p><strong>Fallback server:</strong> <code>news.tcpreset.net:119</code> via Tor (anonymous access)</p>
- <p><strong>Connection method:</strong> All connections routed through Tor SOCKS5 proxy</p>
-
- <div style="margin-top: 15px; padding: 10px; background: #1a2a1a; border-radius: 5px;">
- <strong>โ„น๏ธ Connection priority:</strong>
- <ol style="margin: 10px 0 0 20px;">
- <li>First tries the onion service for maximum anonymity</li>
- <li>If onion service fails, falls back to <code>news.tcpreset.net</code> via Tor</li>
- <li>All traffic is encrypted and routed through Tor network</li>
- <li>Your IP address is hidden from NNTP servers</li>
- </ol>
- </div>
-
- <div style="margin-top: 15px; padding: 10px; background: #2a1a00; border-radius: 5px;">
- <strong>โš ๏ธ Common issues:</strong>
- <ul style="margin: 10px 0 0 20px;">
- <li>Onion service temporarily offline (TTL expired)</li>
- <li>Network connectivity issues</li>
- <li>Tor circuit building problems</li>
- <li>Fallback server configuration/access issues</li>
- </ul>
- </div>
- </div>
-
- <div class="test-box">
- <h3>๐Ÿ“ก NNTP Protocol Information</h3>
- <p><strong>Protocol type:</strong> NNTP Client (newsreader) with <strong>Thread Support</strong></p>
- <p><strong>Commands used:</strong></p>
- <ul style="margin: 10px 0 0 20px; font-family: monospace; font-size: 0.9em;">
- <li><code>MODE READER</code> - Enter reader mode</li>
- <li><code>LIST</code> - Get newsgroup list</li>
- <li><code>GROUP &lt;name&gt;</code> - Select newsgroup</li>
- <li><code>XOVER &lt;range&gt;</code> - Get article overview</li>
- <li><code>ARTICLE &lt;id&gt;</code> - Retrieve article</li>
- <li><code>DATE</code>/<code>HELP</code> - Health check</li>
- <li><code>QUIT</code> - Close connection</li>
- </ul>
- <p><strong>Features:</strong> Thread-based article grouping using References headers</p>
-
- <div style="margin-top: 15px; padding: 10px; background: #1a2a1a; border-radius: 5px;">
- <strong>๐Ÿงต Threading Features:</strong>
- <ul style="margin: 10px 0 0 20px;">
- <li>Automatic thread building using References headers</li>
- <li>Subject-based fallback threading</li>
- <li>Hierarchical reply display with indentation</li>
- <li>Chronological sorting within threads</li>
- </ul>
- </div>
- </div>
-
- <div class="test-box">
- <h3>๐Ÿ”ง Command Line Tests</h3>
- <p>You can also test manually from command line:</p>
- <pre># Test Tor connectivity
-curl --socks5 127.0.0.1:9050 http://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion
-
-# Test NNTP onion service
-timeout 120 curl --socks5 127.0.0.1:9050 -v \\
- telnet://peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion:119
-
-# Test fallback server (anonymous access)
-timeout 60 curl --socks5 127.0.0.1:9050 -v \\
- telnet://news.tcpreset.net:119</pre>
- </div>
-
- <div class="test-box">
- <h3>๐Ÿ’ก Troubleshooting Tips</h3>
- <ul>
- <li>If Tor proxy test fails: Tor is not running or misconfigured</li>
- <li>If DuckDuckGo test fails: Network/Tor circuit issues</li>
- <li>If NNTP onion test fails: The specific NNTP service may be down</li>
- <li>Try again in a few minutes - onion services can be slow/unreliable</li>
- <li>Check Tor logs: <code>journalctl -u tor -f</code></li>
- </ul>
- </div>
-
- <footer class="app-footer">
- <div class="footer-links">
- <a href="https://github.com/gabrix73/onion-newsreader.git" target="_blank" rel="noopener">๐Ÿ“ Source Code</a>
- <a href="monero:44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L" onclick="fallbackCopyXMR(event)">โšก Support Project</a>
- <a href="https://www.virebent.art" target="_blank" rel="noopener">๐ŸŒ virebent.art</a>
- </div>
- </footer>
- </div>
-
- <script>
- function showResult(type, message) {
- const resultsDiv = document.getElementById('test-results');
- const resultClass = type === 'success' ? 'result-success' :
- type === 'error' ? 'result-error' : 'result-info';
- resultsDiv.innerHTML += '<div class="result ' + resultClass + '">' + message + '</div>';
- }
-
- function showProgress(message) {
- const resultsDiv = document.getElementById('test-results');
- resultsDiv.innerHTML += '<div class="result result-info"><span class="spinner">๐Ÿ”„</span> ' + message + '</div>';
- }
-
- async function testTorProxy() {
- showProgress('Testing Tor proxy connectivity...');
- try {
- const response = await fetch('/health');
- const text = await response.text();
- if (text.includes('status-ok') || text.includes('Tor Proxy')) {
- showResult('success', 'โœ… Tor proxy appears to be working');
- } else {
- showResult('error', 'โŒ Tor proxy test inconclusive');
- }
- } catch (error) {
- showResult('error', 'โŒ Could not test Tor proxy: ' + error.message);
- }
- }
-
- async function testDuckDuckGo() {
- showProgress('Testing DuckDuckGo onion service (this may take 30-60 seconds)...');
- showResult('info', '๐Ÿ’ก Manual test required - run: curl --socks5 127.0.0.1:9050 http://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion');
- showResult('info', 'โš ๏ธ Note: This is the current v3 onion address (v2 addresses are deprecated since 2021)');
- }
-
- async function testNNTPOnion() {
- showProgress('Testing NNTP onion service (this may take 2-3 minutes)...');
- try {
- const response = await fetch('/download-status');
- const data = await response.json();
- showResult('info', '๐Ÿ“Š Current NNTP status: ' + data.message);
- } catch (error) {
- showResult('error', 'โŒ Could not check NNTP status: ' + error.message);
- }
-
- showResult('info', '๐Ÿ’ก For full test, try downloading the active file from the main page');
- }
+ // Build reply URL
+ replyURL := ""
+ if headers != nil {
+ subject := headers["Subject"]
+ if subject != "" && !strings.HasPrefix(strings.ToLower(subject), "re:") {
+ subject = "Re: " + subject
+ }
+ msgID := headers["Message-ID"]
+ if msgID != "" && !strings.HasPrefix(msgID, "<") {
+ msgID = "<" + msgID + ">"
+ }
- function fallbackCopyXMR(event) {
- const xmrAddress = "44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L";
-
- if (!navigator.userAgent.includes('Monero')) {
- event.preventDefault();
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(xmrAddress).then(() => {
- alert("โœ… XMR address copied to clipboard!");
- }).catch(() => {
- prompt("Copy XMR address:", xmrAddress);
- });
- } else {
- prompt("Copy XMR address:", xmrAddress);
- }
- }
- }
- </script>
-</body>
-</html>`
-
- w.Write([]byte(testHTML))
+ replyURL = fmt.Sprintf("%s?newsgroups=%s&subject=%s&references=%s",
+ M2UsenetURL,
+ url.QueryEscape(groupName),
+ url.QueryEscape(subject),
+ url.QueryEscape(msgID))
+ }
+
+ tmpl := template.Must(template.New("article").Parse(articleTemplate))
+ tmpl.Execute(w, map[string]interface{}{
+ "GroupName": groupName,
+ "ArticleNum": articleNum,
+ "Headers": headers,
+ "Body": body,
+ "Error": connErr,
+ "ReplyURL": replyURL,
+ "M2UsenetURL": M2UsenetURL,
+ })
}
-func (s *NewsServer) handleHome(w http.ResponseWriter, r *http.Request) {
+func (s *NewsServer) handleHealth(w http.ResponseWriter, r *http.Request) {
+ torOK := s.testTorProxy() == nil
+
+ var nntpStatus, nntpServer string
+ if err := s.client.ensureConnected(); err != nil {
+ nntpStatus = "disconnected"
+ } else {
+ nntpStatus = "connected"
+ nntpServer = s.client.connectedServer
+ }
+
s.mutex.RLock()
groupCount := len(s.groups)
s.mutex.RUnlock()
-
- connectionStatus := "unknown"
- torStatus := "unknown"
-
- if err := s.testTorProxy(); err != nil {
- torStatus = "failed"
- connectionStatus = "tor-proxy-failed"
- } else {
- torStatus = "ok"
- if err := s.client.HealthCheck(); err != nil {
- connectionStatus = "nntp-disconnected"
- } else {
- connectionStatus = "connected"
- }
- }
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ tmpl := template.Must(template.New("health").Parse(healthTemplate))
+ tmpl.Execute(w, map[string]interface{}{
+ "TorOK": torOK,
+ "NNTPStatus": nntpStatus,
+ "NNTPServer": nntpServer,
+ "GroupCount": groupCount,
+ "Session": s.session,
+ "Timestamp": time.Now().Format("2006-01-02 15:04:05 UTC"),
+ })
+}
+
+// ============= Templates =============
- homeHTML := `<!DOCTYPE html>
-<html lang="en">
+const homeTemplate = `<!DOCTYPE html>
+<html>
<head>
- <meta charset="utf-8">
- <meta name="viewport" content="width=device-width, initial-scale=1">
<title>๐Ÿง… Onion Newsreader</title>
+ <meta charset="utf-8">
<style>
- * { margin: 0; padding: 0; box-sizing: border-box; }
- body {
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
- background: #0a0a0a;
- color: #00ff00;
- line-height: 1.6;
- }
- .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
- .header {
- background: #1a1a1a;
- padding: 20px;
- border-radius: 8px;
- margin-bottom: 20px;
- border: 1px solid #333;
- }
- .header h1 { color: #00ff00; margin-bottom: 10px; }
- .header p { color: #888; }
- .status {
- background: #1a1a1a;
- padding: 15px;
- border-radius: 8px;
- margin-bottom: 20px;
- border: 1px solid #333;
- }
- .search-box {
- background: #1a1a1a;
- padding: 20px;
- border-radius: 8px;
- margin-bottom: 20px;
- border: 1px solid #333;
- }
- .search-box input {
- width: 100%;
- padding: 12px;
- background: #0a0a0a;
- border: 1px solid #333;
- color: #00ff00;
- border-radius: 4px;
- font-family: monospace;
- }
- .search-box button {
- padding: 12px 20px;
- background: #333;
- border: none;
- color: #00ff00;
- border-radius: 4px;
- cursor: pointer;
- margin-top: 10px;
- font-family: monospace;
- }
- .search-box button:hover { background: #555; }
- .actions {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
- gap: 15px;
- margin-bottom: 20px;
- }
- .action-card {
- background: #1a1a1a;
- padding: 20px;
- border-radius: 8px;
- border: 1px solid #333;
- text-align: center;
- }
- .action-card a {
- color: #00aaff;
- text-decoration: none;
- font-weight: bold;
- }
- .action-card a:hover { color: #00ff00; }
- .footer {
- text-align: center;
- color: #666;
- margin-top: 40px;
- padding: 20px;
- }
- .status-good { color: #00ff00; }
- .status-warn { color: #ffaa00; }
- .status-error { color: #ff0000; }
- .status-unknown { color: #888; }
- .alert { padding: 15px; border-radius: 8px; margin-bottom: 20px; }
- .alert-error { background: #2a1a1a; border: 1px solid #ff8800; color: #ff8800; }
- .alert-warning { background: #2a2a1a; border: 1px solid #ffaa00; color: #ffaa00; }
- .alert-info { background: #1a2a2a; border: 1px solid #00aaff; color: #00aaff; }
- .app-footer {
- margin-top: 40px;
- padding: 25px 0;
- border-top: 2px solid #333;
- background: #1a1a1a;
- text-align: center;
- }
- .footer-links {
- display: flex;
- justify-content: center;
- gap: 25px;
- flex-wrap: wrap;
- }
- .footer-links a {
- color: #00aaff;
- text-decoration: none;
- font-weight: 500;
- padding: 8px 15px;
- border-radius: 20px;
- background: rgba(0, 170, 255, 0.1);
- transition: all 0.3s ease;
- border: 1px solid rgba(0, 170, 255, 0.2);
- }
- .footer-links a:hover {
- background: #00aaff;
- color: #0a0a0a;
- }
- .footer-links a[href^="monero:"] {
- background: rgba(255, 102, 0, 0.1);
- color: #ff6600;
- border-color: rgba(255, 102, 0, 0.2);
- }
- .footer-links a[href^="monero:"]:hover {
- background: #ff6600;
- color: #0a0a0a;
- }
- .footer-links a[href*="github"] {
- background: rgba(51, 51, 51, 0.1);
- color: #00ff00;
- border-color: rgba(51, 51, 51, 0.2);
- }
- .footer-links a[href*="github"]:hover {
- background: #00ff00;
- color: #0a0a0a;
- }
+ body { font-family: monospace; background: #0a0a0a; color: #0f0; padding: 20px; }
+ .container { max-width: 800px; margin: 0 auto; }
+ .box { background: #1a1a1a; padding: 20px; border-radius: 8px; margin: 15px 0; border: 1px solid #333; }
+ .btn { display: inline-block; background: #333; color: #0f0; padding: 12px 24px; border-radius: 4px; text-decoration: none; margin: 5px; border: none; cursor: pointer; font-family: monospace; }
+ .btn:hover { background: #555; }
+ .btn-primary { background: #004400; border: 1px solid #0f0; }
+ .status-ok { color: #0f0; }
+ .status-err { color: #f80; }
+ a { color: #0af; text-decoration: none; }
+ a:hover { color: #0f0; }
+ h1 { color: #0f0; }
</style>
</head>
<body>
<div class="container">
- <div class="header">
- <h1>๐Ÿง… Onion Newsreader</h1>
- <p>Anonymous NNTP Client via Tor โ€ข Secure Newsgroup Access โ€ข Thread Support</p>
- </div>
-
- {{if eq .ConnectionStatus "tor-proxy-failed"}}
- <div class="alert alert-error">
- <h3>โŒ Tor Connection Failed</h3>
- <p>Cannot connect to Tor proxy at 127.0.0.1:9050</p>
- <p>Please ensure Tor is installed and running: <code>systemctl start tor</code></p>
- </div>
- {{else if eq .ConnectionStatus "nntp-disconnected"}}
- <div class="alert alert-warning">
- <h3>โš ๏ธ NNTP Services Unreachable</h3>
- <p>Tor proxy is working but neither the onion service nor fallback server are reachable.</p>
- <p><strong>Primary target:</strong> <code>peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion</code></p>
- <p><strong>Fallback server:</strong> <code>news.tcpreset.net</code> via Tor (anonymous access)</p>
- <p>This may be temporary - services might be down or experiencing high load.</p>
- <p><a href="/test-onion" style="color: #00aaff;">๐Ÿงช Run diagnostic tests</a> to troubleshoot.</p>
- </div>
- {{else if eq .ConnectionStatus "connected"}}
- <div class="alert alert-info">
- <h3>โœ… All Systems Operational</h3>
- <p>Tor proxy and NNTP service are both reachable.</p>
- </div>
- {{end}}
+ <h1>๐Ÿง… Onion Newsreader</h1>
+ <p>Minimal NNTP client via Tor</p>
- <div class="status">
+ <div class="box">
<h3>๐Ÿ“Š Status</h3>
- <p>๐Ÿ“ก Primary: <code>peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion</code></p>
- <p>๐Ÿ”„ Fallback: <code>news.tcpreset.net</code> via Tor (anonymous access)</p>
- <p>๐ŸŒ Tor Proxy: <span class="status-{{.TorStatusClass}}">{{.TorStatus}}</span></p>
- <p>๐Ÿ“ก NNTP: <span class="status-{{.ConnectionStatusClass}}">{{.ConnectionStatus}}</span></p>
- <p>๐Ÿ“Š Cached Groups: <span class="status-good">{{.GroupCount}}</span></p>
- <p>๐Ÿงต Threading: <span class="status-good">Enabled</span></p>
- <p>๐Ÿ”— Session: <code>{{.Session}}</code></p>
- <p>โšก <a href="/health" style="color: #00aaff;">Detailed Health Check</a></p>
+ <p>Tor Proxy: {{if .TorOK}}<span class="status-ok">โœ… OK</span>{{else}}<span class="status-err">โŒ Failed</span>{{end}}</p>
+ <p>Cached Groups: <strong>{{.GroupCount}}</strong></p>
+ <p>Session: <code>{{.Session}}</code></p>
</div>
- {{if ne .ConnectionStatus "tor-proxy-failed"}}
- <div class="search-box">
- <h3>๐Ÿ” Search Newsgroups</h3>
- <form action="/search" method="get">
- <input type="text" name="q" placeholder="Search newsgroups (e.g., comp.*, alt.*, *.linux.*)" required>
- <button type="submit">Search Groups</button>
- </form>
+ <div class="box">
+ <h3>๐Ÿ”ง Actions</h3>
+ <a href="/download" class="btn btn-primary">๐Ÿ“ฅ Download Active File</a>
+ <a href="/search" class="btn">๐Ÿ” Search Groups</a>
+ <a href="/health" class="btn">๐Ÿ” Health Check</a>
</div>
- <div class="actions">
- <div class="action-card">
- <h4>๐Ÿ“‹ Browse Groups</h4>
- <p><a href="/active">View All Groups</a></p>
- <p>Browse available newsgroups</p>
- </div>
-
- <div class="action-card">
- <h4>๐Ÿ”„ Refresh</h4>
- <p><a href="/active?refresh=1">Refresh Group List</a></p>
- <p>Update newsgroup cache</p>
- </div>
-
- <div class="action-card">
- <h4>๐Ÿ“Š System Info</h4>
- <p><a href="/health">Health Status</a></p>
- <p>Connection and system status</p>
- </div>
-
- <div class="action-card">
- <h4>๐Ÿงช Diagnostics</h4>
- <p><a href="/test-onion">Test Onion Services</a></p>
- <p>Connectivity troubleshooting</p>
- </div>
-
- <div class="action-card">
- <h4>๐Ÿ” Quick Search</h4>
- <p><a href="/search?q=comp.*">comp.*</a> | <a href="/search?q=alt.*">alt.*</a></p>
- <p><a href="/search?q=*linux*">*linux*</a> | <a href="/search?q=sci.*">sci.*</a></p>
- <p>Popular newsgroup categories</p>
- </div>
+ <div class="box">
+ <h3>โœ๏ธ Post New Message</h3>
+ <p>Create a new post (opens m2usenet):</p>
+ <a href="{{.M2UsenetURL}}" target="_blank" class="btn btn-primary">๐Ÿ“ New Post</a>
</div>
- {{else}}
- <div class="alert alert-error">
- <h3>๐Ÿ”ง Troubleshooting Steps</h3>
- <ol>
- <li>Install Tor: <code>apt-get install tor</code></li>
- <li>Start Tor service: <code>systemctl start tor</code></li>
- <li>Enable Tor service: <code>systemctl enable tor</code></li>
- <li>Check Tor status: <code>systemctl status tor</code></li>
- <li>Refresh this page after fixing Tor</li>
- </ol>
- </div>
- {{end}}
- <div class="footer">
- <p>๐Ÿ”’ All connections are routed through Tor for maximum anonymity</p>
- <p>๐Ÿง… Primary: Onion service โ€ข ๐Ÿ”„ Fallback: news.tcpreset.net via Tor (anonymous)</p>
- <p>๐Ÿงต Thread-based article organization for better discussion flow</p>
- <p>โœ๏ธ Posting URL: <code>http://itcxzfm2h36hfj6j7qxksyfm4ipp3co4rkl62sgge7hp6u77lbretiyd.onion:8880/</code></p>
+ <div class="box">
+ <h3>โ„น๏ธ Info</h3>
+ <p>Primary: <code>peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion</code></p>
+ <p>Fallback: <code>news.tcpreset.net</code> (via Tor)</p>
+ <p>Posting: <code>{{.M2UsenetURL}}</code></p>
</div>
-
- <footer class="app-footer">
- <div class="footer-links">
- <a href="https://github.com/gabrix73/onion-newsreader.git" target="_blank" rel="noopener">๐Ÿ“ Source Code</a>
- <a href="monero:44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L" onclick="fallbackCopyXMR(event)">โšก Support Project</a>
- <a href="https://www.virebent.art" target="_blank" rel="noopener">๐ŸŒ virebent.art</a>
- </div>
- </footer>
</div>
-
- <script>
- function fallbackCopyXMR(event) {
- const xmrAddress = "44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L";
-
- if (!navigator.userAgent.includes('Monero')) {
- event.preventDefault();
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(xmrAddress).then(() => {
- alert("โœ… XMR address copied to clipboard!");
- }).catch(() => {
- prompt("Copy XMR address:", xmrAddress);
- });
- } else {
- prompt("Copy XMR address:", xmrAddress);
- }
- }
- }
- </script>
</body>
</html>`
- tmpl, _ := template.New("home").Parse(homeHTML)
-
- torStatusClass := "unknown"
- connectionStatusClass := "unknown"
-
- switch torStatus {
- case "ok":
- torStatusClass = "good"
- case "failed":
- torStatusClass = "error"
- }
-
- switch connectionStatus {
- case "connected":
- connectionStatusClass = "good"
- case "nntp-disconnected":
- connectionStatusClass = "warn"
- case "tor-proxy-failed":
- connectionStatusClass = "error"
- }
-
- data := map[string]interface{}{
- "GroupCount": groupCount,
- "Session": s.session,
- "ConnectionStatus": connectionStatus,
- "ConnectionStatusClass": connectionStatusClass,
- "TorStatus": torStatus,
- "TorStatusClass": torStatusClass,
- }
- tmpl.Execute(w, data)
-}
-
-func (s *NewsServer) handleSearch(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
-
- query := strings.TrimSpace(r.URL.Query().Get("q"))
-
- if query == "" {
- // Show search form
- searchFormHTML := `<!DOCTYPE html>
+const downloadingTemplate = `<!DOCTYPE html>
<html>
<head>
- <title>๐Ÿ” Search Newsgroups - Onion Newsreader</title>
+ <title>โณ Downloading...</title>
<meta charset="utf-8">
<style>
- body { font-family: monospace; background: #0a0a0a; color: #00ff00; padding: 20px; }
- .container { max-width: 800px; margin: 0 auto; }
- .search-box { background: #1a1a1a; padding: 20px; border-radius: 8px; margin: 20px 0; border: 1px solid #333; }
- .search-box input { width: 100%; padding: 12px; background: #0a0a0a; border: 1px solid #333; color: #00ff00; border-radius: 4px; font-family: monospace; }
- .search-box button { padding: 12px 20px; background: #333; border: none; color: #00ff00; border-radius: 4px; cursor: pointer; margin-top: 10px; font-family: monospace; }
- .search-box button:hover { background: #555; }
- .back-link { color: #00aaff; text-decoration: none; }
- .back-link:hover { color: #00ff00; }
- .examples { background: #1a1a1a; padding: 15px; border-radius: 8px; margin: 20px 0; border: 1px solid #333; }
- .examples ul { margin: 10px 0 0 20px; }
- .examples li { margin: 5px 0; }
- .examples code { background: #0a0a0a; padding: 2px 4px; border-radius: 2px; }
- .app-footer {
- margin-top: 40px;
- padding: 25px 0;
- border-top: 2px solid #333;
- background: #1a1a1a;
- text-align: center;
- }
- .footer-links {
- display: flex;
- justify-content: center;
- gap: 25px;
- flex-wrap: wrap;
- }
- .footer-links a {
- color: #00aaff;
- text-decoration: none;
- font-weight: 500;
- padding: 8px 15px;
- border-radius: 20px;
- background: rgba(0, 170, 255, 0.1);
- transition: all 0.3s ease;
- border: 1px solid rgba(0, 170, 255, 0.2);
- }
- .footer-links a:hover {
- background: #00aaff;
- color: #0a0a0a;
- }
- .footer-links a[href^="monero:"] {
- background: rgba(255, 102, 0, 0.1);
- color: #ff6600;
- border-color: rgba(255, 102, 0, 0.2);
- }
- .footer-links a[href^="monero:"]:hover {
- background: #ff6600;
- color: #0a0a0a;
- }
- .footer-links a[href*="github"] {
- background: rgba(51, 51, 51, 0.1);
- color: #00ff00;
- border-color: rgba(51, 51, 51, 0.2);
- }
- .footer-links a[href*="github"]:hover {
- background: #00ff00;
- color: #0a0a0a;
- }
- </style>
-</head>
-<body>
- <div class="container">
- <h1>๐Ÿ” Search Newsgroups</h1>
- <p><a href="/" class="back-link">โ† Back to Home</a></p>
-
- <div class="search-box">
- <h3>Enter Search Query</h3>
- <form action="/search" method="get">
- <input type="text" name="q" placeholder="Search newsgroups (e.g., comp.*, alt.*, *.linux.*)" autofocus>
- <button type="submit">๐Ÿ” Search</button>
- </form>
- </div>
-
- <div class="examples">
- <h3>๐Ÿ“ Search Examples</h3>
- <ul>
- <li><code>comp.*</code> - All computer-related groups</li>
- <li><code>alt.*</code> - All alternative groups</li>
- <li><code>*.linux.*</code> - All Linux-related groups</li>
- <li><code>rec.sport.*</code> - All recreation sport groups</li>
- <li><code>*security*</code> - All groups containing "security"</li>
- <li><code>news.*</code> - All news groups</li>
- <li><code>sci.*</code> - All science groups</li>
- <li><code>soc.*</code> - All social groups</li>
- </ul>
- <p><strong>Wildcards:</strong> Use <code>*</code> to match any characters</p>
- <p><strong>Case:</strong> Search is case-insensitive</p>
- </div>
-
- <footer class="app-footer">
- <div class="footer-links">
- <a href="https://github.com/gabrix73/onion-newsreader.git" target="_blank" rel="noopener">๐Ÿ“ Source Code</a>
- <a href="monero:44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L" onclick="fallbackCopyXMR(event)">โšก Support Project</a>
- <a href="https://www.virebent.art" target="_blank" rel="noopener">๐ŸŒ virebent.art</a>
- </div>
- </footer>
- </div>
-
- <script>
- function fallbackCopyXMR(event) {
- const xmrAddress = "44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L";
-
- if (!navigator.userAgent.includes('Monero')) {
- event.preventDefault();
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(xmrAddress).then(() => {
- alert("โœ… XMR address copied to clipboard!");
- }).catch(() => {
- prompt("Copy XMR address:", xmrAddress);
- });
- } else {
- prompt("Copy XMR address:", xmrAddress);
- }
- }
- }
- </script>
-</body>
-</html>`
- w.Write([]byte(searchFormHTML))
- return
- }
-
- // Perform search
- s.mutex.RLock()
- allGroups := s.groups
- groupCount := len(allGroups)
- s.mutex.RUnlock()
-
- if groupCount == 0 {
- noGroupsHTML := `<!DOCTYPE html>
-<html>
-<head>
- <title>๐Ÿ” Search Results - Onion Newsreader</title>
- <meta charset="utf-8">
- <style>
- body { font-family: monospace; background: #0a0a0a; color: #00ff00; padding: 20px; }
- .container { max-width: 800px; margin: 0 auto; }
- .back-link { color: #00aaff; text-decoration: none; }
- .back-link:hover { color: #00ff00; }
- .warning { background: #2a1a00; border: 1px solid #ffaa00; color: #ffaa00; padding: 15px; border-radius: 8px; margin: 20px 0; }
- .app-footer {
- margin-top: 40px;
- padding: 25px 0;
- border-top: 2px solid #333;
- background: #1a1a1a;
- text-align: center;
- }
- .footer-links {
- display: flex;
- justify-content: center;
- gap: 25px;
- flex-wrap: wrap;
- }
- .footer-links a {
- color: #00aaff;
- text-decoration: none;
- font-weight: 500;
- padding: 8px 15px;
- border-radius: 20px;
- background: rgba(0, 170, 255, 0.1);
- transition: all 0.3s ease;
- border: 1px solid rgba(0, 170, 255, 0.2);
- }
- .footer-links a:hover {
- background: #00aaff;
- color: #0a0a0a;
- }
- .footer-links a[href^="monero:"] {
- background: rgba(255, 102, 0, 0.1);
- color: #ff6600;
- border-color: rgba(255, 102, 0, 0.2);
- }
- .footer-links a[href^="monero:"]:hover {
- background: #ff6600;
- color: #0a0a0a;
- }
- .footer-links a[href*="github"] {
- background: rgba(51, 51, 51, 0.1);
- color: #00ff00;
- border-color: rgba(51, 51, 51, 0.2);
- }
- .footer-links a[href*="github"]:hover {
- background: #00ff00;
- color: #0a0a0a;
- }
+ body { font-family: monospace; background: #0a0a0a; color: #0f0; padding: 20px; text-align: center; }
+ .box { background: #1a1a1a; padding: 30px; border-radius: 8px; max-width: 500px; margin: 50px auto; border: 1px solid #f80; }
+ .spinner { animation: spin 1s linear infinite; display: inline-block; font-size: 2em; }
+ @keyframes spin { 100% { transform: rotate(360deg); } }
+ a { color: #0af; }
+ #status { margin: 20px 0; padding: 15px; background: #0a0a0a; border-radius: 4px; }
</style>
</head>
<body>
- <div class="container">
- <h1>๐Ÿ” Search Results</h1>
- <p><a href="/" class="back-link">โ† Back to Home</a> | <a href="/search" class="back-link">๐Ÿ” New Search</a></p>
-
- <div class="warning">
- <h3>โš ๏ธ No Groups Available</h3>
- <p>No newsgroups are currently cached. You need to download the active file first.</p>
- <p><a href="/active?refresh=1" style="color: #00aaff;">๐Ÿ“ฅ Download Active File</a></p>
- </div>
-
- <footer class="app-footer">
- <div class="footer-links">
- <a href="https://github.com/gabrix73/onion-newsreader.git" target="_blank" rel="noopener">๐Ÿ“ Source Code</a>
- <a href="monero:44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L" onclick="fallbackCopyXMR(event)">โšก Support Project</a>
- <a href="https://www.virebent.art" target="_blank" rel="noopener">๐ŸŒ virebent.art</a>
- </div>
- </footer>
+ <div class="box">
+ <h2><span class="spinner">๐Ÿ”„</span> Downloading Active File</h2>
+ <p>This may take 2-10 minutes via Tor...</p>
+ <div id="status">โณ Starting...</div>
+ <p><a href="/">โ† Back to Home</a></p>
</div>
-
<script>
- function fallbackCopyXMR(event) {
- const xmrAddress = "44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L";
-
- if (!navigator.userAgent.includes('Monero')) {
- event.preventDefault();
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(xmrAddress).then(() => {
- alert("โœ… XMR address copied to clipboard!");
- }).catch(() => {
- prompt("Copy XMR address:", xmrAddress);
+ function checkStatus() {
+ fetch('/download-status')
+ .then(r => r.json())
+ .then(d => {
+ const el = document.getElementById('status');
+ if (d.completed) {
+ el.innerHTML = 'โœ… Done! ' + d.groups + ' groups loaded';
+ el.style.color = '#0f0';
+ setTimeout(() => location.href = '/search', 2000);
+ } else if (d.error) {
+ el.innerHTML = 'โŒ Error: ' + d.error;
+ el.style.color = '#f00';
+ } else if (d.downloading) {
+ el.innerHTML = 'โณ Downloading... (' + d.elapsed + 's) - ' + d.groups + ' groups so far';
+ }
});
- } else {
- prompt("Copy XMR address:", xmrAddress);
- }
}
- }
+ setInterval(checkStatus, 2000);
+ checkStatus();
</script>
</body>
</html>`
- w.Write([]byte(noGroupsHTML))
- return
- }
-
- // Convert search pattern to regex
- pattern := strings.ToLower(query)
- pattern = strings.ReplaceAll(pattern, ".", "\\.")
- pattern = strings.ReplaceAll(pattern, "*", ".*")
- pattern = "^" + pattern + "$"
-
- log.Printf("๐Ÿ” Searching for pattern: %s (regex: %s)", query, pattern)
-
- var matches []*NewsgroupInfo
- for _, group := range allGroups {
- if matched, _ := regexp.MatchString(pattern, strings.ToLower(group.Name)); matched {
- matches = append(matches, group)
- }
- }
-
- // Sort results
- sort.Slice(matches, func(i, j int) bool {
- return matches[i].Name < matches[j].Name
- })
-
- // Limit results to prevent huge pages
- maxResults := 500
- truncated := false
- if len(matches) > maxResults {
- matches = matches[:maxResults]
- truncated = true
- }
-
- log.Printf("๐Ÿ” Search completed: %d matches for '%s'", len(matches), query)
-
- searchResultsHTML := `<!DOCTYPE html>
+
+const searchTemplate = `<!DOCTYPE html>
<html>
<head>
- <title>๐Ÿ” Search Results - Onion Newsreader</title>
+ <title>๐Ÿ” Search Groups</title>
<meta charset="utf-8">
<style>
- body { font-family: monospace; background: #0a0a0a; color: #00ff00; padding: 20px; }
- .container { max-width: 1000px; margin: 0 auto; }
- .search-header { background: #1a1a1a; padding: 20px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #333; }
- .search-box { margin: 15px 0; }
- .search-box input { width: 70%; padding: 8px; background: #0a0a0a; border: 1px solid #333; color: #00ff00; border-radius: 4px; font-family: monospace; }
- .search-box button { padding: 8px 16px; background: #333; border: none; color: #00ff00; border-radius: 4px; cursor: pointer; margin-left: 10px; }
- .search-box button:hover { background: #555; }
- .results-summary { background: #1a1a1a; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #333; }
- .group-list { background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333; }
- .group-item { padding: 8px; margin: 3px 0; background: #0a0a0a; border-radius: 3px; }
- .group-name { color: #00aaff; }
- .group-stats { color: #888; font-size: 0.9em; }
- .back-link { color: #00aaff; text-decoration: none; }
- .back-link:hover { color: #00ff00; }
- .warning { background: #2a1a00; border: 1px solid #ffaa00; color: #ffaa00; padding: 15px; border-radius: 8px; margin: 20px 0; }
- .info { background: #1a2a2a; border: 1px solid #0088aa; color: #0088aa; padding: 15px; border-radius: 8px; margin: 20px 0; }
- .app-footer {
- margin-top: 40px;
- padding: 25px 0;
- border-top: 2px solid #333;
- background: #1a1a1a;
- text-align: center;
- }
- .footer-links {
- display: flex;
- justify-content: center;
- gap: 25px;
- flex-wrap: wrap;
- }
- .footer-links a {
- color: #00aaff;
- text-decoration: none;
- font-weight: 500;
- padding: 8px 15px;
- border-radius: 20px;
- background: rgba(0, 170, 255, 0.1);
- transition: all 0.3s ease;
- border: 1px solid rgba(0, 170, 255, 0.2);
- }
- .footer-links a:hover {
- background: #00aaff;
- color: #0a0a0a;
- }
- .footer-links a[href^="monero:"] {
- background: rgba(255, 102, 0, 0.1);
- color: #ff6600;
- border-color: rgba(255, 102, 0, 0.2);
- }
- .footer-links a[href^="monero:"]:hover {
- background: #ff6600;
- color: #0a0a0a;
- }
- .footer-links a[href*="github"] {
- background: rgba(51, 51, 51, 0.1);
- color: #00ff00;
- border-color: rgba(51, 51, 51, 0.2);
- }
- .footer-links a[href*="github"]:hover {
- background: #00ff00;
- color: #0a0a0a;
- }
+ body { font-family: monospace; background: #0a0a0a; color: #0f0; padding: 20px; }
+ .container { max-width: 900px; margin: 0 auto; }
+ .search-box { background: #1a1a1a; padding: 20px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #333; }
+ .search-input { width: 70%; padding: 12px; background: #0a0a0a; border: 1px solid #333; color: #0f0; font-family: monospace; font-size: 1em; }
+ .search-btn { padding: 12px 24px; background: #004400; border: 1px solid #0f0; color: #0f0; cursor: pointer; font-family: monospace; }
+ .search-btn:hover { background: #006600; }
+ .group { padding: 10px; margin: 5px 0; background: #1a1a1a; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; }
+ .group:hover { background: #2a2a2a; }
+ .group-name { color: #0af; font-weight: bold; }
+ .group-stats { color: #666; font-size: 0.9em; }
+ a { color: #0af; text-decoration: none; }
+ a:hover { color: #0f0; }
+ .btn { display: inline-block; background: #004400; color: #0f0; padding: 6px 12px; border-radius: 3px; font-size: 0.9em; }
+ .btn:hover { background: #006600; }
+ .info { color: #888; margin: 15px 0; }
+ .examples { background: #0a0a0a; padding: 10px; border-radius: 4px; margin-top: 10px; }
+ .examples code { color: #f80; }
</style>
</head>
<body>
<div class="container">
- <div class="search-header">
- <h1>๐Ÿ” Search Results</h1>
- <p><a href="/" class="back-link">โ† Back to Home</a> | <a href="/search" class="back-link">๐Ÿ” New Search</a> | <a href="/active" class="back-link">๐Ÿ“‹ All Groups</a></p>
-
- <div class="search-box">
- <form action="/search" method="get">
- <input type="text" name="q" value="{{.Query}}" placeholder="Search newsgroups...">
- <button type="submit">๐Ÿ” Search</button>
- </form>
+ <h1>๐Ÿ” Search Newsgroups</h1>
+ <p><a href="/">โ† Home</a> | Total groups: <strong>{{.TotalGroups}}</strong></p>
+
+ <div class="search-box">
+ <form action="/search" method="get">
+ <input type="text" name="q" value="{{.Query}}" class="search-input" placeholder="Search pattern (e.g., comp.*, *linux*, alt.binaries.?)" autofocus>
+ <button type="submit" class="search-btn">๐Ÿ” Search</button>
+ </form>
+ <div class="examples">
+ <strong>Examples:</strong>
+ <code>comp.*</code> |
+ <code>alt.binaries.*</code> |
+ <code>*linux*</code> |
+ <code>*privacy*</code> |
+ <code>sci.physics.*</code>
</div>
</div>
- <div class="results-summary">
- <h3>๐Ÿ“Š Search Results</h3>
- <p><strong>Query:</strong> <code>{{.Query}}</code></p>
- <p><strong>Matches:</strong> {{.MatchCount}} out of {{.TotalGroups}} total groups</p>
- {{if .Truncated}}
- <p><strong>โš ๏ธ Results limited to first {{.MaxResults}} matches - try a more specific search</strong></p>
- {{end}}
- </div>
+ {{if .Query}}
+ <p class="info">Found <strong>{{.ResultCount}}</strong> groups matching "<code>{{.Query}}</code>"{{if .Truncated}} (showing first 500){{end}}</p>
+ {{end}}
- {{if eq .MatchCount 0}}
- <div class="info">
- <h3>โ„น๏ธ No Matches Found</h3>
- <p>No newsgroups match your search pattern: <code>{{.Query}}</code></p>
- <p><strong>Tips:</strong></p>
- <ul>
- <li>Try using wildcards: <code>comp.*</code>, <code>*linux*</code>, <code>alt.*</code></li>
- <li>Check spelling and case (search is case-insensitive)</li>
- <li>Use broader patterns: <code>*security*</code> instead of <code>comp.security.*</code></li>
- </ul>
- </div>
- {{else}}
- <div class="group-list">
- <h3>๐Ÿ“‹ Matching Groups</h3>
- {{range .Groups}}
- <div class="group-item">
- <div class="group-name">
- <a href="/group/{{.Name}}" style="color: #00aaff; text-decoration: none;">{{.Name}}</a>
- </div>
- <div class="group-stats">Range: {{.Low}}-{{.High}} ({{.Status}}) โ€ข Articles: {{.ArticleCount}}</div>
+ {{if .Results}}
+ <div id="results">
+ {{range .Results}}
+ <div class="group">
+ <div>
+ <a href="/group/{{.Name}}" class="group-name">{{.Name}}</a>
+ <span class="group-stats">{{.Low}}-{{.High}} ({{.Status}})</span>
</div>
- {{end}}
+ <a href="{{$.M2UsenetURL}}?newsgroups={{.Name}}" target="_blank" class="btn">๐Ÿ“ Post</a>
</div>
{{end}}
-
- <footer class="app-footer">
- <div class="footer-links">
- <a href="https://github.com/gabrix73/onion-newsreader.git" target="_blank" rel="noopener">๐Ÿ“ Source Code</a>
- <a href="monero:44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L" onclick="fallbackCopyXMR(event)">โšก Support Project</a>
- <a href="https://www.virebent.art" target="_blank" rel="noopener">๐ŸŒ virebent.art</a>
- </div>
- </footer>
+ </div>
+ {{else if .Query}}
+ <p>No groups found. Try different patterns like <code>*keyword*</code></p>
+ {{else}}
+ <p class="info">Enter a search pattern above. Use <code>*</code> for wildcards.</p>
+ {{end}}
</div>
-
- <script>
- function fallbackCopyXMR(event) {
- const xmrAddress = "44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L";
-
- if (!navigator.userAgent.includes('Monero')) {
- event.preventDefault();
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(xmrAddress).then(() => {
- alert("โœ… XMR address copied to clipboard!");
- }).catch(() => {
- prompt("Copy XMR address:", xmrAddress);
- });
- } else {
- prompt("Copy XMR address:", xmrAddress);
- }
- }
- }
- </script>
</body>
</html>`
- tmpl, _ := template.New("search").Parse(searchResultsHTML)
-
- // Calculate article counts for display
- for _, group := range matches {
- if group.High > group.Low {
- group.ArticleCount = group.High - group.Low + 1
- } else {
- group.ArticleCount = 0
- }
- }
-
- data := map[string]interface{}{
- "Query": query,
- "Groups": matches,
- "MatchCount": len(matches),
- "TotalGroups": groupCount,
- "Truncated": truncated,
- "MaxResults": maxResults,
- }
- tmpl.Execute(w, data)
-}
-
-func (s *NewsServer) handleGroup(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
-
- // Extract group name from URL path
- path := strings.TrimPrefix(r.URL.Path, "/group/")
- if path == "" {
- http.Redirect(w, r, "/active", http.StatusSeeOther)
- return
- }
-
- groupName := path
- log.Printf("๐Ÿ” Viewing group: %s", groupName)
-
- // Check if group exists in cache
- s.mutex.RLock()
- groupInfo, exists := s.groups[groupName]
- s.mutex.RUnlock()
-
- if !exists {
- notFoundHTML := `<!DOCTYPE html>
+const groupTemplate = `<!DOCTYPE html>
<html>
<head>
- <title>โŒ Group Not Found - Onion Newsreader</title>
+ <title>๐Ÿ“ฐ {{.GroupName}}</title>
<meta charset="utf-8">
<style>
- body { font-family: monospace; background: #0a0a0a; color: #00ff00; padding: 20px; }
- .container { max-width: 800px; margin: 0 auto; }
- .back-link { color: #00aaff; text-decoration: none; }
- .back-link:hover { color: #00ff00; }
- .error { background: #2a1a1a; border: 1px solid #aa0000; color: #aa0000; padding: 15px; border-radius: 8px; margin: 20px 0; }
- .app-footer {
- margin-top: 40px;
- padding: 25px 0;
- border-top: 2px solid #333;
- background: #1a1a1a;
- text-align: center;
- }
- .footer-links {
- display: flex;
- justify-content: center;
- gap: 25px;
- flex-wrap: wrap;
- }
- .footer-links a {
- color: #00aaff;
- text-decoration: none;
- font-weight: 500;
- padding: 8px 15px;
- border-radius: 20px;
- background: rgba(0, 170, 255, 0.1);
- transition: all 0.3s ease;
- border: 1px solid rgba(0, 170, 255, 0.2);
- }
- .footer-links a:hover {
- background: #00aaff;
- color: #0a0a0a;
- }
- .footer-links a[href^="monero:"] {
- background: rgba(255, 102, 0, 0.1);
- color: #ff6600;
- border-color: rgba(255, 102, 0, 0.2);
- }
- .footer-links a[href^="monero:"]:hover {
- background: #ff6600;
- color: #0a0a0a;
- }
- .footer-links a[href*="github"] {
- background: rgba(51, 51, 51, 0.1);
- color: #00ff00;
- border-color: rgba(51, 51, 51, 0.2);
- }
- .footer-links a[href*="github"]:hover {
- background: #00ff00;
- color: #0a0a0a;
- }
+ body { font-family: monospace; background: #0a0a0a; color: #0f0; padding: 20px; }
+ .container { max-width: 900px; margin: 0 auto; }
+ .article { padding: 10px 12px; margin: 4px 0; background: #1a1a1a; border-radius: 4px; border-left: 3px solid #333; }
+ .article:hover { background: #2a2a2a; }
+ .article.depth-0 { border-left-color: #0f0; background: #1a1a1a; }
+ .article.depth-1 { border-left-color: #0af; margin-left: 20px; }
+ .article.depth-2 { border-left-color: #f80; margin-left: 40px; }
+ .article.depth-3 { border-left-color: #f0f; margin-left: 60px; }
+ .article.depth-4 { border-left-color: #ff0; margin-left: 80px; }
+ .article.depth-5 { border-left-color: #0ff; margin-left: 100px; }
+ .article.is-reply { font-size: 0.95em; }
+ .subject { color: #0af; font-weight: bold; margin-bottom: 4px; display: flex; align-items: center; gap: 8px; }
+ .subject a { color: #0af; text-decoration: none; }
+ .subject a:hover { color: #0f0; }
+ .thread-icon { font-size: 0.8em; opacity: 0.7; }
+ .meta { color: #666; font-size: 0.8em; display: flex; justify-content: space-between; align-items: center; }
+ a { color: #0af; text-decoration: none; }
+ a:hover { color: #0f0; }
+ .btn { display: inline-block; background: #004400; color: #0f0; padding: 8px 16px; border-radius: 4px; margin: 5px; }
+ .btn:hover { background: #006600; }
+ .btn-reply { background: #000044; border: 1px solid #0af; padding: 3px 8px; font-size: 0.75em; }
+ .error { background: #2a0a0a; border: 1px solid #a00; color: #f88; padding: 15px; border-radius: 8px; margin: 15px 0; }
+ .header { margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #333; }
+ .thread-info { color: #888; font-size: 0.85em; margin-bottom: 10px; }
</style>
</head>
<body>
<div class="container">
- <h1>โŒ Newsgroup Not Found</h1>
- <p><a href="/" class="back-link">โ† Back to Home</a> | <a href="/active" class="back-link">๐Ÿ“‹ All Groups</a></p>
-
- <div class="error">
- <h3>Group Not Available</h3>
- <p>The newsgroup <strong>` + groupName + `</strong> was not found in the cached group list.</p>
- <p>This could mean:</p>
- <ul>
- <li>The group name is misspelled</li>
- <li>The group doesn't exist on this server</li>
- <li>The active file cache needs to be refreshed</li>
- </ul>
- <p><a href="/active?refresh=1" style="color: #00aaff;">๐Ÿ”„ Refresh Group Cache</a></p>
+ <div class="header">
+ <h1>๐Ÿ“ฐ {{.GroupName}}</h1>
+ <p><a href="/">โ† Home</a> | <a href="/search">๐Ÿ” Search</a></p>
+ <a href="{{.M2UsenetURL}}?newsgroups={{.GroupName}}" target="_blank" class="btn">๐Ÿ“ New Post</a>
</div>
-
- <footer class="app-footer">
- <div class="footer-links">
- <a href="https://github.com/gabrix73/onion-newsreader.git" target="_blank" rel="noopener">๐Ÿ“ Source Code</a>
- <a href="monero:44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L" onclick="fallbackCopyXMR(event)">โšก Support Project</a>
- <a href="https://www.virebent.art" target="_blank" rel="noopener">๐ŸŒ virebent.art</a>
+
+ {{if .Error}}
+ <div class="error">โŒ {{.Error}}</div>
+ {{end}}
+
+ {{if .Articles}}
+ <h3>Threaded View ({{len .Articles}} articles)</h3>
+ <p class="thread-info">๐Ÿงต Posts are organized by thread. Replies are indented under their parent.</p>
+ {{range .Articles}}
+ <div class="article depth-{{.Depth}}{{if gt .Depth 0}} is-reply{{end}}">
+ <div class="subject">
+ {{if gt .Depth 0}}<span class="thread-icon">โ†ณ</span>{{end}}
+ <a href="/article/{{$.GroupName}}/{{.Number}}">{{.Subject}}</a>
</div>
- </footer>
+ <div class="meta">
+ <span>{{.From}} | #{{.Number}}</span>
+ <a href="{{$.M2UsenetURL}}?newsgroups={{$.GroupName}}&subject={{urlquery .ReplySubject}}&references={{urlquery .ReplyRef}}" target="_blank" class="btn btn-reply">๐Ÿ’ฌ Reply</a>
+ </div>
+ </div>
+ {{end}}
+ {{else if not .Error}}
+ <p>No articles found in this group.</p>
+ {{end}}
</div>
-
- <script>
- function fallbackCopyXMR(event) {
- const xmrAddress = "44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L";
-
- if (!navigator.userAgent.includes('Monero')) {
- event.preventDefault();
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(xmrAddress).then(() => {
- alert("โœ… XMR address copied to clipboard!");
- }).catch(() => {
- prompt("Copy XMR address:", xmrAddress);
- });
- } else {
- prompt("Copy XMR address:", xmrAddress);
- }
- }
- }
- </script>
</body>
</html>`
- w.Write([]byte(notFoundHTML))
- return
- }
-
- // Try to get articles for this group and build threads
- var threads []*Thread
- var articleCount, low, high int
- var connectionError string
-
- if err := s.client.ensureConnected(); err != nil {
- connectionError = fmt.Sprintf("Connection failed: %v", err)
- } else {
- var err error
- articleCount, low, high, err = s.client.SelectGroup(groupName)
- if err != nil {
- connectionError = fmt.Sprintf("Failed to select group: %v", err)
- } else {
- maxArticles := 200 // Increased for better threading
- articles, err := s.client.GetArticleOverview(low, high, maxArticles)
- if err != nil {
- connectionError = fmt.Sprintf("Failed to get articles: %v", err)
- } else {
- // Build threads from articles
- threads = buildThreads(articles)
- }
- }
- }
-
- groupHTML := `<!DOCTYPE html>
+
+const articleTemplate = `<!DOCTYPE html>
<html>
<head>
- <title>๐Ÿ“ฐ {{.GroupName}} - Onion Newsreader</title>
+ <title>๐Ÿ“„ Article {{.ArticleNum}} - {{.GroupName}}</title>
<meta charset="utf-8">
<style>
- body { font-family: monospace; background: #0a0a0a; color: #00ff00; padding: 20px; }
- .container { max-width: 1200px; margin: 0 auto; }
- .header { background: #1a1a1a; padding: 20px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #333; }
- .group-info { background: #1a1a1a; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #333; }
- .thread-list { background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333; }
- .thread-item {
- margin: 10px 0;
- padding: 15px;
- background: #0a0a0a;
- border-radius: 5px;
- border-left: 4px solid #333;
- }
- .thread-item:hover {
- background: #1a1a0a;
- border-left-color: #ffaa00;
- }
- .thread-subject {
- color: #00aaff;
- font-weight: bold;
- margin-bottom: 8px;
- }
- .thread-subject a {
- color: #00aaff;
- text-decoration: none;
- }
- .thread-subject a:hover {
- color: #00ff00;
- }
- .thread-meta {
- color: #888;
- font-size: 0.9em;
- margin-bottom: 10px;
- }
- .thread-replies {
- margin-left: 20px;
- border-left: 2px solid #333;
- padding-left: 15px;
- }
- .reply-item {
- margin: 5px 0;
- padding: 8px;
- background: #1a1a1a;
- border-radius: 3px;
- font-size: 0.9em;
- }
- .reply-subject {
- color: #00aaff;
- }
- .reply-subject a {
- color: #00aaff;
- text-decoration: none;
- }
- .reply-subject a:hover {
- color: #00ff00;
- }
- .reply-meta {
- color: #666;
- font-size: 0.8em;
- }
- .back-link { color: #00aaff; text-decoration: none; }
- .back-link:hover { color: #00ff00; }
- .error { background: #2a1a1a; border: 1px solid #aa0000; color: #aa0000; padding: 15px; border-radius: 8px; margin: 20px 0; }
- .warning { background: #2a1a00; border: 1px solid #ffaa00; color: #ffaa00; padding: 15px; border-radius: 8px; margin: 20px 0; }
- .info { background: #1a2a2a; border: 1px solid #0088aa; color: #0088aa; padding: 15px; border-radius: 8px; margin: 20px 0; }
- .refresh-btn { background: #333; color: #00ff00; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; margin: 10px 5px; }
- .refresh-btn:hover { background: #555; }
- .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 10px; }
- .stat-item { background: #0a0a0a; padding: 10px; border-radius: 4px; text-align: center; }
- .posting-section { background: #1a1a1a; padding: 20px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #333; text-align: center; }
- .post-button {
- display: inline-block;
- background: #2a4a2a;
- color: #00ff00;
- text-decoration: none;
- padding: 12px 24px;
- border-radius: 6px;
- font-weight: bold;
- margin: 10px 0;
- border: 2px solid #00aa00;
- transition: all 0.3s ease;
- }
- .post-button:hover {
- background: #3a6a3a;
- border-color: #00ff00;
- box-shadow: 0 0 10px rgba(0, 255, 0, 0.3);
- }
- .posting-note {
- font-size: 0.9em;
- color: #888;
- margin-top: 15px;
- text-align: left;
- background: #0a0a0a;
- padding: 10px;
- border-radius: 4px;
- border-left: 3px solid #ffaa00;
- }
- .thread-count {
- color: #ffaa00;
- font-weight: bold;
- }
- .app-footer {
- margin-top: 40px;
- padding: 25px 0;
- border-top: 2px solid #333;
- background: #1a1a1a;
- text-align: center;
- }
- .footer-links {
- display: flex;
- justify-content: center;
- gap: 25px;
- flex-wrap: wrap;
- }
- .footer-links a {
- color: #00aaff;
- text-decoration: none;
- font-weight: 500;
- padding: 8px 15px;
- border-radius: 20px;
- background: rgba(0, 170, 255, 0.1);
- transition: all 0.3s ease;
- border: 1px solid rgba(0, 170, 255, 0.2);
- }
- .footer-links a:hover {
- background: #00aaff;
- color: #0a0a0a;
- }
- .footer-links a[href^="monero:"] {
- background: rgba(255, 102, 0, 0.1);
- color: #ff6600;
- border-color: rgba(255, 102, 0, 0.2);
- }
- .footer-links a[href^="monero:"]:hover {
- background: #ff6600;
- color: #0a0a0a;
- }
- .footer-links a[href*="github"] {
- background: rgba(51, 51, 51, 0.1);
- color: #00ff00;
- border-color: rgba(51, 51, 51, 0.2);
- }
- .footer-links a[href*="github"]:hover {
- background: #00ff00;
- color: #0a0a0a;
- }
+ body { font-family: monospace; background: #0a0a0a; color: #0f0; padding: 20px; }
+ .container { max-width: 900px; margin: 0 auto; }
+ .header { margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #333; }
+ .headers-box { background: #1a1a1a; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #333; }
+ .header-line { margin: 5px 0; }
+ .header-name { color: #f80; }
+ .header-value { color: #0af; word-break: break-all; }
+ .body-box { background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333; }
+ .body-content { white-space: pre-wrap; word-wrap: break-word; line-height: 1.5; }
+ .quote { color: #888; }
+ a { color: #0af; text-decoration: none; }
+ a:hover { color: #0f0; }
+ .btn { display: inline-block; background: #004400; color: #0f0; padding: 8px 16px; border-radius: 4px; margin: 5px; }
+ .btn:hover { background: #006600; }
+ .btn-reply { background: #000044; border: 1px solid #0af; }
+ .error { background: #2a0a0a; border: 1px solid #a00; color: #f88; padding: 15px; border-radius: 8px; margin: 15px 0; }
</style>
</head>
<body>
<div class="container">
<div class="header">
- <h1>๐Ÿ“ฐ {{.GroupName}}</h1>
- <p><a href="/" class="back-link">โ† Back to Home</a> |
- <a href="/active" class="back-link">๐Ÿ“‹ All Groups</a> |
- <a href="/search?q={{.GroupPattern}}" class="back-link">๐Ÿ” Similar Groups</a></p>
- </div>
-
- <div class="group-info">
- <h3>๐Ÿ“Š Group Information</h3>
- <div class="stats">
- <div class="stat-item">
- <strong>๐Ÿ“ Cached Range:</strong><br>{{.CachedLow}} - {{.CachedHigh}}
- </div>
- <div class="stat-item">
- <strong>๐Ÿ“Š Cached Count:</strong><br>{{.CachedCount}}
- </div>
- {{if .LiveStats}}
- <div class="stat-item">
- <strong>๐Ÿ“ก Live Range:</strong><br>{{.LiveLow}} - {{.LiveHigh}}
- </div>
- <div class="stat-item">
- <strong>๐Ÿ”ข Live Count:</strong><br>{{.LiveCount}}
- </div>
- {{end}}
- <div class="stat-item">
- <strong>๐Ÿ“‹ Status:</strong><br>{{.Status}}
- </div>
- <div class="stat-item">
- <strong>๐Ÿงต Threads:</strong><br><span class="thread-count">{{.ThreadCount}}</span>
- </div>
- </div>
- </div>
-
- <div class="posting-section">
- <h3>โœ๏ธ Post to Newsgroup</h3>
- <p>Create a new post in <strong>{{.GroupName}}</strong></p>
- <a href="http://itcxzfm2h36hfj6j7qxksyfm4ipp3co4rkl62sgge7hp6u77lbretiyd.onion:8880/?newsgroups={{.EncodedGroupName}}" target="_blank" class="post-button">
- ๐Ÿ“ CREATE NEW POST
- </a>
- <p class="posting-note">
- <strong>โš ๏ธ Posting Requirements:</strong><br>
- โ€ข Use <strong>Tor Browser</strong> or configure SOCKS5 proxy (127.0.0.1:9050)<br>
- โ€ข Link opens in new tab to posting service<br>
- โ€ข Anonymous posting available on news.tcpreset.net
+ <h1>๐Ÿ“„ Article #{{.ArticleNum}}</h1>
+ <p>
+ <a href="/">โ† Home</a> |
+ <a href="/group/{{.GroupName}}">โ† {{.GroupName}}</a> |
+ <a href="/search">๐Ÿ” Search</a>
</p>
+ {{if .ReplyURL}}
+ <a href="{{.ReplyURL}}" target="_blank" class="btn btn-reply">๐Ÿ’ฌ Reply</a>
+ {{end}}
+ <a href="{{.M2UsenetURL}}?newsgroups={{.GroupName}}" target="_blank" class="btn">๐Ÿ“ New Post</a>
</div>
- {{if .ConnectionError}}
- <div class="error">
- <h3>โŒ Connection Error</h3>
- <p>{{.ConnectionError}}</p>
- <p>Showing cached information only. <a href="/health" style="color: #00aaff;">Check system health</a></p>
- </div>
- {{end}}
+ {{if .Error}}
+ <div class="error">โŒ {{.Error}}</div>
+ {{else}}
- {{if .Threads}}
- <div class="thread-list">
- <h3>๐Ÿงต Discussion Threads {{if .TruncatedView}}(showing recent {{.MaxArticles}} articles organized into threads){{end}}</h3>
-
- {{range .Threads}}
- <div class="thread-item">
- <div class="thread-subject">
- <a href="/article/{{$.GroupName}}/{{.Root.Article.Number}}">{{.Subject}}</a>
- </div>
- <div class="thread-meta">
- <strong>Started by:</strong> {{.Root.Article.From}} โ€ข
- <strong>Articles:</strong> {{.ArticleCount}} โ€ข
- <strong>Last activity:</strong> {{.LastDate.Format "Jan 2, 2006 15:04"}}
- </div>
-
- {{if .Root.Children}}
- <div class="thread-replies">
- {{range .Root.Children}}
- <div class="reply-item">
- <div class="reply-subject">
- <a href="/article/{{$.GroupName}}/{{.Article.Number}}">{{.Article.Subject}}</a>
- </div>
- <div class="reply-meta">
- By {{.Article.From}} โ€ข {{.Article.ParsedDate.Format "Jan 2, 15:04"}}
- </div>
-
- {{if .Children}}
- {{range .Children}}
- <div class="reply-item" style="margin-left: 15px; border-left: 1px solid #555; padding-left: 10px;">
- <div class="reply-subject">
- <a href="/article/{{$.GroupName}}/{{.Article.Number}}">{{.Article.Subject}}</a>
- </div>
- <div class="reply-meta">
- By {{.Article.From}} โ€ข {{.Article.ParsedDate.Format "Jan 2, 15:04"}}
- </div>
- </div>
- {{end}}
- {{end}}
- </div>
- {{end}}
- </div>
- {{end}}
+ <div class="headers-box">
+ <h3>Headers</h3>
+ {{if .Headers}}
+ {{range $key, $value := .Headers}}
+ <div class="header-line">
+ <span class="header-name">{{$key}}:</span>
+ <span class="header-value">{{$value}}</span>
</div>
{{end}}
-
- {{if .TruncatedView}}
- <div class="info">
- <p><strong>๐Ÿงต Threading Info:</strong> Articles are automatically organized into conversation threads using References headers and subject matching.</p>
- <p><strong>โ„น๏ธ Note:</strong> Only showing the most recent {{.MaxArticles}} articles to prevent slow loading.</p>
- <p>Use a dedicated newsreader client for full group browsing and threading.</p>
- </div>
{{end}}
</div>
- {{else if not .ConnectionError}}
- <div class="warning">
- <h3>โš ๏ธ No Articles Available</h3>
- <p>This group appears to be empty or articles couldn't be retrieved.</p>
- <p>This could be normal for:</p>
- <ul>
- <li>Newly created groups</li>
- <li>Low-traffic groups</li>
- <li>Groups with expired articles</li>
- </ul>
- <button onclick="window.location.reload()" class="refresh-btn">๐Ÿ”„ Refresh</button>
- </div>
- {{end}}
- <div class="info">
- <h3>๐Ÿ’ก Navigation Tips</h3>
- <ul>
- <li><strong>๐Ÿงต Threaded view:</strong> Articles are grouped by conversation thread</li>
- <li><strong>Click thread subjects</strong> to read the original post</li>
- <li><strong>Click reply subjects</strong> to read individual replies</li>
- <li><strong>Indentation shows</strong> the reply hierarchy</li>
- <li><strong>Recent threads first</strong> based on last activity</li>
- </ul>
+ <div class="body-box">
+ <h3>Message</h3>
+ <div class="body-content">{{.Body}}</div>
</div>
-
- <footer class="app-footer">
- <div class="footer-links">
- <a href="https://github.com/gabrix73/onion-newsreader.git" target="_blank" rel="noopener">๐Ÿ“ Source Code</a>
- <a href="monero:44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L" onclick="fallbackCopyXMR(event)">โšก Support Project</a>
- <a href="https://www.virebent.art" target="_blank" rel="noopener">๐ŸŒ virebent.art</a>
- </div>
- </footer>
+
+ {{end}}
</div>
-
- <script>
- function fallbackCopyXMR(event) {
- const xmrAddress = "44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L";
-
- if (!navigator.userAgent.includes('Monero')) {
- event.preventDefault();
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(xmrAddress).then(() => {
- alert("โœ… XMR address copied to clipboard!");
- }).catch(() => {
- prompt("Copy XMR address:", xmrAddress);
- });
- } else {
- prompt("Copy XMR address:", xmrAddress);
- }
- }
- }
- </script>
</body>
</html>`
- tmpl, _ := template.New("group").Parse(groupHTML)
-
- // Generate search pattern for similar groups
- groupPattern := ""
- if strings.Contains(groupName, ".") {
- parts := strings.Split(groupName, ".")
- if len(parts) > 1 {
- groupPattern = parts[0] + ".*"
- }
- }
-
- data := map[string]interface{}{
- "GroupName": groupName,
- "GroupPattern": groupPattern,
- "CachedLow": groupInfo.Low,
- "CachedHigh": groupInfo.High,
- "CachedCount": groupInfo.High - groupInfo.Low + 1,
- "Status": groupInfo.Status,
- "Threads": threads,
- "ThreadCount": len(threads),
- "ConnectionError": connectionError,
- "LiveStats": connectionError == "",
- "MaxArticles": 200,
- "TruncatedView": len(threads) > 0,
- "EncodedGroupName": url.QueryEscape(groupName),
- }
-
- if connectionError == "" {
- data["LiveLow"] = low
- data["LiveHigh"] = high
- data["LiveCount"] = articleCount
- }
-
- tmpl.Execute(w, data)
-}
-
-func (s *NewsServer) handleArticle(w http.ResponseWriter, r *http.Request) {
- w.Header().Set("Content-Type", "text/html; charset=utf-8")
-
- // Extract group name and article number from URL path
- path := strings.TrimPrefix(r.URL.Path, "/article/")
- parts := strings.Split(path, "/")
-
- if len(parts) != 2 {
- http.Redirect(w, r, "/", http.StatusSeeOther)
- return
- }
-
- groupName := parts[0]
- articleNumber, err := strconv.Atoi(parts[1])
- if err != nil {
- http.Redirect(w, r, "/group/"+groupName, http.StatusSeeOther)
- return
- }
-
- log.Printf("๐Ÿ“ฐ Reading article %d from group %s", articleNumber, groupName)
-
- // Ensure we're connected and select the group first
- var headers map[string]string
- var body []string
- var connectionError string
-
- if err := s.client.ensureConnected(); err != nil {
- connectionError = fmt.Sprintf("Connection failed: %v", err)
- } else {
- // Select group first
- _, _, _, err := s.client.SelectGroup(groupName)
- if err != nil {
- connectionError = fmt.Sprintf("Failed to select group: %v", err)
- } else {
- // Get the article
- headers, body, err = s.client.GetArticle(groupName, articleNumber)
- if err != nil {
- connectionError = fmt.Sprintf("Failed to get article: %v", err)
- }
- }
- }
-
- articleHTML := `<!DOCTYPE html>
+const healthTemplate = `<!DOCTYPE html>
<html>
<head>
- <title>๐Ÿ“ฐ Article {{.ArticleNumber}} - {{.GroupName}} - Onion Newsreader</title>
+ <title>๐Ÿ” Health Check</title>
<meta charset="utf-8">
<style>
- body { font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; background: #0a0a0a; color: #00ff00; padding: 20px; line-height: 1.4; }
- .container { max-width: 1000px; margin: 0 auto; }
- .header { background: #1a1a1a; padding: 20px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #333; }
- .article-headers { background: #1a1a1a; padding: 15px; border-radius: 8px; margin-bottom: 20px; border: 1px solid #333; }
- .article-body { background: #1a1a1a; padding: 20px; border-radius: 8px; border: 1px solid #333; }
- .header-item { margin: 5px 0; padding: 5px; background: #0a0a0a; border-radius: 3px; }
- .header-label { color: #ffaa00; font-weight: bold; }
- .header-value { color: #00aaff; word-break: break-word; }
- .body-content {
- color: #cccccc;
- white-space: pre-wrap;
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
- background: #0a0a0a;
- padding: 15px;
- border-radius: 5px;
- border: 1px solid #333;
- overflow-x: auto;
- line-height: 1.4;
- word-wrap: break-word;
- }
- .back-link { color: #00aaff; text-decoration: none; }
- .back-link:hover { color: #00ff00; }
- .error { background: #2a1a1a; border: 1px solid #aa0000; color: #aa0000; padding: 15px; border-radius: 8px; margin: 20px 0; }
- .warning { background: #2a1a00; border: 1px solid #ffaa00; color: #ffaa00; padding: 15px; border-radius: 8px; margin: 20px 0; }
- .info { background: #1a2a2a; border: 1px solid #0088aa; color: #0088aa; padding: 15px; border-radius: 8px; margin: 20px 0; }
- .article-meta { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 10px; }
- .subject-highlight { font-size: 1.1em; color: #00ff00; font-weight: bold; }
- .quoted-text {
- color: #888888;
- font-style: italic;
- border-left: 2px solid #666;
- padding-left: 8px;
- margin-left: 4px;
- display: block;
- }
- .signature {
- color: #666666;
- border-top: 1px solid #333;
- margin-top: 10px;
- padding-top: 10px;
- font-size: 0.9em;
- display: block;
- }
- .url-link {
- color: #00aaff;
- text-decoration: underline;
- }
- .reply-section {
- background: #1a1a1a;
- padding: 20px;
- border-radius: 8px;
- margin-bottom: 20px;
- border: 1px solid #333;
- text-align: center;
- }
- .reply-button {
- display: inline-block;
- background: #2a2a4a;
- color: #00aaff;
- text-decoration: none;
- padding: 12px 24px;
- border-radius: 6px;
- font-weight: bold;
- margin: 10px 0;
- border: 2px solid #0088aa;
- transition: all 0.3s ease;
- }
- .reply-button:hover {
- background: #3a3a6a;
- border-color: #00aaff;
- box-shadow: 0 0 10px rgba(0, 170, 255, 0.3);
- }
- .reply-note {
- font-size: 0.9em;
- color: #888;
- margin-top: 15px;
- text-align: left;
- background: #0a0a0a;
- padding: 10px;
- border-radius: 4px;
- border-left: 3px solid #00aaff;
- }
- .app-footer {
- margin-top: 40px;
- padding: 25px 0;
- border-top: 2px solid #333;
- background: #1a1a1a;
- text-align: center;
- }
- .footer-links {
- display: flex;
- justify-content: center;
- gap: 25px;
- flex-wrap: wrap;
- }
- .footer-links a {
- color: #00aaff;
- text-decoration: none;
- font-weight: 500;
- padding: 8px 15px;
- border-radius: 20px;
- background: rgba(0, 170, 255, 0.1);
- transition: all 0.3s ease;
- border: 1px solid rgba(0, 170, 255, 0.2);
- }
- .footer-links a:hover {
- background: #00aaff;
- color: #0a0a0a;
- }
- .footer-links a[href^="monero:"] {
- background: rgba(255, 102, 0, 0.1);
- color: #ff6600;
- border-color: rgba(255, 102, 0, 0.2);
- }
- .footer-links a[href^="monero:"]:hover {
- background: #ff6600;
- color: #0a0a0a;
- }
- .footer-links a[href*="github"] {
- background: rgba(51, 51, 51, 0.1);
- color: #00ff00;
- border-color: rgba(51, 51, 51, 0.2);
- }
- .footer-links a[href*="github"]:hover {
- background: #00ff00;
- color: #0a0a0a;
- }
+ body { font-family: monospace; background: #0a0a0a; color: #0f0; padding: 20px; }
+ .container { max-width: 600px; margin: 0 auto; }
+ .box { background: #1a1a1a; padding: 20px; border-radius: 8px; margin: 15px 0; border: 1px solid #333; }
+ .ok { color: #0f0; }
+ .err { color: #f80; }
+ a { color: #0af; }
+ code { background: #0a0a0a; padding: 2px 6px; border-radius: 2px; }
</style>
</head>
<body>
<div class="container">
- <div class="header">
- <h1>๐Ÿ“ฐ Article {{.ArticleNumber}}</h1>
- <p><a href="/" class="back-link">โ† Back to Home</a> |
- <a href="/group/{{.GroupName}}" class="back-link">โ† Back to {{.GroupName}}</a> |
- <a href="/active" class="back-link">๐Ÿ“‹ All Groups</a></p>
+ <h1>๐Ÿ” Health Check</h1>
+ <p><a href="/">โ† Home</a></p>
+
+ <div class="box">
+ <h3>Connectivity</h3>
+ <p>Tor Proxy (127.0.0.1:9050): {{if .TorOK}}<span class="ok">โœ… OK</span>{{else}}<span class="err">โŒ Failed</span>{{end}}</p>
+ <p>NNTP Status: <span class="{{if eq .NNTPStatus "connected"}}ok{{else}}err{{end}}">{{.NNTPStatus}}</span></p>
+ {{if .NNTPServer}}<p>Connected to: <code>{{.NNTPServer}}</code></p>{{end}}
</div>
- {{if not .ConnectionError}}
- <div class="reply-section">
- <h3>๐Ÿ’ฌ Reply to this Article</h3>
- <p>Reply to article by <strong>{{.From}}</strong> in <strong>{{.GroupName}}</strong></p>
- <a href="http://itcxzfm2h36hfj6j7qxksyfm4ipp3co4rkl62sgge7hp6u77lbretiyd.onion:8880/#send{{if .EncodedGroupName}}?newsgroups={{.EncodedGroupName}}{{end}}{{if .EncodedSubject}}&subject={{.EncodedSubject}}{{end}}{{if .EncodedMessageID}}&references={{.EncodedMessageID}}{{end}}{{if .EncodedQuotedContent}}&message={{.EncodedQuotedContent}}{{end}}" target="_blank" class="reply-button">
- ๐Ÿ’ฌ REPLY TO POST
- </a>
- <p class="reply-note">
- <strong>โ„น๏ธ Reply Pre-filled Data:</strong><br>
- {{if .Subject}}โ€ข <strong>Subject:</strong> Re: {{.Subject}}<br>{{end}}
- {{if .MessageID}}โ€ข <strong>References:</strong> {{.MessageID}}<br>{{end}}
- โ€ข <strong>Newsgroups:</strong> {{.GroupName}}<br>
- โ€ข <strong>Message:</strong> Quoted original content included<br>
- โ€ข <strong>Direct to:</strong> Send Message tab with pre-filled fields
- </p>
+ <div class="box">
+ <h3>Cache</h3>
+ <p>Groups cached: <strong>{{.GroupCount}}</strong></p>
+ <p>Session: <code>{{.Session}}</code></p>
+ <p>Timestamp: {{.Timestamp}}</p>
</div>
- {{end}}
- {{if .ConnectionError}}
- <div class="error">
- <h3>โŒ Connection Error</h3>
- <p>{{.ConnectionError}}</p>
- <p><a href="/health" style="color: #00aaff;">Check system health</a> |
- <a href="/group/{{.GroupName}}" style="color: #00aaff;">Back to group</a></p>
+ <div class="box">
+ <h3>Manual Tests</h3>
+ <p><code>curl --socks5 127.0.0.1:9050 http://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion</code></p>
</div>
- {{else}}
-
- <div class="article-headers">
- <h3>๐Ÿ“‹ Article Headers</h3>
- <div class="article-meta">
- {{if .Subject}}
- <div class="header-item">
- <span class="header-label">Subject:</span><br>
- <span class="subject-highlight">{{.Subject}}</span>
- </div>
- {{end}}
-
- {{if .From}}
- <div class="header-item">
- <span class="header-label">From:</span><br>
- <span class="header-value">{{.From}}</span>
- </div>
- {{end}}
-
- {{if .Date}}
- <div class="header-item">
- <span class="header-label">Date:</span><br>
- <span class="header-value">{{.Date}}</span>
- </div>
- {{end}}
-
- {{if .MessageID}}
- <div class="header-item">
- <span class="header-label">Message-ID:</span><br>
- <span class="header-value">{{.MessageID}}</span>
- </div>
- {{end}}
-
- {{if .Newsgroups}}
- <div class="header-item">
- <span class="header-label">Newsgroups:</span><br>
- <span class="header-value">{{.Newsgroups}}</span>
- </div>
- {{end}}
-
- {{if .References}}
- <div class="header-item">
- <span class="header-label">References:</span><br>
- <span class="header-value" style="font-size: 0.8em; word-break: break-all;">{{.References}}</span>
- </div>
- {{end}}
- </div>
-
- {{if .AllHeaders}}
- <details style="margin-top: 15px;">
- <summary style="color: #ffaa00; cursor: pointer;">๐Ÿ” Show All Headers</summary>
- <div style="margin-top: 10px; background: #0a0a0a; padding: 10px; border-radius: 5px;">
- {{range $key, $value := .AllHeaders}}
- <div style="margin: 2px 0; font-size: 0.9em;">
- <strong style="color: #ffaa00;">{{$key}}:</strong>
- <span style="color: #cccccc; word-break: break-word;">{{$value}}</span>
- </div>
- {{end}}
- </div>
- </details>
- {{end}}
- </div>
-
- <div class="article-body">
- <h3>๐Ÿ“„ Article Content</h3>
- {{if .Body}}
- <div class="body-content">{{.Body}}</div>
- {{else}}
- <div class="warning">
- <p>โš ๏ธ Article body is empty or could not be retrieved.</p>
- </div>
- {{end}}
- </div>
-
- <div class="info">
- <h3>๐Ÿ’ก Navigation Tips</h3>
- <ul>
- <li><strong>Use browser back button</strong> to return to threaded group view</li>
- <li><strong>References header</strong> shows message threading</li>
- <li><strong>Message-ID</strong> is unique identifier for this article</li>
- <li><strong>Quoted text</strong> appears dimmed and indented</li>
- <li><strong>Signatures</strong> appear separated and dimmed</li>
- <li><strong>Threading</strong> organizes related posts in conversation flow</li>
- </ul>
- </div>
-
- {{end}}
-
- <footer class="app-footer">
- <div class="footer-links">
- <a href="https://github.com/gabrix73/onion-newsreader.git" target="_blank" rel="noopener">๐Ÿ“ Source Code</a>
- <a href="monero:44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L" onclick="fallbackCopyXMR(event)">โšก Support Project</a>
- <a href="https://www.virebent.art" target="_blank" rel="noopener">๐ŸŒ virebent.art</a>
- </div>
- </footer>
</div>
-
- <script>
- function fallbackCopyXMR(event) {
- const xmrAddress = "44L7s3EK6WngbNEZUKBeHwWWTgafpYX98MDaz2LSxkTHWtqPcnhiqzgXC9rjb45VHbWeesdgp2tcR9y5ApegoszyNMemz4L";
-
- if (!navigator.userAgent.includes('Monero')) {
- event.preventDefault();
- if (navigator.clipboard && navigator.clipboard.writeText) {
- navigator.clipboard.writeText(xmrAddress).then(() => {
- alert("โœ… XMR address copied to clipboard!");
- }).catch(() => {
- prompt("Copy XMR address:", xmrAddress);
- });
- } else {
- prompt("Copy XMR address:", xmrAddress);
- }
- }
- }
- </script>
</body>
</html>`
- tmpl, _ := template.New("article").Parse(articleHTML)
-
- // Process body for better display with proper HTML escaping
- var bodyHTML template.HTML
- var quotedBody string // For reply functionality
- if len(body) > 0 {
- bodyText := strings.Join(body, "\n")
-
- // Create quoted version for replies (add > to each line)
- quotedLines := strings.Split(bodyText, "\n")
- for i, line := range quotedLines {
- quotedLines[i] = "> " + line
- }
- quotedBody = strings.Join(quotedLines, "\n")
-
- // Prima escape tutto l'HTML per sicurezza
- bodyText = html.EscapeString(bodyText)
-
- // Poi aggiungi styling per quotes e signature
- lines := strings.Split(bodyText, "\n")
- inSignature := false
-
- for i, line := range lines {
- trimmedLine := strings.TrimSpace(line)
-
- // Detect signature start (-- line)
- if trimmedLine == "--" && !inSignature {
- inSignature = true
- lines[i] = `<div class="signature">` + line
- continue
- }
-
- // Se siamo in signature, continua lo styling
- if inSignature {
- if i == len(lines)-1 {
- lines[i] = line + `</div>` // Chiudi signature alla fine
- }
- continue
- }
-
- // Quote detection (linee che iniziano con >)
- if strings.HasPrefix(trimmedLine, "&gt;") {
- lines[i] = `<div class="quoted-text">` + line + `</div>`
- }
-
- // URL detection semplice
- if strings.Contains(line, "http://") || strings.Contains(line, "https://") || strings.Contains(line, "ftp://") {
- // Non implementiamo link cliccabili per sicurezza, ma evidenziamo
- lines[i] = strings.ReplaceAll(line, "http://", `<span class="url-link">http://`)
- lines[i] = strings.ReplaceAll(lines[i], "https://", `<span class="url-link">https://`)
- lines[i] = strings.ReplaceAll(lines[i], "ftp://", `<span class="url-link">ftp://`)
- // Chiudi span alla fine della linea se contiene URL
- if strings.Contains(lines[i], `<span class="url-link">`) {
- lines[i] += `</span>`
- }
- }
- }
-
- // Se signature non รจ stata chiusa, chiudila
- if inSignature && !strings.Contains(lines[len(lines)-1], "</div>") {
- lines[len(lines)-1] += `</div>`
- }
-
- processedBody := strings.Join(lines, "\n")
-
- // Marca come HTML sicuro
- bodyHTML = template.HTML(processedBody)
- }
-
- data := map[string]interface{}{
- "GroupName": groupName,
- "ArticleNumber": articleNumber,
- "ConnectionError": connectionError,
- "Body": bodyHTML, // Usa bodyHTML invece di bodyText
- "AllHeaders": headers,
- "EncodedGroupName": url.QueryEscape(groupName),
- "QuotedBody": quotedBody,
- }
-
- // Extract common headers for easy access and URL encode them
- if headers != nil {
- if subject, ok := headers["Subject"]; ok {
- data["Subject"] = subject
- // Add "Re: " if not already present for reply functionality
- replySubject := subject
- if !strings.HasPrefix(strings.ToLower(subject), "re:") {
- replySubject = "Re: " + subject
- }
- data["EncodedSubject"] = url.QueryEscape(replySubject)
- }
- if from, ok := headers["From"]; ok {
- data["From"] = from
- }
- if date, ok := headers["Date"]; ok {
- data["Date"] = date
- }
- if msgID, ok := headers["Message-ID"]; ok {
- data["MessageID"] = msgID
- // Ensure Message-ID has proper < > format for References
- properMsgID := msgID
- if !strings.HasPrefix(msgID, "<") {
- properMsgID = "<" + msgID
- }
- if !strings.HasSuffix(properMsgID, ">") {
- properMsgID = properMsgID + ">"
- }
- data["EncodedMessageID"] = url.QueryEscape(properMsgID)
- }
- if newsgroups, ok := headers["Newsgroups"]; ok {
- data["Newsgroups"] = newsgroups
- }
- if references, ok := headers["References"]; ok {
- data["References"] = references
- }
- }
-
- // Prepare quoted content for reply
- if quotedBody != "" && headers != nil {
- if from, ok := headers["From"]; ok {
- if date, ok := headers["Date"]; ok {
- quotedContent := fmt.Sprintf("On %s, %s wrote:\n\n%s", date, from, quotedBody)
- data["EncodedQuotedContent"] = url.QueryEscape(quotedContent)
- }
- }
- }
-
- tmpl.Execute(w, data)
-}
+// ============= Main =============
func main() {
- log.Printf("๐Ÿ“ Logging initialized - writing to stdout and %s", "/var/log/onion-newsreader/onion-newsreader.log")
- log.Printf("๐Ÿง… Onion Newsreader starting with threading support...")
+ log.Println("๐Ÿง… Onion Newsreader starting...")
server := NewNewsServer()
defer server.client.Close()
- log.Printf("๐Ÿš€ Starting Onion Newsreader HTTP server...")
-
http.HandleFunc("/", server.handleHome)
- http.HandleFunc("/health", server.handleHealth)
- http.HandleFunc("/test-onion", server.handleTestOnion)
- http.HandleFunc("/search", server.handleSearch)
- http.HandleFunc("/active", server.handleActive)
+ http.HandleFunc("/download", server.handleDownload)
http.HandleFunc("/download-status", server.handleDownloadStatus)
+ http.HandleFunc("/search", server.handleSearch)
http.HandleFunc("/group/", server.handleGroup)
http.HandleFunc("/article/", server.handleArticle)
+ http.HandleFunc("/health", server.handleHealth)
- log.Printf("โœ… HTTP server initialization complete")
- log.Printf("๐Ÿ”„ Starting background NNTP connection routine...")
-
- go func() {
- for {
- time.Sleep(10 * time.Second)
- log.Printf("๐Ÿ”„ Attempting NNTP connection...")
- if err := server.client.ensureConnected(); err != nil {
- log.Printf("โš ๏ธ NNTP connection failed: %v", err)
- log.Printf("โฐ Retrying NNTP connection in 120 seconds...")
- time.Sleep(2 * time.Minute)
- } else {
- log.Printf("โœ… NNTP connection established successfully")
- break
- }
- }
- }()
-
- log.Printf("๐Ÿš€ Starting HTTP server on 127.0.0.1:8080")
- log.Printf("๐Ÿ“ก Target NNTP: peannyjkqwqfynd24p6dszvtchkq7hfkwymi5by5y332wmosy5dwfaqd.onion:119")
- log.Printf("๐Ÿ”„ Fallback NNTP: news.tcpreset.net:119 (anonymous access)")
- log.Printf("โœ๏ธ Posting URL: http://itcxzfm2h36hfj6j7qxksyfm4ipp3co4rkl62sgge7hp6u77lbretiyd.onion:8880/")
- log.Printf("๐Ÿงต Threading: Enabled (References + Subject-based)")
- log.Printf("๐Ÿ”’ Stealth mode: true")
- log.Printf("๐Ÿ’ฌ Support contact: info@tcpreset.net")
- log.Printf("๐ŸŒ Web interface ready - NNTP connection will be established in background")
+ log.Println("๐Ÿš€ HTTP server on 127.0.0.1:8080")
+ log.Printf("๐Ÿ“ก Primary: %s", PrimaryNNTP)
+ log.Printf("๐Ÿ”„ Fallback: %s", FallbackNNTP)
+ log.Printf("โœ๏ธ Posting: %s", M2UsenetURL)
if err := http.ListenAndServe("127.0.0.1:8080", nil); err != nil {
- log.Fatalf("โŒ HTTP server failed: %v", err)
+ log.Fatal(err)
}
}