Selaa lähdekoodia

Merge pull request #56 from jonasknobloch/lobsters-widget

Add Lobsters widget
Svilen Markov 1 vuosi sitten
vanhempi
commit
8dc34ddfa7

+ 10 - 0
.dockerignore

@@ -0,0 +1,10 @@
+# https://docs.docker.com/build/building/context/#dockerignore-files
+# Ignore all files by default
+*
+
+# Only add necessary files to the Docker build context (Dockerfiles are always included implicitly)
+!/build/
+!/internal/
+!/go.mod
+!/go.sum
+!main.go

+ 8 - 1
Dockerfile.single-platform

@@ -1,7 +1,14 @@
+FROM golang:1.22.3-alpine3.19 AS builder
+
+WORKDIR /app
+COPY . /app
+RUN CGO_ENABLED=0 go build .
+
+
 FROM alpine:3.19
 
 WORKDIR /app
-COPY build/glance /app/glance
+COPY --from=builder /app/glance .
 
 EXPOSE 8080/tcp
 ENTRYPOINT ["/app/glance"]

+ 0 - 6
README.md

@@ -94,12 +94,6 @@ go run .
 
 ### Building Docker image
 
-Build Glance with CGO disabled:
-
-```bash
-CGO_ENABLED=0 go build -o build/glance .
-```
-
 Build the image:
 
 **Make sure to replace "owner" with your name or organization.**

+ 45 - 1
docs/configuration.md

@@ -10,6 +10,7 @@
   - [RSS](#rss)
   - [Videos](#videos)
   - [Hacker News](#hacker-news)
+  - [Lobsters](#lobsters)
   - [Reddit](#reddit)
   - [Search](#search-widget)
   - [Weather](#weather)
@@ -533,6 +534,49 @@ Can be used to specify an additional sort which will be applied on top of the al
 
 The `engagement` sort tries to place the posts with the most points and comments on top, also prioritizing recent over old posts.
 
+### Lobsters
+Display a list of posts from [Lobsters](https://lobste.rs).
+
+Example:
+
+```yaml
+- type: lobsters
+  sort-by: hot
+  tags:
+    - go
+    - security
+    - linux
+  limit: 15
+  collapse-after: 5
+```
+
+<!--
+TODO: add preview
+
+Preview:
+![](images/lobsters-widget-preview.png)
+ -->
+
+#### Properties
+| Name | Type | Required | Default |
+| ---- | ---- | -------- | ------- |
+| limit | integer | no | 15 |
+| collapse-after | integer | no | 5 |
+| sort-by | string | no | hot |
+| tags | array | no | |
+
+##### `limit`
+The maximum number of posts to show.
+
+##### `collapse-after`
+How many posts are visible before the "SHOW MORE" button appears. Set to `-1` to never collapse.
+
+##### `sort-by`
+The sort order in which posts are returned. Possible options are `hot` and `new`.
+
+##### `tags`
+Limit to posts containing one of the given tags. **You cannot specify a sort order when filtering by tags, it will default to `hot`.**
+
 ### Reddit
 Display a list of posts from a specific subreddit.
 
@@ -629,7 +673,7 @@ https://your.proxy/?url={REQUEST-URL}
 ##### `sort-by`
 Can be used to specify the order in which the posts should get returned. Possible values are `hot`, `new`, `top` and `rising`.
 
-##### `top-perid`
+##### `top-period`
 Available only when `sort-by` is set to `top`. Possible values are `hour`, `day`, `week`, `month`, `year` and `all`.
 
 ##### `search`

+ 81 - 0
internal/feed/lobsters.go

@@ -0,0 +1,81 @@
+package feed
+
+import (
+	"net/http"
+	"strings"
+	"time"
+)
+
+type lobstersPostResponseJson struct {
+	CreatedAt    string   `json:"created_at"`
+	Title        string   `json:"title"`
+	URL          string   `json:"url"`
+	Score        int      `json:"score"`
+	CommentCount int      `json:"comment_count"`
+	CommentsURL  string   `json:"comments_url"`
+	Tags         []string `json:"tags"`
+}
+
+type lobstersFeedResponseJson []lobstersPostResponseJson
+
+func getLobstersPostsFromFeed(feedUrl string) (ForumPosts, error) {
+	request, err := http.NewRequest("GET", feedUrl, nil)
+
+	if err != nil {
+		return nil, err
+	}
+
+	feed, err := decodeJsonFromRequest[lobstersFeedResponseJson](defaultClient, request)
+
+	if err != nil {
+		return nil, err
+	}
+
+	posts := make(ForumPosts, 0, len(feed))
+
+	for i := range feed {
+		createdAt, _ := time.Parse(time.RFC3339, feed[i].CreatedAt)
+
+		posts = append(posts, ForumPost{
+			Title:           feed[i].Title,
+			DiscussionUrl:   feed[i].CommentsURL,
+			TargetUrl:       feed[i].URL,
+			TargetUrlDomain: extractDomainFromUrl(feed[i].URL),
+			CommentCount:    feed[i].CommentCount,
+			Score:           feed[i].Score,
+			TimePosted:      createdAt,
+			Tags:            feed[i].Tags,
+		})
+	}
+
+	if len(posts) == 0 {
+		return nil, ErrNoContent
+	}
+
+	return posts, nil
+}
+
+func FetchLobstersPosts(sortBy string, tags []string) (ForumPosts, error) {
+	var feedUrl string
+
+	if sortBy == "hot" {
+		sortBy = "hottest"
+	} else if sortBy == "new" {
+		sortBy = "newest"
+	}
+
+	if len(tags) == 0 {
+		feedUrl = "https://lobste.rs/" + sortBy + ".json"
+	} else {
+		tags := strings.Join(tags, ",")
+		feedUrl = "https://lobste.rs/t/" + tags + ".json"
+	}
+
+	posts, err := getLobstersPostsFromFeed(feedUrl)
+
+	if err != nil {
+		return nil, err
+	}
+
+	return posts, nil
+}

+ 1 - 0
internal/feed/primitives.go

@@ -16,6 +16,7 @@ type ForumPost struct {
 	Score           int
 	Engagement      float64
 	TimePosted      time.Time
+	Tags            []string
 }
 
 type ForumPosts []ForumPost

+ 56 - 0
internal/widget/lobsters.go

@@ -0,0 +1,56 @@
+package widget
+
+import (
+	"context"
+	"html/template"
+	"time"
+
+	"github.com/glanceapp/glance/internal/assets"
+	"github.com/glanceapp/glance/internal/feed"
+)
+
+type Lobsters struct {
+	widgetBase     `yaml:",inline"`
+	Posts          feed.ForumPosts `yaml:"-"`
+	Limit          int             `yaml:"limit"`
+	CollapseAfter  int             `yaml:"collapse-after"`
+	SortBy         string          `yaml:"sort-by"`
+	Tags           []string        `yaml:"tags"`
+	ShowThumbnails bool            `yaml:"-"`
+}
+
+func (widget *Lobsters) Initialize() error {
+	widget.withTitle("Lobsters").withCacheDuration(30 * time.Minute)
+
+	if widget.SortBy == "" || (widget.SortBy != "hot" && widget.SortBy != "new") {
+		widget.SortBy = "hot"
+	}
+
+	if widget.Limit <= 0 {
+		widget.Limit = 15
+	}
+
+	if widget.CollapseAfter == 0 || widget.CollapseAfter < -1 {
+		widget.CollapseAfter = 5
+	}
+
+	return nil
+}
+
+func (widget *Lobsters) Update(ctx context.Context) {
+	posts, err := feed.FetchLobstersPosts(widget.SortBy, widget.Tags)
+
+	if !widget.canContinueUpdateAfterHandlingErr(err) {
+		return
+	}
+
+	if widget.Limit < len(posts) {
+		posts = posts[:widget.Limit]
+	}
+
+	widget.Posts = posts
+}
+
+func (widget *Lobsters) Render() template.HTML {
+	return widget.render(widget, assets.ForumPostsTemplate)
+}

+ 2 - 0
internal/widget/widget.go

@@ -45,6 +45,8 @@ func New(widgetType string) (Widget, error) {
 		return &TwitchGames{}, nil
 	case "twitch-channels":
 		return &TwitchChannels{}, nil
+	case "lobsters":
+		return &Lobsters{}, nil
 	case "change-detection":
 		return &ChangeDetection{}, nil
 	case "repository":