aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-07-11 00:43:57 +0900
committernsfisis <nsfisis@gmail.com>2025-07-11 00:43:57 +0900
commit5f69e3e972831ca471bc971a034f0a3f5f22b5c5 (patch)
tree432cff44241566a4efac03846d45b5051d1b6c70
parent5048060b6f002e2ea8bdec564a708dd21b7d665f (diff)
downloadfeedaka-5f69e3e972831ca471bc971a034f0a3f5f22b5c5.tar.gz
feedaka-5f69e3e972831ca471bc971a034f0a3f5f22b5c5.tar.zst
feedaka-5f69e3e972831ca471bc971a034f0a3f5f22b5c5.zip
feat(backend): remove REST API endpoints
-rw-r--r--backend/main.go310
-rw-r--r--backend/templates/read-feeds.html35
-rw-r--r--backend/templates/settings.html22
-rw-r--r--backend/templates/unread-feeds.html35
4 files changed, 1 insertions, 401 deletions
diff --git a/backend/main.go b/backend/main.go
index d01a0ed..90f95e3 100644
--- a/backend/main.go
+++ b/backend/main.go
@@ -5,14 +5,10 @@ import (
"database/sql"
"embed"
"fmt"
- "html/template"
- "io"
"log"
"net/http"
"os"
"os/signal"
- "strconv"
- "strings"
"syscall"
"time"
@@ -26,28 +22,16 @@ import (
_ "github.com/mattn/go-sqlite3"
"github.com/mmcdole/gofeed"
"github.com/vektah/gqlparser/v2/ast"
- "golang.org/x/exp/slices"
"undef.ninja/x/feedaka/graphql"
)
var (
- basePath string
- db *sql.DB
- //go:embed templates/*
- tmplFS embed.FS
+ db *sql.DB
//go:embed static/*
staticFS embed.FS
)
-type Template struct {
- templates *template.Template
-}
-
-func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
- return t.templates.ExecuteTemplate(w, name, data)
-}
-
func initDB(db *sql.DB) error {
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS feeds (
@@ -69,102 +53,6 @@ CREATE TABLE IF NOT EXISTS articles (
return err
}
-func getIndex(c echo.Context) error {
- // Redirect to /feeds/unread
- return c.Redirect(http.StatusFound, basePath+"/feeds/unread")
-}
-
-func getSettings(c echo.Context) error {
- feedURLs := []string{}
- rows, err := db.Query(`SELECT url FROM feeds`)
- if err != nil {
- return c.String(http.StatusInternalServerError, err.Error())
- }
- defer rows.Close()
- for rows.Next() {
- var url string
- err := rows.Scan(&url)
- if err != nil {
- return c.String(http.StatusInternalServerError, err.Error())
- }
- feedURLs = append(feedURLs, url)
- }
- // Sort feedURLs in ascending order.
- slices.Sort(feedURLs)
-
- return c.Render(http.StatusOK, "settings.html", struct {
- BasePath string
- URLs string
- }{
- BasePath: basePath,
- URLs: strings.Join(feedURLs, "\r\n"),
- })
-}
-
-func postSettings(c echo.Context) error {
- // Get "urls" from form parameters.
- rawUrls := strings.Split(c.FormValue("urls"), "\r\n")
- urls := make([]string, 0, len(rawUrls))
- for _, rawUrl := range rawUrls {
- url := strings.TrimSpace(rawUrl)
- if url != "" {
- urls = append(urls, url)
- }
- }
- existingURLs := make(map[string]bool)
- rows, err := db.Query(`SELECT url FROM feeds`)
- if err != nil {
- return c.String(http.StatusInternalServerError, err.Error())
- }
- defer rows.Close()
- for rows.Next() {
- var url string
- err := rows.Scan(&url)
- if err != nil {
- return c.String(http.StatusInternalServerError, err.Error())
- }
- existingURLs[url] = true
- }
- for _, url := range urls {
- if existingURLs[url] {
- continue
- }
- _, err := db.Exec(
- `INSERT INTO feeds (url, title, fetched_at) VALUES (?, ?, ?)`,
- url,
- "",
- time.Now().AddDate(0, 0, -1).UTC().Format(time.RFC3339),
- )
- if err != nil {
- return c.String(http.StatusInternalServerError, err.Error())
- }
- }
- // Remove:
- for existingURL := range existingURLs {
- // If existingURL is not in urls, it will be removed.
- found := false
- for _, url := range urls {
- if existingURL == url {
- found = true
- break
- }
- }
- if found {
- continue
- }
- // Remove feed and articles.
- _, err = db.Exec(`DELETE FROM articles WHERE feed_id = (SELECT id FROM feeds WHERE url = ?)`, existingURL)
- if err != nil {
- return c.String(http.StatusInternalServerError, err.Error())
- }
- _, err := db.Exec(`DELETE FROM feeds WHERE url = ?`, existingURL)
- if err != nil {
- return c.String(http.StatusInternalServerError, err.Error())
- }
- }
- return c.Redirect(http.StatusSeeOther, basePath+"/settings")
-}
-
func fetchOneFeed(feedID int, url string, ctx context.Context) error {
log.Printf("Fetching %s...\n", url)
fp := gofeed.NewParser()
@@ -272,184 +160,6 @@ func fetchAllFeeds(ctx context.Context) error {
return result.ErrorOrNil()
}
-func getUnreadFeeds(c echo.Context) error {
- rows, err := db.Query(`
-SELECT a.id, a.title, f.url, f.title, f.id
-FROM articles AS a
-INNER JOIN feeds AS f ON a.feed_id = f.id
-WHERE is_read = 0
-ORDER BY a.id DESC
-LIMIT 100
-`)
- if err != nil {
- return c.String(http.StatusInternalServerError, err.Error())
- }
- defer rows.Close()
-
- return renderFeeds(c, "Unread feeds", "unread-feeds.html", rows)
-}
-
-func getReadFeeds(c echo.Context) error {
- rows, err := db.Query(`
-SELECT a.id, a.title, f.url, f.title, f.id
-FROM articles AS a
-INNER JOIN feeds AS f ON a.feed_id = f.id
-WHERE is_read = 1
-ORDER BY a.id DESC
-LIMIT 100
-`)
- if err != nil {
- return c.String(http.StatusInternalServerError, err.Error())
- }
- defer rows.Close()
-
- return renderFeeds(c, "Read feeds", "read-feeds.html", rows)
-}
-
-func renderFeeds(c echo.Context, title string, templateName string, rows *sql.Rows) error {
- type Article struct {
- ID int
- Title string
- URL string
- }
- type Feed struct {
- ID int
- URL string
- Title string
- Articles []Article
- }
- feeds := make(map[int]*Feed)
- for rows.Next() {
- var articleID int
- var articleTitle string
- var feedURL string
- var feedTitle string
- var feedID int
- err := rows.Scan(&articleID, &articleTitle, &feedURL, &feedTitle, &feedID)
- if err != nil {
- return c.String(http.StatusInternalServerError, err.Error())
- }
- if _, ok := feeds[feedID]; !ok {
- feeds[feedID] = &Feed{
- ID: feedID,
- URL: feedURL,
- Title: feedTitle,
- }
- }
- feed := feeds[feedID]
- feed.Articles = append(feed.Articles, Article{
- ID: articleID,
- Title: articleTitle,
- URL: fmt.Sprintf("%s/articles/%d", basePath, articleID),
- })
- }
-
- sortedFeeds := make([]*Feed, 0, len(feeds))
- for _, feed := range feeds {
- sortedFeeds = append(sortedFeeds, feed)
- }
- slices.SortFunc(sortedFeeds, func(a, b *Feed) int {
- // Ascending order by URL.
- if a.URL < b.URL {
- return -1
- } else if a.URL > b.URL {
- return 1
- } else {
- return 0
- }
- })
-
- return c.Render(http.StatusOK, templateName, struct {
- BasePath string
- Title string
- Feeds []*Feed
- }{
- BasePath: basePath,
- Title: title,
- Feeds: sortedFeeds,
- })
-}
-
-func getArticle(c echo.Context) error {
- rawArticleID := c.Param("articleID")
- articleID, err := strconv.Atoi(rawArticleID)
- if err != nil {
- return c.String(http.StatusNotFound, err.Error())
- }
- row := db.QueryRow(`SELECT url FROM articles WHERE id = ?`, articleID)
- if row == nil {
- return c.String(http.StatusNotFound, "Not found")
- }
- var url string
- err = row.Scan(&url)
- if err != nil {
- return c.String(http.StatusInternalServerError, err.Error())
- }
- // Turn is_read on.
- _, err = db.Exec(`UPDATE articles SET is_read = 1 WHERE id = ?`, articleID)
- if err != nil {
- return c.String(http.StatusInternalServerError, err.Error())
- }
- // Redirect to the article URL.
- return c.Redirect(http.StatusFound, url)
-}
-
-func apiPutFeedRead(c echo.Context) error {
- return apiPutFeed(c, "read")
-}
-
-func apiPutFeedUnread(c echo.Context) error {
- return apiPutFeed(c, "unread")
-}
-
-func apiPutFeed(c echo.Context, op string) error {
- rawFeedID := c.Param("feedID")
- feedID, err := strconv.Atoi(rawFeedID)
- if err != nil {
- return c.String(http.StatusNotFound, err.Error())
- }
- // Turn is_read on or off.
- var isReadValue int
- if op == "read" {
- isReadValue = 1
- } else if op == "unread" {
- isReadValue = 0
- }
- _, err = db.Exec(`UPDATE articles SET is_read = ? WHERE feed_id = ?`, isReadValue, feedID)
- if err != nil {
- return c.String(http.StatusInternalServerError, err.Error())
- }
- return c.NoContent(http.StatusOK)
-}
-
-func apiPutArticleRead(c echo.Context) error {
- return apiPutArticle(c, "read")
-}
-
-func apiPutArticleUnread(c echo.Context) error {
- return apiPutArticle(c, "unread")
-}
-
-func apiPutArticle(c echo.Context, op string) error {
- rawArticleID := c.Param("articleID")
- articleID, err := strconv.Atoi(rawArticleID)
- if err != nil {
- return c.String(http.StatusNotFound, err.Error())
- }
- // Turn is_read on or off.
- var isReadValue int
- if op == "read" {
- isReadValue = 1
- } else if op == "unread" {
- isReadValue = 0
- }
- _, err = db.Exec(`UPDATE articles SET is_read = ? WHERE id = ?`, isReadValue, articleID)
- if err != nil {
- return c.String(http.StatusInternalServerError, err.Error())
- }
- return c.NoContent(http.StatusOK)
-}
-
func scheduled(ctx context.Context, d time.Duration, fn func()) {
ticker := time.NewTicker(d)
go func() {
@@ -465,7 +175,6 @@ func scheduled(ctx context.Context, d time.Duration, fn func()) {
}
func main() {
- basePath = os.Getenv("FEEDAKA_BASE_PATH")
port := os.Getenv("FEEDAKA_PORT")
var err error
@@ -480,29 +189,12 @@ func main() {
log.Fatal(err)
}
- t := &Template{
- templates: template.Must(template.ParseFS(tmplFS, "templates/*.html")),
- }
-
e := echo.New()
- e.Renderer = t
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
- e.GET("/", getIndex)
- e.GET("/settings", getSettings)
- e.POST("/settings", postSettings)
- e.GET("/feeds/unread", getUnreadFeeds)
- e.GET("/feeds/read", getReadFeeds)
- e.GET("/articles/:articleID", getArticle)
-
- e.PUT("/api/feeds/:feedID/read", apiPutFeedRead)
- e.PUT("/api/feeds/:feedID/unread", apiPutFeedUnread)
- e.PUT("/api/articles/:articleID/read", apiPutArticleRead)
- e.PUT("/api/articles/:articleID/unread", apiPutArticleUnread)
-
e.GET("/static/*", echo.WrapHandler(http.FileServer(http.FS(staticFS))))
// Setup GraphQL server
diff --git a/backend/templates/read-feeds.html b/backend/templates/read-feeds.html
deleted file mode 100644
index 08a2626..0000000
--- a/backend/templates/read-feeds.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <link rel="stylesheet" href="{{.BasePath}}/static/style.css">
- <link rel="icon" href="{{.BasePath}}/static/favicon.svg">
- <title>{{.Title}} | feedaka</title>
- </head>
- <body class="bg-gray-100 text-gray-900 font-sans antialiased">
- <div class="mx-auto mt-10 px-6">
- <h1 class="text-4xl font-bold underline mb-8">{{.Title}}</h1>
- <main>
- <ul class="list-none pl-5">
- {{- range .Feeds -}}
- <li class="mb-5">
- <a href="{{.URL}}" target="_blank" rel="noreferrer" class="text-blue-500 hover:text-blue-600 hover:underline">{{.Title}}</a>
- <button data-feed-id="{{.ID}}" class="js-unread-feed ml-4 py-1 px-0 w-6 h-6 rounded border-2 border-green-500 bg-green-500 hover:bg-transparent focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50 transition duration-200"></button>
- </li>
- <ul class="list-none pl-5 mt-4">
- {{- range .Articles -}}
- <li class="mb-3">
- <a href="{{.URL}}" target="_blank" rel="noreferrer" class="text-blue-500 hover:text-blue-600 hover:underline">{{.Title}}</a>
- <button data-article-id="{{.ID}}" class="js-unread-article ml-4 py-1 px-0 w-6 h-6 rounded border-2 border-green-500 bg-green-500 hover:bg-transparent focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50 transition duration-200"></button>
- </li>
- {{- end -}}
- </ul>
- {{- end -}}
- </ul>
- </main>
- </div>
- <script>window.BASE_PATH = "{{.BasePath}}";</script>
- <script src="{{.BasePath}}/static/index.js" type="module" defer></script>
- </body>
-</html>
diff --git a/backend/templates/settings.html b/backend/templates/settings.html
deleted file mode 100644
index 65efd8d..0000000
--- a/backend/templates/settings.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<!DOCTYPE html>
-<html>
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <link rel="stylesheet" href="{{.BasePath}}/static/style.css">
- <link rel="icon" href="{{.BasePath}}/static/favicon.svg">
- <title>Settings | feedaka</title>
- </head>
- <body class="bg-gray-100 text-gray-900 font-sans antialiased">
- <div class="mx-auto mt-10 px-6">
- <h1 class="text-4xl font-bold underline mb-8">Settings</h1>
- <main>
- <h2 class="text-2xl mb-3">URLs</h2>
- <form method="POST" class="space-y-4">
- <textarea name="urls" rows="10" cols="80" class="w-full p-2 border rounded resize-y">{{.URLs}}</textarea>
- <input type="submit" value="Submit" class="py-2 px-4 bg-blue-600 text-white rounded hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50" />
- </form>
- </main>
- </div>
- </body>
-</html>
diff --git a/backend/templates/unread-feeds.html b/backend/templates/unread-feeds.html
deleted file mode 100644
index 676f24e..0000000
--- a/backend/templates/unread-feeds.html
+++ /dev/null
@@ -1,35 +0,0 @@
-<!DOCTYPE html>
-<html>
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <link rel="stylesheet" href="{{.BasePath}}/static/style.css">
- <link rel="icon" href="{{.BasePath}}/static/favicon.svg">
- <title>{{.Title}} | feedaka</title>
- </head>
- <body class="bg-gray-100 text-gray-900 font-sans antialiased">
- <div class="mx-auto mt-10 px-6">
- <h1 class="text-4xl font-bold underline mb-8">{{.Title}}</h1>
- <main>
- <ul class="list-none pl-5">
- {{- range .Feeds -}}
- <li class="mb-5">
- <a href="{{.URL}}" target="_blank" rel="noreferrer" class="text-blue-500 hover:text-blue-600 hover:underline">{{.Title}}</a>
- <button data-feed-id="{{.ID}}" class="js-read-feed ml-4 py-1 px-0 w-6 h-6 rounded border-2 border-green-500 hover:bg-green-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50 transition duration-200"></button>
- </li>
- <ul class="list-none pl-5 mt-4">
- {{- range .Articles -}}
- <li class="mb-3">
- <a href="{{.URL}}" target="_blank" rel="noreferrer" class="text-blue-500 hover:text-blue-600 hover:underline">{{.Title}}</a>
- <button data-article-id="{{.ID}}" class="js-read-article ml-4 py-1 px-0 w-6 h-6 rounded border-2 border-green-500 hover:bg-green-500 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-opacity-50 transition duration-200"></button>
- </li>
- {{- end -}}
- </ul>
- {{- end -}}
- </ul>
- </main>
- </div>
- <script>window.BASE_PATH = "{{.BasePath}}";</script>
- <script src="{{.BasePath}}/static/index.js" type="module" defer></script>
- </body>
-</html>