Commit ed92ed6c8a628c10eb19a15d5bf4a6de19309b46

Parents: 41d2c58ed262e4a2b2d10dbc2397b603680e09a8

From: Moritz Poldrack <git@moritz.sh>
Date: Sat Jun 11 01:40:11 2022 +0700

added ARTICLE command

		

Stats

README.md +1/-1
reader.go +26/-0
reader_test.go +77/-0
status.go +1/-0

Changeset

  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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
diff --git a/README.md b/README.md
index a03ee3a88a6420aea2d1a219386ceb1630d32f5f..4751ddd2bdbb922c253ac5fc0f52ced1ae62c57f 100644
--- a/README.md
+++ b/README.md
@@ -29,7 +29,7 @@ | NEWNEWS           | NEWNEWS       | ☓           | RFC3977  |
 | OVER              | OVER          | ☓           | RFC3977  |
 | LIST OVERVIEW.FMT | OVER          | ☓           | RFC3977  |
 | POST              | POST          | ☓           | RFC3977  |
-| ARTICLE           | READER        | ☓           | RFC3977  |
+| ARTICLE           | READER        | 🗸           | RFC3977  |
 | BODY              | READER        | ☓           | RFC3977  |
 | DATE              | READER        | ☓           | RFC3977  |
 | GROUP             | READER        | 🗸           | RFC3977  |
diff --git a/reader.go b/reader.go
index 483c749c0da2e0042fa0c8e0aad6821b70c6a8b1..d68f64711105cbb07344e97ca03590b73389b7ca 100644
--- a/reader.go
+++ b/reader.go
@@ -5,6 +5,32 @@ 	"context"
 	"fmt"
 )
 
+func (c *Conn) Article(ctx context.Context, msg MessageIdentifier) (*Response, error) {
+	if c.caps&CapReader == 0 {
+		return nil, ErrCapabilityNotSupported
+	}
+
+	resp, err := c.CmdFull(ctx, "ARTICLE %s", msg.msgID())
+	if err != nil {
+		return nil, fmt.Errorf("failed to retrieve article '%s': %w", msg.msgID(), err)
+	}
+
+	switch resp.Status.Code {
+	case StatusArticleFollows:
+		return resp, nil
+	case StatusNoSuchNewsgroup:
+		return resp, ErrNoNewsgroupSelected
+	case StatusCurrentArticleNumberInvalid:
+		return resp, ErrCurrentArticleNumberInvalid
+	case StatusNoArticleWithGivenMessageID:
+		return resp, ErrNoArticleWithGivenMessageID
+	case StatusNoArticleWithGivenNumber:
+		return resp, ErrNoArticleWithGivenNumber
+	default:
+		return resp, ErrUnexpectedResponse
+	}
+}
+
 func (c *Conn) Group(ctx context.Context, group string) error {
 	if c.caps&CapReader == 0 {
 		return ErrCapabilityNotSupported
diff --git a/reader_test.go b/reader_test.go
index ad92bfb64fb5d9e5e8593b7957cac89609e9fe0e..8394e287e0add6f8318f272aa054e67df0465479 100644
--- a/reader_test.go
+++ b/reader_test.go
@@ -9,6 +9,83 @@
 	"mpldr.codes/usenet/nntp"
 )
 
+func TestArticle(t *testing.T) {
+	if NewsServerSecure == "" || NewsServerUser == "" || NewsServerPassword == "" {
+		t.Log("secure server address, username, and password required in variables_test.go")
+		t.SkipNow()
+	}
+	tests := []struct {
+		name          string
+		message       nntp.MessageIdentifier
+		expectedError error
+		validate      func(*testing.T, *nntp.Response)
+	}{
+		/*{ // Eweka sends 411 instead of 412
+			name:          "no-selected-group",
+			message:       nntp.ArticleNumber(54160654),
+			expectedError: nntp.ErrNoSuchNewsgroup,
+			validate:      func(t *testing.T, resp *nntp.Response) { fmt.Println(resp.Status.String()); t.Fail() },
+		},*/
+		{
+			name:          "non-existant-article",
+			message:       nntp.MessageID("probably-not-valid-@some-client.lol"),
+			expectedError: nntp.ErrNoArticleWithGivenMessageID,
+			validate:      func(t *testing.T, r *nntp.Response) {},
+		},
+		{
+			name:          "existing-article",
+			message:       nntp.MessageID("HtIsMuKsOrQwViIoPiBzAsZy-1654899257340@nyuu"),
+			expectedError: nil,
+			validate: func(t *testing.T, r *nntp.Response) {
+				if r.Status.Code != nntp.StatusArticleFollows {
+					t.Errorf("wrong status: %d", r.Status.Code)
+				}
+				if r.Header.Get("Organization") != "Eweka Internet Services" {
+					t.Errorf("wrong organization '%s' in header", r.Header.Get("Organization"))
+				}
+				if r.Header.Get("Lines") != "4" {
+					t.Errorf("wrong linecount '%s' in header", r.Header.Get("Lines"))
+				}
+				if r.Header.Get("User-Agent") != "Nyuu/0.4.1" {
+					t.Errorf("wrong user agent '%s' in header", r.Header.Get("User-Agent"))
+				}
+				if r.Header.Get("X-Received-Bytes") != "562" {
+					t.Errorf("wrong received byte count '%s' in header", r.Header.Get("X-Received-Bytes"))
+				}
+				if len(r.Body) != 98 {
+					t.Errorf("length of body '%d' does not match expectation 562", len(r.Body))
+				}
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			c, err := nntp.Dial(NewsServerSecure)
+			if err != nil {
+				t.Skipf("connection to '%s' failed: %v", NewsServerSecure, err)
+			}
+			defer c.Close(context.Background())
+
+			ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+			defer cancel()
+			err = c.LoginUserPass(ctx, NewsServerUser, NewsServerPassword)
+			if err != nil {
+				t.Skipf("login failed: %v", err)
+			}
+
+			ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
+			defer cancel()
+			resp, err := c.Article(ctx, test.message)
+			if !errors.Is(err, test.expectedError) {
+				t.Errorf("unexpected result:\n\tgot:      %v\n\texpected: %v\n", err, test.expectedError)
+			}
+
+			test.validate(t, resp)
+		})
+	}
+}
+
 func TestGroup(t *testing.T) {
 	if NewsServerSecure == "" || NewsServerUser == "" || NewsServerPassword == "" {
 		t.Log("secure server address, username, and password required in variables_test.go")
diff --git a/status.go b/status.go
index 2f76695780b5e08f9b0ba874e5abdbed9bb6d0e5..d604742694b2e22e17368fb686b1bf5ba32b3420 100644
--- a/status.go
+++ b/status.go
@@ -8,6 +8,7 @@ 	StatusServiceAvailable            = 200
 	StatusServiceNoPosting            = 201
 	StatusConnectionClosing           = 205
 	StatusGroupSelected               = 211
+	StatusArticleFollows              = 220
 	StatusHeadersFollow               = 221
 	StatusArticleExists               = 223
 	StatusAuthAccepted                = 281