Commit c351fa0b59f435e50c75ad19a2ab757054e1ac28

Parents: eaf21885b8b931e81efbde06220cbbf56126d149

From: Moritz Poldrack <git@moritz.sh>
Date: Sat Jun 11 17:37:48 2022 +0700

added NEXT and LAST commands

		

Stats

README.md +2/-2
reader.go +48/-0
reader_test.go +104/-0
status.go +5/-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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
diff --git a/README.md b/README.md
index d5d1516f2711580c75195fa6b0781de13e87d4b0..bdb92fb7575221c2d58bece76bb928bfb3be0742 100644
--- a/README.md
+++ b/README.md
@@ -33,10 +33,10 @@ | ARTICLE           | READER        | 🗸           | RFC3977  |
 | BODY              | READER        | 🗸           | RFC3977  |
 | DATE              | READER        | 🗸           | RFC3977  |
 | GROUP             | READER        | 🗸           | RFC3977  |
-| LAST              | READER        | ☓           | RFC3977  |
+| LAST              | READER        | 🗸           | RFC3977  |
 | LISTGROUP         | READER        | 🗸           | RFC3977  |
 | NEWGROUPS²        | READER        | ?           | RFC3977  |
-| NEXT              | READER        | ☓           | RFC3977  |
+| NEXT              | READER        | 🗸           | RFC3977  |
 
 ¹) if you happen to know a provider using SASL, please let me know
 ²) untested, because my provider does not follow standards
diff --git a/reader.go b/reader.go
index f9b3f6ebf0c0385638623297d367aeb9c9b2d1ef..b2d488dcbc4f1340766abd18f30d1887cf72afd9 100644
--- a/reader.go
+++ b/reader.go
@@ -177,3 +177,51 @@ 	default:
 		return nil, ErrUnexpectedResponse
 	}
 }
+
+func (c *Conn) Next(ctx context.Context) error {
+	if c.caps&CapReader == 0 {
+		return ErrCapabilityNotSupported
+	}
+
+	resp, err := c.CmdNoBody(ctx, "NEXT")
+	if err != nil {
+		return fmt.Errorf("failed to select next article: %w", err)
+	}
+
+	switch resp.Status.Code {
+	case StatusArticleFound:
+		return nil
+	case StatusNoSuchNewsgroup:
+		return ErrNoSuchNewsgroup
+	case StatusCurrentArticleNumberInvalid:
+		return ErrCurrentArticleNumberInvalid
+	case StatusNoNextArticleInGroup:
+		return ErrNoNextArticleInGroup
+	default:
+		return ErrUnexpectedResponse
+	}
+}
+
+func (c *Conn) Last(ctx context.Context) error {
+	if c.caps&CapReader == 0 {
+		return ErrCapabilityNotSupported
+	}
+
+	resp, err := c.CmdNoBody(ctx, "LAST")
+	if err != nil {
+		return fmt.Errorf("failed to select previous article: %w", err)
+	}
+
+	switch resp.Status.Code {
+	case StatusArticleFound:
+		return nil
+	case StatusNoSuchNewsgroup:
+		return ErrNoSuchNewsgroup
+	case StatusCurrentArticleNumberInvalid:
+		return ErrCurrentArticleNumberInvalid
+	case StatusNoPreviousArticleInGroup:
+		return ErrNoPreviousArticleInGroup
+	default:
+		return ErrUnexpectedResponse
+	}
+}
diff --git a/reader_test.go b/reader_test.go
index 468f84520a2bc94279cf1898f85a90201fd3a680..c9f1ba1d7bf08f5ccf17a6d719d21aafed6fbecd 100644
--- a/reader_test.go
+++ b/reader_test.go
@@ -397,3 +397,107 @@ 	}
 
 	t.Skip("¯\\_(ツ)_/¯ my provider doesn't support it… Sorry.")
 }
+
+func TestNext(t *testing.T) {
+	if NewsServerSecure == "" || NewsServerUser == "" || NewsServerPassword == "" {
+		t.Log("secure server address, username, and password required in variables_test.go")
+		t.SkipNow()
+	}
+
+	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()
+	err = c.Group(ctx, "alt.binaries.dvdr.german")
+	if err != nil {
+		t.Skipf("joining group failed: %v", err)
+	}
+
+	ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	err = c.Stat(ctx, nntp.ArticleNumber(11835721))
+	if err != nil {
+		t.Skipf("selecting post failed: %v", err)
+	}
+
+	ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	err = c.Next(ctx)
+	if err != nil {
+		t.Errorf("selecting post failed: %v", err)
+	}
+
+	ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	headers, err := c.Head(ctx, nntp.CurrentMessage)
+	if err != nil {
+		t.Skipf("failed to stat current message: %v", err)
+	}
+
+	if len(headers) == 0 || headers["Message-Id"][0] != "<fLBjgfRK48LZdJO0oOB9_5o28@JBinUp.local>" {
+		t.Errorf("wrong or no messageID '%s'", headers["Message-Id"][0])
+	}
+}
+
+func TestLast(t *testing.T) {
+	if NewsServerSecure == "" || NewsServerUser == "" || NewsServerPassword == "" {
+		t.Log("secure server address, username, and password required in variables_test.go")
+		t.SkipNow()
+	}
+
+	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()
+	err = c.Group(ctx, "alt.binaries.dvdr.german")
+	if err != nil {
+		t.Skipf("joining group failed: %v", err)
+	}
+
+	ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	err = c.Stat(ctx, nntp.ArticleNumber(11835721))
+	if err != nil {
+		t.Skipf("selecting post failed: %v", err)
+	}
+
+	ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	err = c.Last(ctx)
+	if err != nil {
+		t.Errorf("selecting post failed: %v", err)
+	}
+
+	ctx, cancel = context.WithTimeout(context.Background(), 5*time.Second)
+	defer cancel()
+	headers, err := c.Head(ctx, nntp.CurrentMessage)
+	if err != nil {
+		t.Skipf("failed to stat current message: %v", err)
+	}
+
+	if len(headers) == 0 || headers["Message-Id"][0] != "<nheCBcocIzUnGdAQV6X5_5o28@JBinUp.local>" {
+		t.Errorf("wrong or no messageID '%s'", headers["Message-Id"][0])
+	}
+}
diff --git a/status.go b/status.go
index 2a9df0409a6171d16bb0452bbcd4d35803226ef6..3606ae5eb7c000868359cc34dbd2a263e0f96628 100644
--- a/status.go
+++ b/status.go
@@ -12,6 +12,7 @@ 	StatusGroupSelected               = 211
 	StatusArticleNumbersFollow        = 211
 	StatusArticleFollows              = 220
 	StatusBodyFollows                 = 222
+	StatusArticleFound                = 223
 	StatusHeadersFollow               = 221
 	StatusArticleExists               = 223
 	StatusAuthAccepted                = 281
@@ -22,6 +23,8 @@ 	StatusTemporarilyUnavailable      = 400
 	StatusNoSuchNewsgroup             = 411
 	StatusNoNewsgroupSelected         = 412
 	StatusCurrentArticleNumberInvalid = 420
+	StatusNoNextArticleInGroup        = 421
+	StatusNoPreviousArticleInGroup    = 422
 	StatusNoArticleWithGivenNumber    = 423
 	StatusNoArticleWithGivenMessageID = 430
 	StatusAuthFailed                  = 481
@@ -31,6 +34,8 @@ )
 
 var (
 	ErrCurrentArticleNumberInvalid = errors.New("current article number invalid")
+	ErrNoNextArticleInGroup        = errors.New("no next article in group")
+	ErrNoPreviousArticleInGroup    = errors.New("no previous article in group")
 	ErrNoArticleWithGivenNumber    = errors.New("no article with that number")
 	ErrNoArticleWithGivenMessageID = errors.New("no article with that message-ID")
 	ErrNoSuchNewsgroup             = errors.New("no such newsgroup")