diff options
Diffstat (limited to 'internal/thread/thread.go')
| -rw-r--r-- | internal/thread/thread.go | 76 |
1 files changed, 76 insertions, 0 deletions
diff --git a/internal/thread/thread.go b/internal/thread/thread.go new file mode 100644 index 0000000..ed9a47b --- /dev/null +++ b/internal/thread/thread.go @@ -0,0 +1,76 @@ +// Package thread builds local Usenet conversation trees from article headers. +package thread + +import ( + "sort" + "strings" + + "aegis/internal/nntp" +) + +type Node struct { + Header nntp.ArticleHeader + Parent *Node + Children []*Node +} + +func Build(headers []nntp.ArticleHeader) []*Node { + ordered := append([]nntp.ArticleHeader(nil), headers...) + sort.SliceStable(ordered, func(i, j int) bool { + if ordered[i].Number == ordered[j].Number { + return ordered[i].MessageID < ordered[j].MessageID + } + return ordered[i].Number < ordered[j].Number + }) + nodes := make(map[string]*Node, len(ordered)) + for _, header := range ordered { + node := &Node{Header: header} + if key := canonicalID(header.MessageID); key != "" { + nodes[key] = node + } + } + var roots []*Node + for _, node := range nodesInOrder(ordered, nodes) { + parentID := lastReference(node.Header.References) + parent := nodes[canonicalID(parentID)] + if parent == nil || parent == node { + roots = append(roots, node) + continue + } + node.Parent = parent + parent.Children = append(parent.Children, node) + } + for _, node := range nodesInOrder(ordered, nodes) { + sort.SliceStable(node.Children, func(i, j int) bool { return node.Children[i].Header.Number < node.Children[j].Header.Number }) + } + return roots +} + +func nodesInOrder(headers []nntp.ArticleHeader, nodes map[string]*Node) []*Node { + result := make([]*Node, 0, len(headers)) + seen := make(map[*Node]struct{}, len(headers)) + for _, header := range headers { + node := nodes[canonicalID(header.MessageID)] + if node == nil { + continue + } + if _, ok := seen[node]; ok { + continue + } + seen[node] = struct{}{} + result = append(result, node) + } + return result +} + +func lastReference(value string) string { + parts := strings.Fields(value) + if len(parts) == 0 { + return "" + } + return parts[len(parts)-1] +} + +func canonicalID(value string) string { + return strings.ToLower(strings.TrimSpace(value)) +} |
