├── static
├── radio.ico
└── coffee.ico
├── screenshots
├── freerss_portal.png
└── freerss_withpreview.png
├── menu.svg
├── index.js
├── postcss.config.js
├── archive
├── drag_svelte.js
├── drag_svelte.html
├── drag.html
├── drag_vanilla.js
├── Drag.svelte
└── rssparts.html
├── tailwind.config.js
├── cheveron-down.svg
├── .gitignore
├── refresh.svg
├── spec.txt
├── index.html
├── rollup.config.js
├── README.md
├── twsrc.css
├── LICENSE
├── Makefile
├── about.html
├── DelUserForm.svelte
├── LoginForm.svelte
├── SignupForm.svelte
├── EditUserForm.svelte
├── App.svelte
├── Grid.svelte
├── RSSView.svelte
└── freerss.go
/static/radio.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robdelacruz/freerss/HEAD/static/radio.ico
--------------------------------------------------------------------------------
/static/coffee.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robdelacruz/freerss/HEAD/static/coffee.ico
--------------------------------------------------------------------------------
/screenshots/freerss_portal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robdelacruz/freerss/HEAD/screenshots/freerss_portal.png
--------------------------------------------------------------------------------
/screenshots/freerss_withpreview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/robdelacruz/freerss/HEAD/screenshots/freerss_withpreview.png
--------------------------------------------------------------------------------
/menu.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | import App from "./App.svelte";
2 | let app = new App({
3 | target: document.querySelector("#container"),
4 | props: {
5 | },
6 | });
7 |
8 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('cssnano')({
4 | preset: 'default',
5 | }),
6 | ],
7 | };
8 |
--------------------------------------------------------------------------------
/archive/drag_svelte.js:
--------------------------------------------------------------------------------
1 | import Drag from "./Drag.svelte";
2 | const drag = new Drag({
3 | target: document.querySelector("#a"),
4 | props: {},
5 | });
6 |
7 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | content: ["./*.{html,js,svelte,css}"],
3 | theme: {
4 | extend: {},
5 | },
6 | plugins: [],
7 | }
8 |
9 |
--------------------------------------------------------------------------------
/cheveron-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.swp
2 | *.o
3 | *.db
4 | *.map
5 | node_modules
6 | package.json
7 | package-lock.json
8 | static/style.css
9 | static/bundle.css
10 | static/bundle.js
11 | freerss
12 |
13 |
--------------------------------------------------------------------------------
/refresh.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/spec.txt:
--------------------------------------------------------------------------------
1 | https://github.com/mmcdole/gofeed
2 | https://github.com/gorilla/feeds
3 |
4 | feedsearch:
5 | https://feedsearch.dev/api/v1/search?result=true&url=twit.tv
6 | https://developer.feedly.com/v3/search/
7 |
8 | https://stackoverflow.com/questions/45267125/how-to-generate-unique-random-alphanumeric-tokens-in-golang
9 |
10 | https://stackoverflow.com/questions/8054429/how-do-i-handle-a-click-anywhere-in-the-page-even-when-a-certain-element-stops
11 |
12 |
--------------------------------------------------------------------------------
/archive/drag_svelte.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | jstest
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | FreeRSS
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import svelte from "rollup-plugin-svelte";
2 | import resolve from "@rollup/plugin-node-resolve";
3 |
4 | export default {
5 | input: "index.js",
6 | output: {
7 | file: "static/bundle.js",
8 | format: "iife",
9 | name: "app",
10 | sourcemap: true
11 | },
12 | plugins: [
13 | svelte({
14 | compilerOptions: {
15 | dev: true,
16 | css: false,
17 | }
18 | }),
19 | resolve({
20 | browser: true,
21 | dedupe: ["svelte"]
22 | })
23 | ]
24 | };
25 |
26 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## FreeRSS
2 |
3 | Web-based RSS Viewer and Portal.
4 |
5 | Dependencies and Tools used:
6 | - [gofeed library](https://github.com/mmcdole/gofeed)
7 | - [Svelte](https://svelte.dev)
8 | - [Tailwind CSS](https://tailwindcss.com)
9 | - [Zondicons](http://www.zondicons.com/)
10 |
11 | ## Usage
12 |
13 | Run once:
14 |
15 | $ make dep
16 | $ make webtools
17 |
18 | Build and test:
19 |
20 | $ make clean
21 | $ make
22 | $ freerss -i portal.db
23 |
24 | Run 'freerss portal.db' to start the web service.
25 |
26 | ## Screenshots
27 |
28 | 
29 | 
30 |
31 | ## Contact
32 | Twitter: @robcomputing
33 | Source: http://github.com/robdelacruz/freerss
34 |
35 |
--------------------------------------------------------------------------------
/twsrc.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 |
3 | @tailwind components;
4 |
5 | @tailwind utilities;
6 |
7 | .w-widget {width: 20rem;}
8 |
9 | .widget {@apply bg-gray-800 text-gray-200 mb-2 py-2 px-4;}
10 |
11 | .linklist li {@apply mb-2 leading-tight;}
12 |
13 | .content h1, .content h2, .content h3, .content h4 {@apply text-xs;}
14 | .content p, .content ul, .content ol, .content blockquote, .content pre {@apply mb-1;}
15 | .content a {@apply underline;}
16 | .content ul {@apply list-disc list-inside;}
17 | .content ol {@apply list-decimal list-inside;}
18 | .content blockquote {@apply bg-gray-700 px-8 py-4;}
19 | .content pre {@apply bg-gray-700 p-2;}
20 | .content code {@apply font-mono;}
21 | .content img[src*="#thumb"] {width: 100px;}
22 | .content img[src*="#sm"] {width: 150px;}
23 | .content img[src*="#med"] {width: 180px;}
24 | .content img[src*="#lg"] {width: 200px;}
25 | .content img[src*="#xl"] {width: 300px;}
26 | .content img[src*="#stretch"] {width: 100%;}
27 | .content img[src*="#left"] {@apply float-left mr-2;}
28 | .content img[src*="#right"] {@apply float-right ml-2;}
29 | .content iframe {@apply w-full;}
30 |
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Rob de la Cruz
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/archive/drag.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | jstest
7 |
8 |
9 |
10 |
11 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | # Usage:
2 | # 'make dep' and 'make webtools' to install dependencies.
3 | # 'make clean' to clear all work files
4 | # 'make' to build css and js into static/
5 | # 'make serve' to start dev webserver
6 |
7 | NODE_VER = 17
8 |
9 | JSFILES = index.js App.svelte Grid.svelte RSSView.svelte LoginForm.svelte SignupForm.svelte EditUserForm.svelte DelUserForm.svelte
10 |
11 | all: freerss static/style.css static/bundle.js
12 |
13 | nodejs:
14 | curl -fsSL https://deb.nodesource.com/setup_$(NODE_VER).x | sudo bash -
15 | sudo apt install nodejs
16 | sudo npm install -g npx
17 |
18 | dep:
19 | go env -w GO111MODULE=auto
20 | go get github.com/mmcdole/gofeed
21 | go get github.com/gorilla/feeds
22 |
23 | webtools:
24 | npm install --save-dev tailwindcss
25 | npm install --save-dev postcss
26 | npm install --save-dev postcss-cli
27 | npm install --save-dev cssnano
28 | npm install --save-dev svelte
29 | npm install --save-dev rollup
30 | npm install --save-dev rollup-plugin-svelte
31 | npm install --save-dev @rollup/plugin-node-resolve
32 |
33 | static/style.css: twsrc.css tailwind.config.js
34 | npx tailwind -i twsrc.css -o twsrc.o 1>/dev/null
35 | npx postcss twsrc.o > static/style.css
36 | #npx tailwind -i twsrc.css -o static/style.css 1>/dev/null
37 |
38 | freerss: freerss.go
39 | go build -o freerss freerss.go
40 |
41 | static/bundle.js: $(JSFILES)
42 | npx rollup -c
43 |
44 | clean:
45 | rm -rf freerss static/*.js static/*.css static/*.map
46 |
47 | serve:
48 | python -m SimpleHTTPServer
49 |
50 |
--------------------------------------------------------------------------------
/about.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | About FreeRSS
7 |
8 |
9 |
10 |
17 |
18 |
19 |
20 |
FreeRSS v1
21 |
RSS Viewer and Web Portal
22 |
Copyright (c) 2020 by Rob de la Cruz
23 |
Project Page
24 |
Support FreeRSS Development and Hosting
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/archive/drag_vanilla.js:
--------------------------------------------------------------------------------
1 | function targetHasClass(e, ...cc) {
2 | for (let i=0; i < cc.length; i++) {
3 | let c = cc[i];
4 | if (e.target.classList.contains(c)) {
5 | return true;
6 | }
7 | }
8 | return false;
9 | }
10 |
11 | function ondragstart(e) {
12 | if (!targetHasClass(e, "widget")) {
13 | return;
14 | }
15 |
16 | e.dataTransfer.setData("text/html", e.target.outerHTML);
17 | }
18 |
19 | function ondragover(e) {
20 | if (!targetHasClass(e, "widget", "dropzone")) {
21 | return;
22 | }
23 |
24 | e.preventDefault();
25 | e.dataTransfer.dropEffect = "move";
26 | }
27 |
28 | function ondrop(e) {
29 | if (!targetHasClass(e, "widget", "dropzone")) {
30 | return;
31 | }
32 |
33 | e.preventDefault();
34 | let outerHTML = e.dataTransfer.getData("text/html");
35 |
36 | if (targetHasClass(e, "widget")) {
37 | e.target.insertAdjacentHTML("beforebegin", outerHTML);
38 | return;
39 | }
40 |
41 | // "dropzone" column
42 | let childWidgets = e.target.querySelectorAll(".widget");
43 | if (childWidgets.length == 0) {
44 | // empty column
45 | e.target.insertAdjacentHTML("afterbegin", outerHTML);
46 | return;
47 | }
48 | let bottomWidget = childWidgets[childWidgets.length-1];
49 | bottomWidget.insertAdjacentHTML("afterend", outerHTML);
50 | }
51 |
52 | function ondragend(e) {
53 | if (!targetHasClass(e, "widget", "dropzone")) {
54 | return;
55 | }
56 |
57 | // if drop was completed
58 | if (e.dataTransfer.dropEffect != "none") {
59 | e.target.remove();
60 | }
61 | }
62 |
63 | document.addEventListener("dragstart", ondragstart);
64 | document.addEventListener("dragover", ondragover);
65 | document.addEventListener("drop", ondrop);
66 | document.addEventListener("dragend", ondragend);
67 |
68 |
--------------------------------------------------------------------------------
/archive/Drag.svelte:
--------------------------------------------------------------------------------
1 |
77 |
78 |
80 |
81 |
82 | {#each cols as col, icol}
83 |
88 | {/each}
89 |
90 |
91 |
92 |
93 |
99 |
100 |
--------------------------------------------------------------------------------
/DelUserForm.svelte:
--------------------------------------------------------------------------------
1 |
27 |
28 |
89 |
90 |
--------------------------------------------------------------------------------
/LoginForm.svelte:
--------------------------------------------------------------------------------
1 |
63 |
64 |
92 |
93 |
--------------------------------------------------------------------------------
/SignupForm.svelte:
--------------------------------------------------------------------------------
1 |
30 |
31 |
96 |
97 |
--------------------------------------------------------------------------------
/EditUserForm.svelte:
--------------------------------------------------------------------------------
1 |
34 |
35 |
104 |
105 |
--------------------------------------------------------------------------------
/App.svelte:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 | {#if ui.username != ""}
10 |
26 |
Logout
27 | {:else}
28 |
Login
29 | {/if}
30 |
31 |
32 |
33 | {#if ui.mode == ""}
34 |
35 | {:else if ui.mode == "login"}
36 |
41 | {:else if ui.mode == "signup"}
42 |
47 | {:else if ui.mode == "edituser"}
48 |
53 | {:else if ui.mode == "deluser"}
54 |
59 | {/if}
60 |
61 |
62 |
162 |
163 |
--------------------------------------------------------------------------------
/archive/rssparts.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | jstest
7 |
8 |
9 |
10 |
11 |
12 |
13 |
92 |
93 |
141 |
142 |
164 |
165 |
166 |
167 |
--------------------------------------------------------------------------------
/Grid.svelte:
--------------------------------------------------------------------------------
1 |
292 |
293 |
294 | {#if ui.mode == ""}
295 | {#each ui.cols as col, icol}
296 |
297 | {#each ui.cols[icol] as w, irow (w.wid)}
298 |
299 | {/each}
300 |
301 | {:else}
302 |
You don't have any widgets yet.
303 | Add a new widget or
304 | Add sample widgets
305 |
306 | {/each}
307 | {:else if ui.mode == "loading"}
308 |
Loading...
309 | {/if}
310 |
311 |
312 |
319 |
320 |
--------------------------------------------------------------------------------
/RSSView.svelte:
--------------------------------------------------------------------------------
1 |
91 |
92 |
293 |
294 |
--------------------------------------------------------------------------------
/freerss.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "database/sql"
6 | "encoding/json"
7 | "errors"
8 | "fmt"
9 | "html"
10 | "io"
11 | "io/ioutil"
12 | "log"
13 | "net/http"
14 | "net/url"
15 | "os"
16 | "path"
17 | "strconv"
18 | "strings"
19 | "time"
20 |
21 | xhtml "golang.org/x/net/html"
22 | "golang.org/x/net/html/atom"
23 |
24 | "github.com/gorilla/feeds"
25 | _ "github.com/mattn/go-sqlite3"
26 | "github.com/mmcdole/gofeed"
27 | "golang.org/x/crypto/bcrypt"
28 | )
29 |
30 | type PrintFunc func(format string, a ...interface{}) (n int, err error)
31 |
32 | type Feed struct {
33 | Title string `json:"title"`
34 | Url string `json:"url"`
35 | Desc string `json:"desc"`
36 | Pubdate string `json:"pubdate"`
37 | Pubtime time.Time `json:"-"`
38 | Entries []*Entry `json:"entries"`
39 | }
40 | type Entry struct {
41 | Title string `json:"title"`
42 | Url string `json:"url"`
43 | Desc string `json:"desc"`
44 | Body string `json:"body"`
45 | Author string `json:"author"`
46 | Pubdate string `json:"pubdate"`
47 | Pubtime time.Time `json:"-"`
48 | }
49 | type User struct {
50 | Userid int64
51 | Username string
52 | HashedPwd string
53 | }
54 |
55 | func (f *Feed) String() string {
56 | bs, err := json.MarshalIndent(f, "", "\t")
57 | if err != nil {
58 | return ""
59 | }
60 | return string(bs)
61 | }
62 | func (e *Entry) String() string {
63 | bs, err := json.MarshalIndent(e, "", "\t")
64 | if err != nil {
65 | return ""
66 | }
67 | return string(bs)
68 | }
69 | func parseFeed(gfparser *gofeed.Parser, body string, maxitems int) (*Feed, error) {
70 | gf, err := gfparser.ParseString(body)
71 | if err != nil {
72 | return nil, err
73 | }
74 |
75 | f := Feed{}
76 | f.Title = gf.Title
77 | f.Url = gf.Link
78 | f.Desc = gf.Description
79 | convdate(gf.PublishedParsed, &f.Pubtime, &f.Pubdate)
80 |
81 | if maxitems == 0 {
82 | maxitems = len(gf.Items)
83 | }
84 |
85 | for i, it := range gf.Items {
86 | e := Entry{}
87 | e.Title = it.Title
88 | e.Url = it.Link
89 | e.Desc = it.Description
90 | e.Body = it.Content
91 | convdate(it.PublishedParsed, &e.Pubtime, &e.Pubdate)
92 |
93 | f.Entries = append(f.Entries, &e)
94 |
95 | if i >= maxitems-1 {
96 | break
97 | }
98 | }
99 | return &f, nil
100 | }
101 | func convdate(t *time.Time, dt *time.Time, sdt *string) {
102 | if t != nil {
103 | *dt = *t
104 | *sdt = dt.Format(time.RFC3339)
105 | }
106 | }
107 |
108 | func main() {
109 | err := run(os.Args[1:])
110 | if err != nil {
111 | fmt.Fprintf(os.Stderr, "%s\n", err)
112 | os.Exit(1)
113 | }
114 | }
115 |
116 | func runtesthash(args []string) error {
117 | if len(args) == 0 {
118 | return errors.New("Please specify a username")
119 | }
120 | if len(args) == 1 {
121 | username := args[0]
122 | shash := genHash(username)
123 | fmt.Printf("%s\n", shash)
124 | return nil
125 | }
126 |
127 | username := args[0]
128 | shash := args[1]
129 | if validateHash(shash, username) {
130 | fmt.Printf("validate ok\n")
131 | } else {
132 | fmt.Printf("not validated\n")
133 | }
134 | return nil
135 | }
136 | func runtestsignup(args []string) error {
137 | sw, parms := parseArgs(args)
138 | // [-i new_file] Create and initialize db file
139 | if sw["i"] != "" {
140 | dbfile := sw["i"]
141 | if fileExists(dbfile) {
142 | return fmt.Errorf("File '%s' already exists. Can't initialize it.\n", dbfile)
143 | }
144 | createTables(dbfile)
145 | return nil
146 | }
147 |
148 | // Need to specify a db file as first parameter.
149 | if len(parms) == 0 {
150 | return errors.New("Specify a db file")
151 | }
152 |
153 | // Exit if db file doesn't exist.
154 | dbfile := parms[0]
155 | if !fileExists(dbfile) {
156 | return fmt.Errorf(`Database file '%s' doesn't exist. Create one using:
157 | freerss -i
158 | `, dbfile)
159 | }
160 |
161 | db, err := sql.Open("sqlite3", dbfile)
162 | if err != nil {
163 | return fmt.Errorf("Error opening '%s' (%s)\n", dbfile, err)
164 | }
165 |
166 | if len(parms) < 3 {
167 | return fmt.Errorf("Specify a username and password")
168 | }
169 | username := parms[1]
170 | pwd := parms[2]
171 | err = signup(db, username, pwd)
172 | if err != nil {
173 | return err
174 | }
175 | return nil
176 | }
177 |
178 | func rundiscoverrss(args []string) error {
179 | if len(args) == 0 {
180 | return errors.New("Please specify a feed url")
181 | }
182 | qurl := args[0]
183 | feeds, err := discoverfeeds(qurl)
184 | if err != nil {
185 | return err
186 | }
187 |
188 | if len(feeds) == 0 {
189 | fmt.Println("No feeds found.")
190 | return nil
191 | }
192 |
193 | fmt.Println("Found feeds:")
194 | for _, feed := range feeds {
195 | fmt.Println(feed)
196 | }
197 | return nil
198 | }
199 |
200 | func run(args []string) error {
201 | sw, parms := parseArgs(args)
202 |
203 | // [-i new_file] Create and initialize db file
204 | if sw["i"] != "" {
205 | dbfile := sw["i"]
206 | if fileExists(dbfile) {
207 | return fmt.Errorf("File '%s' already exists. Can't initialize it.\n", dbfile)
208 | }
209 | createTables(dbfile)
210 | return nil
211 | }
212 |
213 | // Need to specify a db file as first parameter.
214 | if len(parms) == 0 {
215 | s := `Usage:
216 |
217 | Start webservice using database file:
218 | freerss [port]
219 |
220 | Initialize new database file:
221 | freerss -i
222 |
223 | `
224 | fmt.Printf(s)
225 | return nil
226 | }
227 |
228 | // Exit if db file doesn't exist.
229 | dbfile := parms[0]
230 | if !fileExists(dbfile) {
231 | return fmt.Errorf(`Database file '%s' doesn't exist. Create one using:
232 | freerss -i
233 | `, dbfile)
234 | }
235 |
236 | db, err := sql.Open("sqlite3", dbfile)
237 | if err != nil {
238 | return fmt.Errorf("Error opening '%s' (%s)\n", dbfile, err)
239 | }
240 |
241 | gfparser := gofeed.NewParser()
242 |
243 | http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { http.ServeFile(w, r, "./static/radio.ico") })
244 | http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
245 | http.Handle("/", http.StripPrefix("/", http.FileServer(http.Dir("./"))))
246 | http.HandleFunc("/api/feed/", feedHandler(nil, gfparser))
247 | http.HandleFunc("/api/discoverfeed/", discoverfeedHandler(nil, gfparser))
248 | http.HandleFunc("/api/login/", loginHandler(db))
249 | http.HandleFunc("/api/signup/", signupHandler(db))
250 | http.HandleFunc("/api/edituser/", edituserHandler(db))
251 | http.HandleFunc("/api/deluser/", deluserHandler(db))
252 | http.HandleFunc("/api/savegrid/", savegridHandler(db))
253 | http.HandleFunc("/api/loadgrid/", loadgridHandler(db))
254 | http.HandleFunc("/api/testfeed/", testfeedHandler(db))
255 |
256 | port := "8000"
257 | if len(parms) > 1 {
258 | port = parms[1]
259 | }
260 | fmt.Printf("Listening on %s...\n", port)
261 | err = http.ListenAndServe(fmt.Sprintf(":%s", port), nil)
262 | return err
263 | }
264 |
265 | func createTables(newfile string) {
266 | if fileExists(newfile) {
267 | s := fmt.Sprintf("File '%s' already exists. Can't initialize it.\n", newfile)
268 | fmt.Printf(s)
269 | os.Exit(1)
270 | }
271 |
272 | db, err := sql.Open("sqlite3", newfile)
273 | if err != nil {
274 | fmt.Printf("Error opening '%s' (%s)\n", newfile, err)
275 | os.Exit(1)
276 | }
277 |
278 | ss := []string{
279 | "CREATE TABLE user (user_id INTEGER PRIMARY KEY NOT NULL, username TEXT UNIQUE, password TEXT);",
280 | "INSERT INTO user (user_id, username, password) VALUES (1, 'admin', '');",
281 | "CREATE TABLE savedgrid (user_id INTEGER PRIMARY KEY NOT NULL, gridjson TEXT);",
282 | }
283 |
284 | tx, err := db.Begin()
285 | if err != nil {
286 | log.Printf("DB error (%s)\n", err)
287 | os.Exit(1)
288 | }
289 | for _, s := range ss {
290 | _, err := txexec(tx, s)
291 | if err != nil {
292 | tx.Rollback()
293 | log.Printf("DB error (%s)\n", err)
294 | os.Exit(1)
295 | }
296 | }
297 | err = tx.Commit()
298 | if err != nil {
299 | log.Printf("DB error (%s)\n", err)
300 | os.Exit(1)
301 | }
302 | }
303 |
304 | //*** DB functions ***
305 | func sqlstmt(db *sql.DB, s string) *sql.Stmt {
306 | stmt, err := db.Prepare(s)
307 | if err != nil {
308 | log.Fatalf("db.Prepare() sql: '%s'\nerror: '%s'", s, err)
309 | }
310 | return stmt
311 | }
312 | func sqlexec(db *sql.DB, s string, pp ...interface{}) (sql.Result, error) {
313 | stmt := sqlstmt(db, s)
314 | defer stmt.Close()
315 | return stmt.Exec(pp...)
316 | }
317 | func txstmt(tx *sql.Tx, s string) *sql.Stmt {
318 | stmt, err := tx.Prepare(s)
319 | if err != nil {
320 | log.Fatalf("tx.Prepare() sql: '%s'\nerror: '%s'", s, err)
321 | }
322 | return stmt
323 | }
324 | func txexec(tx *sql.Tx, s string, pp ...interface{}) (sql.Result, error) {
325 | stmt := txstmt(tx, s)
326 | defer stmt.Close()
327 | return stmt.Exec(pp...)
328 | }
329 |
330 | //*** Helper functions ***
331 |
332 | // Helper function to make fmt.Fprintf(w, ...) calls shorter.
333 | // Ex.
334 | // Replace:
335 | // fmt.Fprintf(w, "Some text %s.
", str)
336 | // fmt.Fprintf(w, "Some other text %s.
", str)
337 | // with the shorter version:
338 | // P := makeFprintf(w)
339 | // P("Some text %s.
", str)
340 | // P("Some other text %s.
", str)
341 | func makeFprintf(w io.Writer) func(format string, a ...interface{}) (n int, err error) {
342 | return func(format string, a ...interface{}) (n int, err error) {
343 | return fmt.Fprintf(w, format, a...)
344 | }
345 | }
346 | func listContains(ss []string, v string) bool {
347 | for _, s := range ss {
348 | if v == s {
349 | return true
350 | }
351 | }
352 | return false
353 | }
354 | func fileExists(file string) bool {
355 | _, err := os.Stat(file)
356 | if err != nil && os.IsNotExist(err) {
357 | return false
358 | }
359 | return true
360 | }
361 | func makePrintFunc(w io.Writer) func(format string, a ...interface{}) (n int, err error) {
362 | // Return closure enclosing io.Writer.
363 | return func(format string, a ...interface{}) (n int, err error) {
364 | return fmt.Fprintf(w, format, a...)
365 | }
366 | }
367 | func atoi(s string) int {
368 | if s == "" {
369 | return 0
370 | }
371 | n, err := strconv.Atoi(s)
372 | if err != nil {
373 | return 0
374 | }
375 | return n
376 | }
377 | func idtoi(sid string) int64 {
378 | return int64(atoi(sid))
379 | }
380 | func itoa(n int64) string {
381 | return strconv.FormatInt(n, 10)
382 | }
383 | func atof(s string) float64 {
384 | if s == "" {
385 | return 0.0
386 | }
387 | f, err := strconv.ParseFloat(s, 64)
388 | if err != nil {
389 | return 0.0
390 | }
391 | return f
392 | }
393 |
394 | func unescapeUrl(qurl string) string {
395 | returl := "/"
396 | if qurl != "" {
397 | returl, _ = url.QueryUnescape(qurl)
398 | }
399 | return returl
400 | }
401 | func escape(s string) string {
402 | return html.EscapeString(s)
403 | }
404 |
405 | func parseArgs(args []string) (map[string]string, []string) {
406 | switches := map[string]string{}
407 | parms := []string{}
408 |
409 | standaloneSwitches := []string{}
410 | definitionSwitches := []string{"i"}
411 | fNoMoreSwitches := false
412 | curKey := ""
413 |
414 | for _, arg := range args {
415 | if fNoMoreSwitches {
416 | // any arg after "--" is a standalone parameter
417 | parms = append(parms, arg)
418 | } else if arg == "--" {
419 | // "--" means no more switches to come
420 | fNoMoreSwitches = true
421 | } else if strings.HasPrefix(arg, "--") {
422 | switches[arg[2:]] = "y"
423 | curKey = ""
424 | } else if strings.HasPrefix(arg, "-") {
425 | if listContains(definitionSwitches, arg[1:]) {
426 | // -a "val"
427 | curKey = arg[1:]
428 | continue
429 | }
430 | for _, ch := range arg[1:] {
431 | // -a, -b, -ab
432 | sch := string(ch)
433 | if listContains(standaloneSwitches, sch) {
434 | switches[sch] = "y"
435 | }
436 | }
437 | } else if curKey != "" {
438 | switches[curKey] = arg
439 | curKey = ""
440 | } else {
441 | // standalone parameter
442 | parms = append(parms, arg)
443 | }
444 | }
445 |
446 | return switches, parms
447 | }
448 |
449 | func handleErr(w http.ResponseWriter, err error, sfunc string) {
450 | log.Printf("%s: server error (%s)\n", sfunc, err)
451 | http.Error(w, fmt.Sprintf("%s", err), 500)
452 | }
453 | func handleDbErr(w http.ResponseWriter, err error, sfunc string) bool {
454 | if err == sql.ErrNoRows {
455 | http.Error(w, "Not found.", 404)
456 | return true
457 | }
458 | if err != nil {
459 | log.Printf("%s: database error (%s)\n", sfunc, err)
460 | http.Error(w, "Server database error.", 500)
461 | return true
462 | }
463 | return false
464 | }
465 | func handleTxErr(tx *sql.Tx, err error) bool {
466 | if err != nil {
467 | tx.Rollback()
468 | return true
469 | }
470 | return false
471 | }
472 |
473 | func genHash(sinput string) string {
474 | bsHash, err := bcrypt.GenerateFromPassword([]byte(sinput), bcrypt.DefaultCost)
475 | if err != nil {
476 | return ""
477 | }
478 | return string(bsHash)
479 | }
480 | func validateHash(shash, sinput string) bool {
481 | if shash == "" && sinput == "" {
482 | return true
483 | }
484 | err := bcrypt.CompareHashAndPassword([]byte(shash), []byte(sinput))
485 | if err != nil {
486 | return false
487 | }
488 | return true
489 | }
490 |
491 | func findUser(db *sql.DB, username string) *User {
492 | s := "SELECT user_id, username, password FROM user WHERE username = ?"
493 | row := db.QueryRow(s, username)
494 | var u User
495 | err := row.Scan(&u.Userid, &u.Username, &u.HashedPwd)
496 | if err == sql.ErrNoRows {
497 | return nil
498 | }
499 | if err != nil {
500 | return nil
501 | }
502 | return &u
503 | }
504 | func isUsernameExists(db *sql.DB, username string) bool {
505 | if findUser(db, username) == nil {
506 | return false
507 | }
508 | return true
509 | }
510 |
511 | func feedHandler(db *sql.DB, gfparser *gofeed.Parser) http.HandlerFunc {
512 | return func(w http.ResponseWriter, r *http.Request) {
513 | qurl := unescapeUrl(r.FormValue("url"))
514 | if qurl == "" {
515 | http.Error(w, "?url= required", 401)
516 | return
517 | }
518 | qmaxitems := atoi(r.FormValue("maxitems"))
519 |
520 | res, err := http.Get(qurl)
521 | if err != nil {
522 | http.Error(w, fmt.Sprintf("Not found: %s", qurl), 404)
523 | return
524 | }
525 | defer res.Body.Close()
526 | bs, err := ioutil.ReadAll(res.Body)
527 | if err != nil {
528 | http.Error(w, fmt.Sprintf("error reading feed (%s)", err), 404)
529 | return
530 | }
531 |
532 | w.Header().Set("Content-Type", "application/json")
533 | P := makeFprintf(w)
534 | f, err := parseFeed(gfparser, string(bs), qmaxitems)
535 | if err != nil {
536 | handleErr(w, err, "feedHandler")
537 | return
538 | }
539 | P("%s\n", f)
540 | }
541 | }
542 |
543 | func discoverfeedHandler(db *sql.DB, gfparser *gofeed.Parser) http.HandlerFunc {
544 | return func(w http.ResponseWriter, r *http.Request) {
545 | qurl := unescapeUrl(r.FormValue("url"))
546 | if qurl == "" {
547 | http.Error(w, "?url= required", 401)
548 | return
549 | }
550 |
551 | feeds, err := discoverfeeds(qurl)
552 | if err != nil {
553 | handleErr(w, err, "discoverfeedHandler")
554 | return
555 | }
556 | bs, err := json.MarshalIndent(feeds, "", "\t")
557 | if err != nil {
558 | handleErr(w, err, "discoverfeedHandler")
559 | return
560 | }
561 |
562 | w.Header().Set("Content-Type", "application/json")
563 | P := makeFprintf(w)
564 | P("%s\n", string(bs))
565 | }
566 | }
567 | func discoverfeeds(qurl string) ([]string, error) {
568 | res, err := http.Get(qurl)
569 | if err != nil {
570 | return nil, err
571 | }
572 | defer res.Body.Close()
573 |
574 | bs, err := ioutil.ReadAll(res.Body)
575 | if err != nil {
576 | return nil, err
577 | }
578 |
579 | feeds := []string{}
580 |
581 | // Check if url is already an rss feed.
582 | gfparser := gofeed.NewParser()
583 | if isValidFeed(gfparser, bs) {
584 | feeds = append(feeds, qurl)
585 | }
586 | ubase, _ := url.Parse(qurl)
587 |
588 | surls := getFeedLinks(bs)
589 | for _, surl := range surls {
590 | surl = completeFeedUrl(ubase, surl)
591 | feeds = append(feeds, surl)
592 | }
593 |
594 | return feeds, nil
595 | }
596 | func getAttr(tok xhtml.Token, k string) string {
597 | for _, attr := range tok.Attr {
598 | if attr.Key == k {
599 | return attr.Val
600 | }
601 | }
602 | return ""
603 | }
604 | func isValidFeed(gfparser *gofeed.Parser, bs []byte) bool {
605 | _, err := parseFeed(gfparser, string(bs), 0)
606 | if err != nil {
607 | return false
608 | }
609 | return true
610 | }
611 | func getFeedLinks(bs []byte) []string {
612 | var feeds []string
613 |
614 | z := xhtml.NewTokenizer(bytes.NewReader(bs))
615 | for {
616 | tt := z.Next()
617 | if tt == xhtml.ErrorToken {
618 | break
619 | }
620 |
621 | tok := z.Token()
622 | if tok.DataAtom != atom.Link {
623 | continue
624 | }
625 | stype := getAttr(tok, "type")
626 | if stype != "application/rss+xml" && stype != "application/atom+xml" {
627 | continue
628 | }
629 | href := getAttr(tok, "href")
630 | if href == "" {
631 | continue
632 | }
633 | feeds = append(feeds, href)
634 | }
635 |
636 | return feeds
637 | }
638 | func completeFeedUrl(ubase *url.URL, sfeedurl string) string {
639 | ufeed, _ := url.Parse(sfeedurl)
640 | if ufeed.Scheme == "" {
641 | ufeed.Scheme = ubase.Scheme
642 | }
643 | if ufeed.Host == "" {
644 | ufeed.Host = ubase.Host
645 | }
646 | // if feed is relative to baseurl
647 | if !strings.HasPrefix(ufeed.Path, "/") {
648 | ufeed.Path = path.Join(ubase.Path, ufeed.Path)
649 | }
650 | return ufeed.String()
651 | }
652 |
653 | func genTok(u *User) string {
654 | tok := genHash(fmt.Sprintf("%s_%s", u.Username, u.HashedPwd))
655 | return tok
656 | }
657 | func validateTok(tok string, u *User) bool {
658 | return validateHash(tok, fmt.Sprintf("%s_%s", u.Username, u.HashedPwd))
659 | }
660 |
661 | var ErrLoginIncorrect = errors.New("Incorrect username or password")
662 |
663 | func login(db *sql.DB, username, pwd string) (string, error) {
664 | var u User
665 | s := "SELECT user_id, username, password FROM user WHERE username = ?"
666 | row := db.QueryRow(s, username)
667 | err := row.Scan(&u.Userid, &u.Username, &u.HashedPwd)
668 | if err == sql.ErrNoRows {
669 | return "", ErrLoginIncorrect
670 | }
671 | if err != nil {
672 | return "", err
673 | }
674 | if !validateHash(u.HashedPwd, pwd) {
675 | return "", ErrLoginIncorrect
676 | }
677 |
678 | // Return session token, this will be used to authenticate username
679 | // on every request by calling validateTok()
680 | tok := genTok(&u)
681 | return tok, nil
682 | }
683 |
684 | type LoginResult struct {
685 | Tok string `json:"tok"`
686 | Error string `json:"error"`
687 | }
688 |
689 | func loginHandler(db *sql.DB) http.HandlerFunc {
690 | type LoginReq struct {
691 | Username string `json:"username"`
692 | Pwd string `json:"pwd"`
693 | }
694 | return func(w http.ResponseWriter, r *http.Request) {
695 | if r.Method != "POST" {
696 | http.Error(w, "Use POST method", 401)
697 | return
698 | }
699 | bs, err := ioutil.ReadAll(r.Body)
700 | if err != nil {
701 | handleErr(w, err, "loginHandler")
702 | return
703 | }
704 | var loginreq LoginReq
705 | err = json.Unmarshal(bs, &loginreq)
706 | if err != nil {
707 | handleErr(w, err, "loginHandler")
708 | return
709 | }
710 |
711 | var result LoginResult
712 | tok, err := login(db, loginreq.Username, loginreq.Pwd)
713 | if err != nil {
714 | result.Error = fmt.Sprintf("%s", err)
715 | }
716 | result.Tok = tok
717 |
718 | w.Header().Set("Content-Type", "application/json")
719 | P := makeFprintf(w)
720 | bs, _ = json.MarshalIndent(result, "", "\t")
721 | P("%s\n", string(bs))
722 | }
723 | }
724 |
725 | func signup(db *sql.DB, username, pwd string) error {
726 | if isUsernameExists(db, username) {
727 | return fmt.Errorf("username '%s' already exists", username)
728 | }
729 |
730 | hashedPwd := genHash(pwd)
731 | s := "INSERT INTO user (username, password) VALUES (?, ?);"
732 | _, err := sqlexec(db, s, username, hashedPwd)
733 | if err != nil {
734 | return fmt.Errorf("DB error creating user: %s", err)
735 | }
736 | return nil
737 | }
738 | func signupHandler(db *sql.DB) http.HandlerFunc {
739 | type SignupReq struct {
740 | Username string `json:"username"`
741 | Pwd string `json:"pwd"`
742 | }
743 | return func(w http.ResponseWriter, r *http.Request) {
744 | if r.Method != "POST" {
745 | http.Error(w, "Use POST method", 401)
746 | return
747 | }
748 |
749 | bs, err := ioutil.ReadAll(r.Body)
750 | if err != nil {
751 | handleErr(w, err, "signupHandler")
752 | return
753 | }
754 | var signupreq SignupReq
755 | err = json.Unmarshal(bs, &signupreq)
756 | if err != nil {
757 | handleErr(w, err, "signupHandler")
758 | return
759 | }
760 | if signupreq.Username == "" {
761 | http.Error(w, "username required", 401)
762 | return
763 | }
764 |
765 | w.Header().Set("Content-Type", "application/json")
766 | P := makeFprintf(w)
767 |
768 | // Attempt to sign up new user.
769 | var result LoginResult
770 | err = signup(db, signupreq.Username, signupreq.Pwd)
771 | if err != nil {
772 | result.Error = fmt.Sprintf("%s", err)
773 | bs, _ := json.MarshalIndent(result, "", "\t")
774 | P("%s\n", string(bs))
775 | return
776 | }
777 |
778 | // Log in the newly signed up user.
779 | tok, err := login(db, signupreq.Username, signupreq.Pwd)
780 | result.Tok = tok
781 | if err != nil {
782 | result.Error = fmt.Sprintf("%s", err)
783 | }
784 | bs, _ = json.MarshalIndent(result, "", "\t")
785 | P("%s\n", string(bs))
786 | }
787 | }
788 |
789 | func edituser(db *sql.DB, username, pwd string, newpwd string) error {
790 | // Validate existing password
791 | _, err := login(db, username, pwd)
792 | if err != nil {
793 | return err
794 | }
795 |
796 | // Set new password
797 | hashedPwd := genHash(newpwd)
798 | s := "UPDATE user SET password = ? WHERE username = ?"
799 | _, err = sqlexec(db, s, hashedPwd, username)
800 | if err != nil {
801 | return fmt.Errorf("DB error updating user password: %s", err)
802 | }
803 | return nil
804 | }
805 | func edituserHandler(db *sql.DB) http.HandlerFunc {
806 | type EditUserReq struct {
807 | Username string `json:"username"`
808 | Pwd string `json:"pwd"`
809 | NewPwd string `json:"newpwd"`
810 | }
811 | return func(w http.ResponseWriter, r *http.Request) {
812 | if r.Method != "POST" {
813 | http.Error(w, "Use POST method", 401)
814 | return
815 | }
816 |
817 | bs, err := ioutil.ReadAll(r.Body)
818 | if err != nil {
819 | handleErr(w, err, "edituserHandler")
820 | return
821 | }
822 | var req EditUserReq
823 | err = json.Unmarshal(bs, &req)
824 | if err != nil {
825 | handleErr(w, err, "edituserHandler")
826 | return
827 | }
828 | if req.Username == "" {
829 | http.Error(w, "username required", 401)
830 | return
831 | }
832 |
833 | w.Header().Set("Content-Type", "application/json")
834 | P := makeFprintf(w)
835 |
836 | // Attempt to edit user.
837 | var result LoginResult
838 | err = edituser(db, req.Username, req.Pwd, req.NewPwd)
839 | if err != nil {
840 | result.Error = fmt.Sprintf("%s", err)
841 | bs, _ := json.MarshalIndent(result, "", "\t")
842 | P("%s\n", string(bs))
843 | return
844 | }
845 |
846 | // Log in the newly edited user.
847 | tok, err := login(db, req.Username, req.NewPwd)
848 | result.Tok = tok
849 | if err != nil {
850 | result.Error = fmt.Sprintf("%s", err)
851 | }
852 | bs, _ = json.MarshalIndent(result, "", "\t")
853 | P("%s\n", string(bs))
854 | }
855 | }
856 |
857 | func deluser(db *sql.DB, username, pwd string) error {
858 | // Validate existing password
859 | _, err := login(db, username, pwd)
860 | if err != nil {
861 | return err
862 | }
863 |
864 | // Delete user
865 | s := "DELETE FROM user WHERE username = ?"
866 | _, err = sqlexec(db, s, username)
867 | if err != nil {
868 | return fmt.Errorf("DB error deleting user: %s", err)
869 | }
870 | return nil
871 | }
872 | func deluserHandler(db *sql.DB) http.HandlerFunc {
873 | type DelUserReq struct {
874 | Username string `json:"username"`
875 | Pwd string `json:"pwd"`
876 | }
877 | return func(w http.ResponseWriter, r *http.Request) {
878 | if r.Method != "POST" {
879 | http.Error(w, "Use POST method", 401)
880 | return
881 | }
882 |
883 | bs, err := ioutil.ReadAll(r.Body)
884 | if err != nil {
885 | handleErr(w, err, "deluserHandler")
886 | return
887 | }
888 | var req DelUserReq
889 | err = json.Unmarshal(bs, &req)
890 | if err != nil {
891 | handleErr(w, err, "deluserHandler")
892 | return
893 | }
894 | if req.Username == "" {
895 | http.Error(w, "username required", 401)
896 | return
897 | }
898 |
899 | // Attempt to delete user.
900 | var result LoginResult
901 | err = deluser(db, req.Username, req.Pwd)
902 | if err != nil {
903 | result.Error = fmt.Sprintf("%s", err)
904 | }
905 |
906 | w.Header().Set("Content-Type", "application/json")
907 | P := makeFprintf(w)
908 | bs, _ = json.MarshalIndent(result, "", "\t")
909 | P("%s\n", string(bs))
910 | }
911 | }
912 |
913 | func saveGrid(db *sql.DB, userid int64, gridjson string) error {
914 | s := "INSERT OR REPLACE INTO savedgrid (user_id, gridjson) VALUES (?, ?);"
915 | _, err := sqlexec(db, s, userid, gridjson)
916 | if err != nil {
917 | return fmt.Errorf("DB error saving grid: %s", err)
918 | }
919 | return nil
920 | }
921 | func savegridHandler(db *sql.DB) http.HandlerFunc {
922 | return func(w http.ResponseWriter, r *http.Request) {
923 | if r.Method != "POST" {
924 | http.Error(w, "Use POST method", 401)
925 | return
926 | }
927 |
928 | q := r.URL.Query()
929 | username := q.Get("username")
930 | tok := q.Get("tok")
931 | if username == "" {
932 | http.Error(w, "username required", 401)
933 | return
934 | }
935 | if tok == "" {
936 | http.Error(w, "tok required", 401)
937 | return
938 | }
939 | bs, err := ioutil.ReadAll(r.Body)
940 | if err != nil {
941 | handleErr(w, err, "saveGrid")
942 | return
943 | }
944 | gridjson := string(bs)
945 |
946 | u := findUser(db, username)
947 | if u == nil {
948 | http.Error(w, fmt.Sprintf("No user '%s'", username), 401)
949 | return
950 | }
951 | if !validateTok(tok, u) {
952 | http.Error(w, fmt.Sprintf("Token not validated for '%s' ", u.Username), 401)
953 | return
954 | }
955 | err = saveGrid(db, u.Userid, gridjson)
956 | if err != nil {
957 | handleErr(w, err, "saveGrid")
958 | return
959 | }
960 | }
961 | }
962 |
963 | func loadGrid(db *sql.DB, userid int64) string {
964 | s := "SELECT gridjson FROM savedgrid WHERE user_id = ?"
965 | row := db.QueryRow(s, userid)
966 | var gridjson string
967 | err := row.Scan(&gridjson)
968 | if err == sql.ErrNoRows {
969 | return ""
970 | }
971 | if err != nil {
972 | fmt.Printf("loadGrid() err: %s\n", err)
973 | return ""
974 | }
975 | return gridjson
976 | }
977 | func loadgridHandler(db *sql.DB) http.HandlerFunc {
978 | return func(w http.ResponseWriter, r *http.Request) {
979 | username := r.FormValue("username")
980 | tok := r.FormValue("tok")
981 | if username == "" {
982 | http.Error(w, "username required", 401)
983 | return
984 | }
985 | if tok == "" {
986 | http.Error(w, "tok required", 401)
987 | return
988 | }
989 |
990 | u := findUser(db, username)
991 | if u == nil {
992 | http.Error(w, fmt.Sprintf("No user '%s'", username), 401)
993 | return
994 | }
995 | if !validateTok(tok, u) {
996 | http.Error(w, fmt.Sprintf("Token not validated for '%s' ", u.Username), 401)
997 | return
998 | }
999 | gridjson := loadGrid(db, u.Userid)
1000 | if gridjson == "" {
1001 | gridjson = "[]"
1002 | }
1003 |
1004 | w.Header().Set("Content-Type", "application/json")
1005 | P := makeFprintf(w)
1006 | P("%s\n", gridjson)
1007 | }
1008 | }
1009 |
1010 | func testfeedHandler(db *sql.DB) http.HandlerFunc {
1011 | return func(w http.ResponseWriter, r *http.Request) {
1012 | // Allow requests from all sites.
1013 | w.Header().Set("Access-Control-Allow-Origin", "*")
1014 | w.Header().Set("Content-Type", "application/rss+xml")
1015 | P := makeFprintf(w)
1016 |
1017 | now := time.Now()
1018 | f := feeds.Feed{
1019 | Title: "FreeRSS test rss feed",
1020 | Link: &feeds.Link{Href: "http://freerss.robdelacruz.com/api/testfeed"},
1021 | Description: "Test RSS feed with inline script",
1022 | Author: &feeds.Author{},
1023 | Created: now,
1024 | }
1025 | item := feeds.Item{
1026 | Title: "test item with inline script",
1027 | Link: &feeds.Link{Href: "http://freerss.robdelacruz.com/api/testfeed"},
1028 | Description: `This is markup containing an inline script.
1029 | click me
1030 |
1031 | `,
1032 | Author: &feeds.Author{},
1033 | Created: now,
1034 | }
1035 | f.Items = []*feeds.Item{
1036 | &item,
1037 | }
1038 | rss, err := f.ToRss()
1039 | if err != nil {
1040 | handleErr(w, err, "testfeedHandler")
1041 | return
1042 | }
1043 | P("%s\n", rss)
1044 | }
1045 | }
1046 |
--------------------------------------------------------------------------------