diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..8708dce --- /dev/null +++ b/.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 diff --git a/Dockerfile.single-platform b/Dockerfile.single-platform index 1930f99..5309059 100644 --- a/Dockerfile.single-platform +++ b/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"] diff --git a/README.md b/README.md index afff5a0..a9de87b 100644 --- a/README.md +++ b/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.** diff --git a/docs/configuration.md b/docs/configuration.md index 6caedd7..90d9d7d 100644 --- a/docs/configuration.md +++ b/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 +``` + + + +#### 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` diff --git a/internal/feed/lobsters.go b/internal/feed/lobsters.go new file mode 100644 index 0000000..e103f56 --- /dev/null +++ b/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 +} diff --git a/internal/feed/primitives.go b/internal/feed/primitives.go index 7982360..33c3247 100644 --- a/internal/feed/primitives.go +++ b/internal/feed/primitives.go @@ -16,6 +16,7 @@ type ForumPost struct { Score int Engagement float64 TimePosted time.Time + Tags []string } type ForumPosts []ForumPost diff --git a/internal/widget/lobsters.go b/internal/widget/lobsters.go new file mode 100644 index 0000000..d9a8c43 --- /dev/null +++ b/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) +} diff --git a/internal/widget/widget.go b/internal/widget/widget.go index 8af7964..5465b63 100644 --- a/internal/widget/widget.go +++ b/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":