1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
|
diff --git a/README.md b/README.md
index e0a6f58d05ad45bf73e94816d588defe449e3a93..a06939182cbbf6b0d32724aef3336b6c77c1a3e3 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,7 @@ | Command | Capability | Implemented | Standard |
|-------------------|---------------|-------------|----------|
| CAPABILITIES | mandatory | 🗸 | RFC3977 |
| HEAD | mandatory | 🗸 | RFC3977 |
-| HELP | mandatory | ☓ | RFC3977 |
+| HELP | mandatory | 🗸 | RFC3977 |
| QUIT | mandatory | ☓ | RFC3977 |
| STAT | mandatory | ☓ | RFC3977 |
| AUTHINFO USER | AUTHINFO USER | 🗸 | RFC4643 |
diff --git a/mandatory.go b/mandatory.go
index 399b18a24b8100693c7c37913a8f7aacc3fd7abd..b0c685f17263675b899093f4f3e3b5c488438e9c 100644
--- a/mandatory.go
+++ b/mandatory.go
@@ -39,3 +39,17 @@ default:
return nil, ErrUnexpectedResponse
}
}
+
+func (c *Conn) Help(ctx context.Context) (string, error) {
+ resp, err := c.Cmd(ctx, "HELP")
+ if err != nil {
+ return "", fmt.Errorf("failed to get helptext: %w", err)
+ }
+
+ switch resp.Status.Code {
+ case StatusHelpTextFollows:
+ return fmt.Sprintf("%s", resp.Body), nil
+ default:
+ return "", ErrUnexpectedResponse
+ }
+}
diff --git a/mandatory_test.go b/mandatory_test.go
index 323f73ebb377a264b383680b87470432e2199acd..d97fe725e12639a4d88354443eca4e89e6fac8b8 100644
--- a/mandatory_test.go
+++ b/mandatory_test.go
@@ -3,6 +3,7 @@
import (
"context"
"errors"
+ "strings"
"testing"
"time"
@@ -113,3 +114,41 @@ }
})
}
}
+
+func TestHelp(t *testing.T) {
+ c, err := nntp.Dial(NewsServerPlain, nntp.OptionUnencrypted)
+ if err != nil {
+ t.Skipf("connection to '%s' failed: %v", NewsServerSecure, err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ err = c.LoginUserPass(ctx, NewsServerUser, NewsServerPassword)
+ if err != nil {
+ t.Skipf("failed to authenticate: %v", err)
+ }
+
+ ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+ help, err := c.Help(ctx)
+ if err != nil {
+ t.Errorf("requesting help failed: %v", err)
+ }
+
+ contained := make(map[string]bool)
+ for _, v := range strings.Split(help, "\n") {
+ v = strings.TrimLeft(v, " ")
+ contained[strings.Split(v, " ")[0]] = true
+ }
+
+ mandatory := []string{
+ "quit",
+ "head",
+ "stat",
+ }
+ for _, key := range mandatory {
+ if !contained[key] {
+ t.Errorf("mandatory command '%s' is missing", key)
+ }
+ }
+}
diff --git a/status.go b/status.go
index 5d6880a5fb8eefa37e51b6cc2ad26d3917807691..b62a344a58fac463e2109568f4b5530792dba5f3 100644
--- a/status.go
+++ b/status.go
@@ -3,6 +3,7 @@
import "errors"
const (
+ StatusHelpTextFollows = 100
StatusServiceAvailable = 200
StatusServiceNoPosting = 201
StatusGroupSelected = 211
|