9
9
"net/http"
10
10
"os"
11
11
"path/filepath"
12
+ "strings"
12
13
"time"
13
14
14
15
"github.com/didip/tollbooth/v7"
@@ -33,6 +34,7 @@ type Server struct {
33
34
Credentials map [string ]string
34
35
35
36
indexPage * template.Template
37
+ rulePage * template.Template
36
38
}
37
39
38
40
// JSON is a map alias, just for convenience
@@ -44,6 +46,7 @@ func (s *Server) Run(ctx context.Context, address string, port int, frontendDir
44
46
45
47
_ = os .Mkdir (filepath .Join (frontendDir , "components" ), 0o700 )
46
48
t := template .Must (template .ParseGlob (filepath .Join (frontendDir , "components" , "*.gohtml" )))
49
+ s .rulePage = template .Must (template .Must (t .Clone ()).ParseFiles (filepath .Join (frontendDir , "rule.gohtml" )))
47
50
s .indexPage = template .Must (template .Must (t .Clone ()).ParseFiles (filepath .Join (frontendDir , "index.gohtml" )))
48
51
httpServer := & http.Server {
49
52
Addr : fmt .Sprintf ("%s:%d" , address , port ),
@@ -77,20 +80,19 @@ func (s *Server) routes(frontendDir string) chi.Router {
77
80
router .Route ("/api" , func (r chi.Router ) {
78
81
r .Get ("/content/v1/parser" , s .extractArticleEmulateReadability )
79
82
r .Post ("/extract" , s .extractArticle )
80
-
81
- r .Get ("/rule" , s .getRule )
82
- r .Get ("/rule/{id}" , s .getRuleByID )
83
- r .Get ("/rules" , s .getAllRules )
84
83
r .Post ("/auth" , s .authFake )
85
84
86
85
r .Group (func (protected chi.Router ) {
87
86
protected .Use (basicAuth ("ureadability" , s .Credentials ))
88
87
protected .Post ("/rule" , s .saveRule )
89
88
protected .Post ("/toggle-rule/{id}" , s .toggleRule )
89
+ protected .Post ("/preview" , s .handlePreview )
90
90
})
91
91
})
92
92
93
93
router .Get ("/" , s .handleIndex )
94
+ router .Get ("/add/" , s .handleAdd )
95
+ router .Get ("/edit/{id}" , s .handleEdit )
94
96
95
97
_ = os .Mkdir (filepath .Join (frontendDir , "static" ), 0o700 )
96
98
fs , err := UM .NewFileServer ("/" , filepath .Join (frontendDir , "static" ), UM .FsOptSPA )
@@ -119,6 +121,42 @@ func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
119
121
}
120
122
}
121
123
124
+ func (s * Server ) handleAdd (w http.ResponseWriter , _ * http.Request ) {
125
+ data := struct {
126
+ Title string
127
+ Rule datastore.Rule
128
+ }{
129
+ Title : "Добавление правила" ,
130
+ Rule : datastore.Rule {}, // Empty rule for the form
131
+ }
132
+ err := s .rulePage .ExecuteTemplate (w , "base.gohtml" , data )
133
+ if err != nil {
134
+ log .Printf ("[WARN] failed to render add template, %v" , err )
135
+ http .Error (w , err .Error (), http .StatusInternalServerError )
136
+ }
137
+ }
138
+
139
+ func (s * Server ) handleEdit (w http.ResponseWriter , r * http.Request ) {
140
+ id := getBid (chi .URLParam (r , "id" ))
141
+ rule , found := s .Readability .Rules .GetByID (r .Context (), id )
142
+ if ! found {
143
+ http .Error (w , "Rule not found" , http .StatusNotFound )
144
+ return
145
+ }
146
+ data := struct {
147
+ Title string
148
+ Rule datastore.Rule
149
+ }{
150
+ Title : "Редактирование правила" ,
151
+ Rule : rule ,
152
+ }
153
+ err := s .rulePage .ExecuteTemplate (w , "base.gohtml" , data )
154
+ if err != nil {
155
+ log .Printf ("[WARN] failed to render edit template, %v" , err )
156
+ http .Error (w , err .Error (), http .StatusInternalServerError )
157
+ }
158
+ }
159
+
122
160
func (s * Server ) extractArticle (w http.ResponseWriter , r * http.Request ) {
123
161
artRequest := extractor.Response {}
124
162
if err := render .DecodeJSON (r .Body , & artRequest ); err != nil {
@@ -176,61 +214,108 @@ func (s *Server) extractArticleEmulateReadability(w http.ResponseWriter, r *http
176
214
render .JSON (w , r , & res )
177
215
}
178
216
179
- // getRule find rule matching url param (domain portion only)
180
- func (s * Server ) getRule (w http.ResponseWriter , r * http.Request ) {
181
- url := r .URL .Query ().Get ("url" )
182
- if url == "" {
183
- render .Status (r , http .StatusExpectationFailed )
184
- render .JSON (w , r , JSON {"error" : "no url passed" })
217
+ // generates previews for the provided test URLs
218
+ func (s * Server ) handlePreview (w http.ResponseWriter , r * http.Request ) {
219
+ err := r .ParseForm ()
220
+ if err != nil {
221
+ http .Error (w , "Failed to parse form" , http .StatusBadRequest )
185
222
return
186
223
}
187
224
188
- rule , found := s .Readability .Rules .Get (r .Context (), url )
189
- if ! found {
190
- render .Status (r , http .StatusBadRequest )
191
- render .JSON (w , r , JSON {"error" : "not found" })
192
- return
225
+ testURLs := strings .Split (r .FormValue ("test_urls" ), "\n " )
226
+ content := strings .TrimSpace (r .FormValue ("content" ))
227
+ log .Printf ("[INFO] test urls: %v" , testURLs )
228
+ log .Printf ("[INFO] custom rule: %v" , content )
229
+
230
+ // Create a temporary rule for extraction
231
+ var tempRule * datastore.Rule
232
+ if content != "" {
233
+ tempRule = & datastore.Rule {
234
+ Enabled : true ,
235
+ Content : content ,
236
+ }
193
237
}
194
238
195
- log .Printf ("[DEBUG] rule for %s found, %v" , url , rule )
196
- render .JSON (w , r , rule )
197
- }
239
+ var responses []extractor.Response
240
+ for _ , url := range testURLs {
241
+ url = strings .TrimSpace (url )
242
+ if url == "" {
243
+ continue
244
+ }
198
245
199
- // getRuleByID returns rule by id - GET /rule/:id"
200
- func (s * Server ) getRuleByID (w http.ResponseWriter , r * http.Request ) {
201
- id := getBid (chi .URLParam (r , "id" ))
202
- rule , found := s .Readability .Rules .GetByID (r .Context (), id )
203
- if ! found {
204
- render .Status (r , http .StatusBadRequest )
205
- render .JSON (w , r , JSON {"error" : "not found" })
206
- return
246
+ log .Printf ("[DEBUG] custom rule provided for %s: %v" , url , tempRule )
247
+ result , e := s .Readability .ExtractByRule (r .Context (), url , tempRule )
248
+ if e != nil {
249
+ log .Printf ("[WARN] failed to extract content for %s: %v" , url , e )
250
+ continue
251
+ }
252
+
253
+ responses = append (responses , * result )
254
+ }
255
+
256
+ // create a new type where Rich would be type template.HTML instead of string,
257
+ // to avoid escaping in the template
258
+ type result struct {
259
+ Title string
260
+ Excerpt string
261
+ Rich template.HTML
262
+ Content string
263
+ }
264
+
265
+ var results []result
266
+ for _ , r := range responses {
267
+ results = append (results , result {
268
+ Title : r .Title ,
269
+ Excerpt : r .Excerpt ,
270
+ //nolint: gosec // this content is escaped by Extractor, so it's safe to use it as is
271
+ Rich : template .HTML (r .Rich ),
272
+ Content : r .Content ,
273
+ })
274
+ }
275
+
276
+ data := struct {
277
+ Results []result
278
+ }{
279
+ Results : results ,
207
280
}
208
- log .Printf ("[DEBUG] rule for %s found, %v" , id .Hex (), rule )
209
- render .JSON (w , r , & rule )
210
- }
211
281
212
- // getAllRules returns list of all rules, including disabled
213
- func (s * Server ) getAllRules (w http.ResponseWriter , r * http.Request ) {
214
- render .JSON (w , r , s .Readability .Rules .All (r .Context ()))
282
+ err = s .rulePage .ExecuteTemplate (w , "preview.gohtml" , data )
283
+ if err != nil {
284
+ http .Error (w , err .Error (), http .StatusInternalServerError )
285
+ }
215
286
}
216
287
217
288
// saveRule upsert rule, forcing enabled=true
218
289
func (s * Server ) saveRule (w http.ResponseWriter , r * http.Request ) {
219
- rule := datastore.Rule {}
290
+ err := r .ParseForm ()
291
+ if err != nil {
292
+ http .Error (w , "Failed to parse form" , http .StatusBadRequest )
293
+ return
294
+ }
295
+ rule := datastore.Rule {
296
+ Enabled : true ,
297
+ ID : getBid (r .FormValue ("id" )),
298
+ Domain : r .FormValue ("domain" ),
299
+ Author : r .FormValue ("author" ),
300
+ Content : r .FormValue ("content" ),
301
+ MatchURLs : strings .Split (r .FormValue ("match_url" ), "\n " ),
302
+ Excludes : strings .Split (r .FormValue ("excludes" ), "\n " ),
303
+ TestURLs : strings .Split (r .FormValue ("test_urls" ), "\n " ),
304
+ }
220
305
221
- if err := render . DecodeJSON ( r . Body , & rule ); err != nil {
222
- render . Status ( r , http . StatusInternalServerError )
223
- render . JSON (w , r , JSON { "error" : err . Error ()} )
306
+ // return error in case domain is not set
307
+ if rule . Domain == "" {
308
+ http . Error (w , "Domain is required" , http . StatusBadRequest )
224
309
return
225
310
}
226
311
227
- rule .Enabled = true
228
312
srule , err := s .Readability .Rules .Save (r .Context (), rule )
229
313
if err != nil {
230
- render .Status (r , http .StatusBadRequest )
231
- render .JSON (w , r , JSON {"error" : err .Error ()})
314
+ http .Error (w , err .Error (), http .StatusInternalServerError )
232
315
return
233
316
}
317
+
318
+ w .Header ().Set ("HX-Redirect" , "/" )
234
319
render .JSON (w , r , & srule )
235
320
}
236
321
0 commit comments