99 "net/http"
1010 "os"
1111 "path/filepath"
12+ "strings"
1213 "time"
1314
1415 "github.com/didip/tollbooth/v7"
@@ -33,6 +34,7 @@ type Server struct {
3334 Credentials map [string ]string
3435
3536 indexPage * template.Template
37+ rulePage * template.Template
3638}
3739
3840// JSON is a map alias, just for convenience
@@ -44,6 +46,7 @@ func (s *Server) Run(ctx context.Context, address string, port int, frontendDir
4446
4547 _ = os .Mkdir (filepath .Join (frontendDir , "components" ), 0o700 )
4648 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" )))
4750 s .indexPage = template .Must (template .Must (t .Clone ()).ParseFiles (filepath .Join (frontendDir , "index.gohtml" )))
4851 httpServer := & http.Server {
4952 Addr : fmt .Sprintf ("%s:%d" , address , port ),
@@ -77,20 +80,19 @@ func (s *Server) routes(frontendDir string) chi.Router {
7780 router .Route ("/api" , func (r chi.Router ) {
7881 r .Get ("/content/v1/parser" , s .extractArticleEmulateReadability )
7982 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 )
8483 r .Post ("/auth" , s .authFake )
8584
8685 r .Group (func (protected chi.Router ) {
8786 protected .Use (basicAuth ("ureadability" , s .Credentials ))
8887 protected .Post ("/rule" , s .saveRule )
8988 protected .Post ("/toggle-rule/{id}" , s .toggleRule )
89+ protected .Post ("/preview" , s .handlePreview )
9090 })
9191 })
9292
9393 router .Get ("/" , s .handleIndex )
94+ router .Get ("/add/" , s .handleAdd )
95+ router .Get ("/edit/{id}" , s .handleEdit )
9496
9597 _ = os .Mkdir (filepath .Join (frontendDir , "static" ), 0o700 )
9698 fs , err := UM .NewFileServer ("/" , filepath .Join (frontendDir , "static" ), UM .FsOptSPA )
@@ -119,6 +121,42 @@ func (s *Server) handleIndex(w http.ResponseWriter, r *http.Request) {
119121 }
120122}
121123
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+
122160func (s * Server ) extractArticle (w http.ResponseWriter , r * http.Request ) {
123161 artRequest := extractor.Response {}
124162 if err := render .DecodeJSON (r .Body , & artRequest ); err != nil {
@@ -176,61 +214,108 @@ func (s *Server) extractArticleEmulateReadability(w http.ResponseWriter, r *http
176214 render .JSON (w , r , & res )
177215}
178216
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 )
185222 return
186223 }
187224
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+ }
193237 }
194238
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+ }
198245
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 ,
207280 }
208- log .Printf ("[DEBUG] rule for %s found, %v" , id .Hex (), rule )
209- render .JSON (w , r , & rule )
210- }
211281
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+ }
215286}
216287
217288// saveRule upsert rule, forcing enabled=true
218289func (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+ }
220305
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 )
224309 return
225310 }
226311
227- rule .Enabled = true
228312 srule , err := s .Readability .Rules .Save (r .Context (), rule )
229313 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 )
232315 return
233316 }
317+
318+ w .Header ().Set ("HX-Redirect" , "/" )
234319 render .JSON (w , r , & srule )
235320}
236321
0 commit comments