mirror of
https://github.com/miniflux/v2.git
synced 2025-08-01 17:38:37 +00:00
Move internal packages to an internal folder
For reference: https://go.dev/doc/go1.4#internalpackages
This commit is contained in:
parent
c234903255
commit
168a870c02
433 changed files with 1121 additions and 1123 deletions
32
internal/model/api_key.go
Normal file
32
internal/model/api_key.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/crypto"
|
||||
)
|
||||
|
||||
// APIKey represents an application API key.
|
||||
type APIKey struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
Token string
|
||||
Description string
|
||||
LastUsedAt *time.Time
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// NewAPIKey initializes a new APIKey.
|
||||
func NewAPIKey(userID int64, description string) *APIKey {
|
||||
return &APIKey{
|
||||
UserID: userID,
|
||||
Token: crypto.GenerateRandomString(32),
|
||||
Description: description,
|
||||
}
|
||||
}
|
||||
|
||||
// APIKeys represents a collection of API Key.
|
||||
type APIKeys []*APIKey
|
58
internal/model/app_session.go
Normal file
58
internal/model/app_session.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// SessionData represents the data attached to the session.
|
||||
type SessionData struct {
|
||||
CSRF string `json:"csrf"`
|
||||
OAuth2State string `json:"oauth2_state"`
|
||||
FlashMessage string `json:"flash_message"`
|
||||
FlashErrorMessage string `json:"flash_error_message"`
|
||||
Language string `json:"language"`
|
||||
Theme string `json:"theme"`
|
||||
PocketRequestToken string `json:"pocket_request_token"`
|
||||
}
|
||||
|
||||
func (s SessionData) String() string {
|
||||
return fmt.Sprintf(`CSRF=%q, OAuth2State=%q, FlashMsg=%q, FlashErrMsg=%q, Lang=%q, Theme=%q, PocketTkn=%q`,
|
||||
s.CSRF, s.OAuth2State, s.FlashMessage, s.FlashErrorMessage, s.Language, s.Theme, s.PocketRequestToken)
|
||||
}
|
||||
|
||||
// Value converts the session data to JSON.
|
||||
func (s SessionData) Value() (driver.Value, error) {
|
||||
j, err := json.Marshal(s)
|
||||
return j, err
|
||||
}
|
||||
|
||||
// Scan converts raw JSON data.
|
||||
func (s *SessionData) Scan(src interface{}) error {
|
||||
source, ok := src.([]byte)
|
||||
if !ok {
|
||||
return errors.New("session: unable to assert type of src")
|
||||
}
|
||||
|
||||
err := json.Unmarshal(source, s)
|
||||
if err != nil {
|
||||
return fmt.Errorf("session: %v", err)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Session represents a session in the system.
|
||||
type Session struct {
|
||||
ID string
|
||||
Data *SessionData
|
||||
}
|
||||
|
||||
func (s *Session) String() string {
|
||||
return fmt.Sprintf(`ID="%s", Data={%v}`, s.ID, s.Data)
|
||||
}
|
11
internal/model/categories_sort_options.go
Normal file
11
internal/model/categories_sort_options.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
|
||||
func CategoriesSortingOptions() map[string]string {
|
||||
return map[string]string{
|
||||
"unread_count": "form.prefs.select.unread_count",
|
||||
"alphabetical": "form.prefs.select.alphabetical",
|
||||
}
|
||||
}
|
35
internal/model/category.go
Normal file
35
internal/model/category.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Category represents a feed category.
|
||||
type Category struct {
|
||||
ID int64 `json:"id"`
|
||||
Title string `json:"title"`
|
||||
UserID int64 `json:"user_id"`
|
||||
HideGlobally bool `json:"hide_globally"`
|
||||
FeedCount *int `json:"feed_count,omitempty"`
|
||||
TotalUnread *int `json:"total_unread,omitempty"`
|
||||
}
|
||||
|
||||
func (c *Category) String() string {
|
||||
return fmt.Sprintf("ID=%d, UserID=%d, Title=%s", c.ID, c.UserID, c.Title)
|
||||
}
|
||||
|
||||
// CategoryRequest represents the request to create or update a category.
|
||||
type CategoryRequest struct {
|
||||
Title string `json:"title"`
|
||||
HideGlobally string `json:"hide_globally"`
|
||||
}
|
||||
|
||||
// Patch updates category fields.
|
||||
func (cr *CategoryRequest) Patch(category *Category) {
|
||||
category.Title = cr.Title
|
||||
category.HideGlobally = cr.HideGlobally != ""
|
||||
}
|
||||
|
||||
// Categories represents a list of categories.
|
||||
type Categories []*Category
|
34
internal/model/enclosure.go
Normal file
34
internal/model/enclosure.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
import "strings"
|
||||
|
||||
// Enclosure represents an attachment.
|
||||
type Enclosure struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
EntryID int64 `json:"entry_id"`
|
||||
URL string `json:"url"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Size int64 `json:"size"`
|
||||
MediaProgression int64 `json:"media_progression"`
|
||||
}
|
||||
|
||||
// Html5MimeType will modify the actual MimeType to allow direct playback from HTML5 player for some kind of MimeType
|
||||
func (e Enclosure) Html5MimeType() string {
|
||||
if strings.HasPrefix(e.MimeType, "video") {
|
||||
switch e.MimeType {
|
||||
// Solution from this stackoverflow discussion:
|
||||
// https://stackoverflow.com/questions/15277147/m4v-mimetype-video-mp4-or-video-m4v/66945470#66945470
|
||||
// tested at the time of this commit (06/2023) on latest Firefox & Vivaldi on this feed
|
||||
// https://www.florenceporcel.com/podcast/lfhdu.xml
|
||||
case "video/m4v":
|
||||
return "video/x-m4v"
|
||||
}
|
||||
}
|
||||
return e.MimeType
|
||||
}
|
||||
|
||||
// EnclosureList represents a list of attachments.
|
||||
type EnclosureList []*Enclosure
|
33
internal/model/enclosure_test.go
Normal file
33
internal/model/enclosure_test.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEnclosure_Html5MimeTypeGivesOriginalMimeType(t *testing.T) {
|
||||
enclosure := Enclosure{MimeType: "thing/thisMimeTypeIsNotExpectedToBeReplaced"}
|
||||
if enclosure.Html5MimeType() != enclosure.MimeType {
|
||||
t.Fatalf(
|
||||
"HTML5 MimeType must provide original MimeType if not explicitly Replaced. Got %s ,expected '%s' ",
|
||||
enclosure.Html5MimeType(),
|
||||
enclosure.MimeType,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnclosure_Html5MimeTypeReplaceStandardM4vByAppleSpecificMimeType(t *testing.T) {
|
||||
enclosure := Enclosure{MimeType: "video/m4v"}
|
||||
if enclosure.Html5MimeType() != "video/x-m4v" {
|
||||
// Solution from this stackoverflow discussion:
|
||||
// https://stackoverflow.com/questions/15277147/m4v-mimetype-video-mp4-or-video-m4v/66945470#66945470
|
||||
// tested at the time of this commit (06/2023) on latest Firefox & Vivaldi on this feed
|
||||
// https://www.florenceporcel.com/podcast/lfhdu.xml
|
||||
t.Fatalf(
|
||||
"HTML5 MimeType must be replaced by 'video/x-m4v' when originally video/m4v to ensure playbacks in brownser. Got '%s'",
|
||||
enclosure.Html5MimeType(),
|
||||
)
|
||||
}
|
||||
}
|
49
internal/model/entry.go
Normal file
49
internal/model/entry.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Entry statuses and default sorting order.
|
||||
const (
|
||||
EntryStatusUnread = "unread"
|
||||
EntryStatusRead = "read"
|
||||
EntryStatusRemoved = "removed"
|
||||
DefaultSortingOrder = "published_at"
|
||||
DefaultSortingDirection = "asc"
|
||||
)
|
||||
|
||||
// Entry represents a feed item in the system.
|
||||
type Entry struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
FeedID int64 `json:"feed_id"`
|
||||
Status string `json:"status"`
|
||||
Hash string `json:"hash"`
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
CommentsURL string `json:"comments_url"`
|
||||
Date time.Time `json:"published_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ChangedAt time.Time `json:"changed_at"`
|
||||
Content string `json:"content"`
|
||||
Author string `json:"author"`
|
||||
ShareCode string `json:"share_code"`
|
||||
Starred bool `json:"starred"`
|
||||
ReadingTime int `json:"reading_time"`
|
||||
Enclosures EnclosureList `json:"enclosures"`
|
||||
Feed *Feed `json:"feed,omitempty"`
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
// Entries represents a list of entries.
|
||||
type Entries []*Entry
|
||||
|
||||
// EntriesStatusUpdateRequest represents a request to change entries status.
|
||||
type EntriesStatusUpdateRequest struct {
|
||||
EntryIDs []int64 `json:"entry_ids"`
|
||||
Status string `json:"status"`
|
||||
}
|
258
internal/model/feed.go
Normal file
258
internal/model/feed.go
Normal file
|
@ -0,0 +1,258 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/client"
|
||||
)
|
||||
|
||||
// List of supported schedulers.
|
||||
const (
|
||||
SchedulerRoundRobin = "round_robin"
|
||||
SchedulerEntryFrequency = "entry_frequency"
|
||||
// Default settings for the feed query builder
|
||||
DefaultFeedSorting = "parsing_error_count"
|
||||
DefaultFeedSortingDirection = "desc"
|
||||
)
|
||||
|
||||
// Feed represents a feed in the application.
|
||||
type Feed struct {
|
||||
ID int64 `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
FeedURL string `json:"feed_url"`
|
||||
SiteURL string `json:"site_url"`
|
||||
Title string `json:"title"`
|
||||
CheckedAt time.Time `json:"checked_at"`
|
||||
NextCheckAt time.Time `json:"next_check_at"`
|
||||
EtagHeader string `json:"etag_header"`
|
||||
LastModifiedHeader string `json:"last_modified_header"`
|
||||
ParsingErrorMsg string `json:"parsing_error_message"`
|
||||
ParsingErrorCount int `json:"parsing_error_count"`
|
||||
ScraperRules string `json:"scraper_rules"`
|
||||
RewriteRules string `json:"rewrite_rules"`
|
||||
Crawler bool `json:"crawler"`
|
||||
BlocklistRules string `json:"blocklist_rules"`
|
||||
KeeplistRules string `json:"keeplist_rules"`
|
||||
UrlRewriteRules string `json:"urlrewrite_rules"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Cookie string `json:"cookie"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Disabled bool `json:"disabled"`
|
||||
NoMediaPlayer bool `json:"no_media_player"`
|
||||
IgnoreHTTPCache bool `json:"ignore_http_cache"`
|
||||
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
|
||||
FetchViaProxy bool `json:"fetch_via_proxy"`
|
||||
Category *Category `json:"category,omitempty"`
|
||||
Entries Entries `json:"entries,omitempty"`
|
||||
IconURL string `json:"icon_url"`
|
||||
Icon *FeedIcon `json:"icon"`
|
||||
HideGlobally bool `json:"hide_globally"`
|
||||
UnreadCount int `json:"-"`
|
||||
ReadCount int `json:"-"`
|
||||
}
|
||||
|
||||
type FeedCounters struct {
|
||||
ReadCounters map[int64]int `json:"reads"`
|
||||
UnreadCounters map[int64]int `json:"unreads"`
|
||||
}
|
||||
|
||||
func (f *Feed) String() string {
|
||||
return fmt.Sprintf("ID=%d, UserID=%d, FeedURL=%s, SiteURL=%s, Title=%s, Category={%s}",
|
||||
f.ID,
|
||||
f.UserID,
|
||||
f.FeedURL,
|
||||
f.SiteURL,
|
||||
f.Title,
|
||||
f.Category,
|
||||
)
|
||||
}
|
||||
|
||||
// WithClientResponse updates feed attributes from an HTTP request.
|
||||
func (f *Feed) WithClientResponse(response *client.Response) {
|
||||
f.EtagHeader = response.ETag
|
||||
f.LastModifiedHeader = response.LastModified
|
||||
f.FeedURL = response.EffectiveURL
|
||||
}
|
||||
|
||||
// WithCategoryID initializes the category attribute of the feed.
|
||||
func (f *Feed) WithCategoryID(categoryID int64) {
|
||||
f.Category = &Category{ID: categoryID}
|
||||
}
|
||||
|
||||
// WithError adds a new error message and increment the error counter.
|
||||
func (f *Feed) WithError(message string) {
|
||||
f.ParsingErrorCount++
|
||||
f.ParsingErrorMsg = message
|
||||
}
|
||||
|
||||
// ResetErrorCounter removes all previous errors.
|
||||
func (f *Feed) ResetErrorCounter() {
|
||||
f.ParsingErrorCount = 0
|
||||
f.ParsingErrorMsg = ""
|
||||
}
|
||||
|
||||
// CheckedNow set attribute values when the feed is refreshed.
|
||||
func (f *Feed) CheckedNow() {
|
||||
f.CheckedAt = time.Now()
|
||||
|
||||
if f.SiteURL == "" {
|
||||
f.SiteURL = f.FeedURL
|
||||
}
|
||||
}
|
||||
|
||||
// ScheduleNextCheck set "next_check_at" of a feed based on the scheduler selected from the configuration.
|
||||
func (f *Feed) ScheduleNextCheck(weeklyCount int) {
|
||||
switch config.Opts.PollingScheduler() {
|
||||
case SchedulerEntryFrequency:
|
||||
var intervalMinutes int
|
||||
if weeklyCount == 0 {
|
||||
intervalMinutes = config.Opts.SchedulerEntryFrequencyMaxInterval()
|
||||
} else {
|
||||
intervalMinutes = int(math.Round(float64(7*24*60) / float64(weeklyCount)))
|
||||
}
|
||||
intervalMinutes = int(math.Min(float64(intervalMinutes), float64(config.Opts.SchedulerEntryFrequencyMaxInterval())))
|
||||
intervalMinutes = int(math.Max(float64(intervalMinutes), float64(config.Opts.SchedulerEntryFrequencyMinInterval())))
|
||||
f.NextCheckAt = time.Now().Add(time.Minute * time.Duration(intervalMinutes))
|
||||
default:
|
||||
f.NextCheckAt = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// FeedCreationRequest represents the request to create a feed.
|
||||
type FeedCreationRequest struct {
|
||||
FeedURL string `json:"feed_url"`
|
||||
CategoryID int64 `json:"category_id"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Cookie string `json:"cookie"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
Crawler bool `json:"crawler"`
|
||||
Disabled bool `json:"disabled"`
|
||||
NoMediaPlayer bool `json:"no_media_player"`
|
||||
IgnoreHTTPCache bool `json:"ignore_http_cache"`
|
||||
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
|
||||
FetchViaProxy bool `json:"fetch_via_proxy"`
|
||||
ScraperRules string `json:"scraper_rules"`
|
||||
RewriteRules string `json:"rewrite_rules"`
|
||||
BlocklistRules string `json:"blocklist_rules"`
|
||||
KeeplistRules string `json:"keeplist_rules"`
|
||||
HideGlobally bool `json:"hide_globally"`
|
||||
UrlRewriteRules string `json:"urlrewrite_rules"`
|
||||
}
|
||||
|
||||
// FeedModificationRequest represents the request to update a feed.
|
||||
type FeedModificationRequest struct {
|
||||
FeedURL *string `json:"feed_url"`
|
||||
SiteURL *string `json:"site_url"`
|
||||
Title *string `json:"title"`
|
||||
ScraperRules *string `json:"scraper_rules"`
|
||||
RewriteRules *string `json:"rewrite_rules"`
|
||||
BlocklistRules *string `json:"blocklist_rules"`
|
||||
KeeplistRules *string `json:"keeplist_rules"`
|
||||
UrlRewriteRules *string `json:"urlrewrite_rules"`
|
||||
Crawler *bool `json:"crawler"`
|
||||
UserAgent *string `json:"user_agent"`
|
||||
Cookie *string `json:"cookie"`
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
CategoryID *int64 `json:"category_id"`
|
||||
Disabled *bool `json:"disabled"`
|
||||
NoMediaPlayer *bool `json:"no_media_player"`
|
||||
IgnoreHTTPCache *bool `json:"ignore_http_cache"`
|
||||
AllowSelfSignedCertificates *bool `json:"allow_self_signed_certificates"`
|
||||
FetchViaProxy *bool `json:"fetch_via_proxy"`
|
||||
HideGlobally *bool `json:"hide_globally"`
|
||||
}
|
||||
|
||||
// Patch updates a feed with modified values.
|
||||
func (f *FeedModificationRequest) Patch(feed *Feed) {
|
||||
if f.FeedURL != nil && *f.FeedURL != "" {
|
||||
feed.FeedURL = *f.FeedURL
|
||||
}
|
||||
|
||||
if f.SiteURL != nil && *f.SiteURL != "" {
|
||||
feed.SiteURL = *f.SiteURL
|
||||
}
|
||||
|
||||
if f.Title != nil && *f.Title != "" {
|
||||
feed.Title = *f.Title
|
||||
}
|
||||
|
||||
if f.ScraperRules != nil {
|
||||
feed.ScraperRules = *f.ScraperRules
|
||||
}
|
||||
|
||||
if f.RewriteRules != nil {
|
||||
feed.RewriteRules = *f.RewriteRules
|
||||
}
|
||||
|
||||
if f.KeeplistRules != nil {
|
||||
feed.KeeplistRules = *f.KeeplistRules
|
||||
}
|
||||
|
||||
if f.UrlRewriteRules != nil {
|
||||
feed.UrlRewriteRules = *f.UrlRewriteRules
|
||||
}
|
||||
|
||||
if f.BlocklistRules != nil {
|
||||
feed.BlocklistRules = *f.BlocklistRules
|
||||
}
|
||||
|
||||
if f.Crawler != nil {
|
||||
feed.Crawler = *f.Crawler
|
||||
}
|
||||
|
||||
if f.UserAgent != nil {
|
||||
feed.UserAgent = *f.UserAgent
|
||||
}
|
||||
|
||||
if f.Cookie != nil {
|
||||
feed.Cookie = *f.Cookie
|
||||
}
|
||||
|
||||
if f.Username != nil {
|
||||
feed.Username = *f.Username
|
||||
}
|
||||
|
||||
if f.Password != nil {
|
||||
feed.Password = *f.Password
|
||||
}
|
||||
|
||||
if f.CategoryID != nil && *f.CategoryID > 0 {
|
||||
feed.Category.ID = *f.CategoryID
|
||||
}
|
||||
|
||||
if f.Disabled != nil {
|
||||
feed.Disabled = *f.Disabled
|
||||
}
|
||||
|
||||
if f.NoMediaPlayer != nil {
|
||||
feed.NoMediaPlayer = *f.NoMediaPlayer
|
||||
}
|
||||
|
||||
if f.IgnoreHTTPCache != nil {
|
||||
feed.IgnoreHTTPCache = *f.IgnoreHTTPCache
|
||||
}
|
||||
|
||||
if f.AllowSelfSignedCertificates != nil {
|
||||
feed.AllowSelfSignedCertificates = *f.AllowSelfSignedCertificates
|
||||
}
|
||||
|
||||
if f.FetchViaProxy != nil {
|
||||
feed.FetchViaProxy = *f.FetchViaProxy
|
||||
}
|
||||
|
||||
if f.HideGlobally != nil {
|
||||
feed.HideGlobally = *f.HideGlobally
|
||||
}
|
||||
}
|
||||
|
||||
// Feeds is a list of feed
|
||||
type Feeds []*Feed
|
154
internal/model/feed_test.go
Normal file
154
internal/model/feed_test.go
Normal file
|
@ -0,0 +1,154 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/config"
|
||||
"miniflux.app/v2/internal/http/client"
|
||||
)
|
||||
|
||||
func TestFeedWithResponse(t *testing.T) {
|
||||
response := &client.Response{ETag: "Some etag", LastModified: "Some date", EffectiveURL: "Some URL"}
|
||||
|
||||
feed := &Feed{}
|
||||
feed.WithClientResponse(response)
|
||||
|
||||
if feed.EtagHeader != "Some etag" {
|
||||
t.Fatal(`The ETag header should be set`)
|
||||
}
|
||||
|
||||
if feed.LastModifiedHeader != "Some date" {
|
||||
t.Fatal(`The LastModified header should be set`)
|
||||
}
|
||||
|
||||
if feed.FeedURL != "Some URL" {
|
||||
t.Fatal(`The Feed URL should be set`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeedCategorySetter(t *testing.T) {
|
||||
feed := &Feed{}
|
||||
feed.WithCategoryID(int64(123))
|
||||
|
||||
if feed.Category == nil {
|
||||
t.Fatal(`The category field should not be null`)
|
||||
}
|
||||
|
||||
if feed.Category.ID != int64(123) {
|
||||
t.Error(`The category ID must be set`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeedErrorCounter(t *testing.T) {
|
||||
feed := &Feed{}
|
||||
feed.WithError("Some Error")
|
||||
|
||||
if feed.ParsingErrorMsg != "Some Error" {
|
||||
t.Error(`The error message must be set`)
|
||||
}
|
||||
|
||||
if feed.ParsingErrorCount != 1 {
|
||||
t.Error(`The error counter must be set to 1`)
|
||||
}
|
||||
|
||||
feed.ResetErrorCounter()
|
||||
|
||||
if feed.ParsingErrorMsg != "" {
|
||||
t.Error(`The error message must be removed`)
|
||||
}
|
||||
|
||||
if feed.ParsingErrorCount != 0 {
|
||||
t.Error(`The error counter must be set to 0`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeedCheckedNow(t *testing.T) {
|
||||
feed := &Feed{}
|
||||
feed.FeedURL = "https://example.org/feed"
|
||||
feed.CheckedNow()
|
||||
|
||||
if feed.SiteURL != feed.FeedURL {
|
||||
t.Error(`The site URL must not be empty`)
|
||||
}
|
||||
|
||||
if feed.CheckedAt.IsZero() {
|
||||
t.Error(`The checked date must be set`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeedScheduleNextCheckDefault(t *testing.T) {
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
|
||||
feed := &Feed{}
|
||||
weeklyCount := 10
|
||||
feed.ScheduleNextCheck(weeklyCount)
|
||||
|
||||
if feed.NextCheckAt.IsZero() {
|
||||
t.Error(`The next_check_at must be set`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeedScheduleNextCheckEntryCountBasedMaxInterval(t *testing.T) {
|
||||
maxInterval := 5
|
||||
minInterval := 1
|
||||
os.Clearenv()
|
||||
os.Setenv("POLLING_SCHEDULER", "entry_frequency")
|
||||
os.Setenv("SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL", fmt.Sprintf("%d", maxInterval))
|
||||
os.Setenv("SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL", fmt.Sprintf("%d", minInterval))
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
feed := &Feed{}
|
||||
weeklyCount := maxInterval * 100
|
||||
feed.ScheduleNextCheck(weeklyCount)
|
||||
|
||||
if feed.NextCheckAt.IsZero() {
|
||||
t.Error(`The next_check_at must be set`)
|
||||
}
|
||||
|
||||
if feed.NextCheckAt.After(time.Now().Add(time.Minute * time.Duration(maxInterval))) {
|
||||
t.Error(`The next_check_at should not be after the now + max interval`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFeedScheduleNextCheckEntryCountBasedMinInterval(t *testing.T) {
|
||||
maxInterval := 500
|
||||
minInterval := 100
|
||||
os.Clearenv()
|
||||
os.Setenv("POLLING_SCHEDULER", "entry_frequency")
|
||||
os.Setenv("SCHEDULER_ENTRY_FREQUENCY_MAX_INTERVAL", fmt.Sprintf("%d", maxInterval))
|
||||
os.Setenv("SCHEDULER_ENTRY_FREQUENCY_MIN_INTERVAL", fmt.Sprintf("%d", minInterval))
|
||||
|
||||
var err error
|
||||
parser := config.NewParser()
|
||||
config.Opts, err = parser.ParseEnvironmentVariables()
|
||||
if err != nil {
|
||||
t.Fatalf(`Parsing failure: %v`, err)
|
||||
}
|
||||
feed := &Feed{}
|
||||
weeklyCount := minInterval / 2
|
||||
feed.ScheduleNextCheck(weeklyCount)
|
||||
|
||||
if feed.NextCheckAt.IsZero() {
|
||||
t.Error(`The next_check_at must be set`)
|
||||
}
|
||||
|
||||
if feed.NextCheckAt.Before(time.Now().Add(time.Minute * time.Duration(minInterval))) {
|
||||
t.Error(`The next_check_at should not be before the now + min interval`)
|
||||
}
|
||||
}
|
15
internal/model/home_page.go
Normal file
15
internal/model/home_page.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
|
||||
// HomePages returns the list of available home pages.
|
||||
func HomePages() map[string]string {
|
||||
return map[string]string{
|
||||
"unread": "menu.unread",
|
||||
"starred": "menu.starred",
|
||||
"history": "menu.history",
|
||||
"feeds": "menu.feeds",
|
||||
"categories": "menu.categories",
|
||||
}
|
||||
}
|
31
internal/model/icon.go
Normal file
31
internal/model/icon.go
Normal file
|
@ -0,0 +1,31 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Icon represents a website icon (favicon)
|
||||
type Icon struct {
|
||||
ID int64 `json:"id"`
|
||||
Hash string `json:"hash"`
|
||||
MimeType string `json:"mime_type"`
|
||||
Content []byte `json:"content"`
|
||||
}
|
||||
|
||||
// DataURL returns the data URL of the icon.
|
||||
func (i *Icon) DataURL() string {
|
||||
return fmt.Sprintf("%s;base64,%s", i.MimeType, base64.StdEncoding.EncodeToString(i.Content))
|
||||
}
|
||||
|
||||
// Icons represents a list of icons.
|
||||
type Icons []*Icon
|
||||
|
||||
// FeedIcon is a junction table between feeds and icons.
|
||||
type FeedIcon struct {
|
||||
FeedID int64 `json:"feed_id"`
|
||||
IconID int64 `json:"icon_id"`
|
||||
}
|
60
internal/model/integration.go
Normal file
60
internal/model/integration.go
Normal file
|
@ -0,0 +1,60 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
|
||||
// Integration represents user integration settings.
|
||||
type Integration struct {
|
||||
UserID int64
|
||||
PinboardEnabled bool
|
||||
PinboardToken string
|
||||
PinboardTags string
|
||||
PinboardMarkAsUnread bool
|
||||
InstapaperEnabled bool
|
||||
InstapaperUsername string
|
||||
InstapaperPassword string
|
||||
FeverEnabled bool
|
||||
FeverUsername string
|
||||
FeverToken string
|
||||
GoogleReaderEnabled bool
|
||||
GoogleReaderUsername string
|
||||
GoogleReaderPassword string
|
||||
WallabagEnabled bool
|
||||
WallabagOnlyURL bool
|
||||
WallabagURL string
|
||||
WallabagClientID string
|
||||
WallabagClientSecret string
|
||||
WallabagUsername string
|
||||
WallabagPassword string
|
||||
NunuxKeeperEnabled bool
|
||||
NunuxKeeperURL string
|
||||
NunuxKeeperAPIKey string
|
||||
NotionEnabled bool
|
||||
NotionToken string
|
||||
NotionPageID string
|
||||
EspialEnabled bool
|
||||
EspialURL string
|
||||
EspialAPIKey string
|
||||
EspialTags string
|
||||
ReadwiseEnabled bool
|
||||
ReadwiseAPIKey string
|
||||
PocketEnabled bool
|
||||
PocketAccessToken string
|
||||
PocketConsumerKey string
|
||||
TelegramBotEnabled bool
|
||||
TelegramBotToken string
|
||||
TelegramBotChatID string
|
||||
LinkdingEnabled bool
|
||||
LinkdingURL string
|
||||
LinkdingAPIKey string
|
||||
LinkdingTags string
|
||||
LinkdingMarkAsUnread bool
|
||||
MatrixBotEnabled bool
|
||||
MatrixBotUser string
|
||||
MatrixBotPassword string
|
||||
MatrixBotURL string
|
||||
MatrixBotChatID string
|
||||
AppriseEnabled bool
|
||||
AppriseURL string
|
||||
AppriseServicesURL string
|
||||
}
|
13
internal/model/job.go
Normal file
13
internal/model/job.go
Normal file
|
@ -0,0 +1,13 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
|
||||
// Job represents a payload sent to the processing queue.
|
||||
type Job struct {
|
||||
UserID int64
|
||||
FeedID int64
|
||||
}
|
||||
|
||||
// JobList represents a list of jobs.
|
||||
type JobList []Job
|
28
internal/model/model.go
Normal file
28
internal/model/model.go
Normal file
|
@ -0,0 +1,28 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
|
||||
// OptionalString populates an optional string field.
|
||||
func OptionalString(value string) *string {
|
||||
if value != "" {
|
||||
return &value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OptionalInt populates an optional int field.
|
||||
func OptionalInt(value int) *int {
|
||||
if value > 0 {
|
||||
return &value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// OptionalInt64 populates an optional int64 field.
|
||||
func OptionalInt64(value int64) *int64 {
|
||||
if value > 0 {
|
||||
return &value
|
||||
}
|
||||
return nil
|
||||
}
|
15
internal/model/subscription.go
Normal file
15
internal/model/subscription.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
|
||||
// SubscriptionDiscoveryRequest represents a request to discover subscriptions.
|
||||
type SubscriptionDiscoveryRequest struct {
|
||||
URL string `json:"url"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Cookie string `json:"cookie"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
FetchViaProxy bool `json:"fetch_via_proxy"`
|
||||
AllowSelfSignedCertificates bool `json:"allow_self_signed_certificates"`
|
||||
}
|
35
internal/model/theme.go
Normal file
35
internal/model/theme.go
Normal file
|
@ -0,0 +1,35 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
|
||||
// Themes returns the list of available themes.
|
||||
func Themes() map[string]string {
|
||||
return map[string]string{
|
||||
"light_serif": "Light - Serif",
|
||||
"light_sans_serif": "Light - Sans Serif",
|
||||
"dark_serif": "Dark - Serif",
|
||||
"dark_sans_serif": "Dark - Sans Serif",
|
||||
"system_serif": "System - Serif",
|
||||
"system_sans_serif": "System - Sans Serif",
|
||||
}
|
||||
}
|
||||
|
||||
// ThemeColor returns the color for the address bar or/and the browser color.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/Manifest#theme_color
|
||||
// https://developers.google.com/web/tools/lighthouse/audits/address-bar
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/meta/name/theme-color
|
||||
func ThemeColor(theme, colorScheme string) string {
|
||||
switch theme {
|
||||
case "dark_serif", "dark_sans_serif":
|
||||
return "#222"
|
||||
case "system_serif", "system_sans_serif":
|
||||
if colorScheme == "dark" {
|
||||
return "#222"
|
||||
}
|
||||
|
||||
return "#fff"
|
||||
default:
|
||||
return "#fff"
|
||||
}
|
||||
}
|
181
internal/model/user.go
Normal file
181
internal/model/user.go
Normal file
|
@ -0,0 +1,181 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/timezone"
|
||||
)
|
||||
|
||||
// User represents a user in the system.
|
||||
type User struct {
|
||||
ID int64 `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"-"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
Theme string `json:"theme"`
|
||||
Language string `json:"language"`
|
||||
Timezone string `json:"timezone"`
|
||||
EntryDirection string `json:"entry_sorting_direction"`
|
||||
EntryOrder string `json:"entry_sorting_order"`
|
||||
Stylesheet string `json:"stylesheet"`
|
||||
GoogleID string `json:"google_id"`
|
||||
OpenIDConnectID string `json:"openid_connect_id"`
|
||||
EntriesPerPage int `json:"entries_per_page"`
|
||||
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
|
||||
ShowReadingTime bool `json:"show_reading_time"`
|
||||
EntrySwipe bool `json:"entry_swipe"`
|
||||
GestureNav string `json:"gesture_nav"`
|
||||
LastLoginAt *time.Time `json:"last_login_at"`
|
||||
DisplayMode string `json:"display_mode"`
|
||||
DefaultReadingSpeed int `json:"default_reading_speed"`
|
||||
CJKReadingSpeed int `json:"cjk_reading_speed"`
|
||||
DefaultHomePage string `json:"default_home_page"`
|
||||
CategoriesSortingOrder string `json:"categories_sorting_order"`
|
||||
MarkReadOnView bool `json:"mark_read_on_view"`
|
||||
}
|
||||
|
||||
// UserCreationRequest represents the request to create a user.
|
||||
type UserCreationRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
GoogleID string `json:"google_id"`
|
||||
OpenIDConnectID string `json:"openid_connect_id"`
|
||||
}
|
||||
|
||||
// UserModificationRequest represents the request to update a user.
|
||||
type UserModificationRequest struct {
|
||||
Username *string `json:"username"`
|
||||
Password *string `json:"password"`
|
||||
Theme *string `json:"theme"`
|
||||
Language *string `json:"language"`
|
||||
Timezone *string `json:"timezone"`
|
||||
EntryDirection *string `json:"entry_sorting_direction"`
|
||||
EntryOrder *string `json:"entry_sorting_order"`
|
||||
Stylesheet *string `json:"stylesheet"`
|
||||
GoogleID *string `json:"google_id"`
|
||||
OpenIDConnectID *string `json:"openid_connect_id"`
|
||||
EntriesPerPage *int `json:"entries_per_page"`
|
||||
IsAdmin *bool `json:"is_admin"`
|
||||
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
||||
ShowReadingTime *bool `json:"show_reading_time"`
|
||||
EntrySwipe *bool `json:"entry_swipe"`
|
||||
GestureNav *string `json:"gesture_nav"`
|
||||
DisplayMode *string `json:"display_mode"`
|
||||
DefaultReadingSpeed *int `json:"default_reading_speed"`
|
||||
CJKReadingSpeed *int `json:"cjk_reading_speed"`
|
||||
DefaultHomePage *string `json:"default_home_page"`
|
||||
CategoriesSortingOrder *string `json:"categories_sorting_order"`
|
||||
MarkReadOnView *bool `json:"mark_read_on_view"`
|
||||
}
|
||||
|
||||
// Patch updates the User object with the modification request.
|
||||
func (u *UserModificationRequest) Patch(user *User) {
|
||||
if u.Username != nil {
|
||||
user.Username = *u.Username
|
||||
}
|
||||
|
||||
if u.Password != nil {
|
||||
user.Password = *u.Password
|
||||
}
|
||||
|
||||
if u.IsAdmin != nil {
|
||||
user.IsAdmin = *u.IsAdmin
|
||||
}
|
||||
|
||||
if u.Theme != nil {
|
||||
user.Theme = *u.Theme
|
||||
}
|
||||
|
||||
if u.Language != nil {
|
||||
user.Language = *u.Language
|
||||
}
|
||||
|
||||
if u.Timezone != nil {
|
||||
user.Timezone = *u.Timezone
|
||||
}
|
||||
|
||||
if u.EntryDirection != nil {
|
||||
user.EntryDirection = *u.EntryDirection
|
||||
}
|
||||
|
||||
if u.EntryOrder != nil {
|
||||
user.EntryOrder = *u.EntryOrder
|
||||
}
|
||||
|
||||
if u.Stylesheet != nil {
|
||||
user.Stylesheet = *u.Stylesheet
|
||||
}
|
||||
|
||||
if u.GoogleID != nil {
|
||||
user.GoogleID = *u.GoogleID
|
||||
}
|
||||
|
||||
if u.OpenIDConnectID != nil {
|
||||
user.OpenIDConnectID = *u.OpenIDConnectID
|
||||
}
|
||||
|
||||
if u.EntriesPerPage != nil {
|
||||
user.EntriesPerPage = *u.EntriesPerPage
|
||||
}
|
||||
|
||||
if u.KeyboardShortcuts != nil {
|
||||
user.KeyboardShortcuts = *u.KeyboardShortcuts
|
||||
}
|
||||
|
||||
if u.ShowReadingTime != nil {
|
||||
user.ShowReadingTime = *u.ShowReadingTime
|
||||
}
|
||||
|
||||
if u.EntrySwipe != nil {
|
||||
user.EntrySwipe = *u.EntrySwipe
|
||||
}
|
||||
|
||||
if u.GestureNav != nil {
|
||||
user.GestureNav = *u.GestureNav
|
||||
}
|
||||
|
||||
if u.DisplayMode != nil {
|
||||
user.DisplayMode = *u.DisplayMode
|
||||
}
|
||||
|
||||
if u.DefaultReadingSpeed != nil {
|
||||
user.DefaultReadingSpeed = *u.DefaultReadingSpeed
|
||||
}
|
||||
|
||||
if u.CJKReadingSpeed != nil {
|
||||
user.CJKReadingSpeed = *u.CJKReadingSpeed
|
||||
}
|
||||
|
||||
if u.DefaultHomePage != nil {
|
||||
user.DefaultHomePage = *u.DefaultHomePage
|
||||
}
|
||||
|
||||
if u.CategoriesSortingOrder != nil {
|
||||
user.CategoriesSortingOrder = *u.CategoriesSortingOrder
|
||||
}
|
||||
|
||||
if u.MarkReadOnView != nil {
|
||||
user.MarkReadOnView = *u.MarkReadOnView
|
||||
}
|
||||
}
|
||||
|
||||
// UseTimezone converts last login date to the given timezone.
|
||||
func (u *User) UseTimezone(tz string) {
|
||||
if u.LastLoginAt != nil {
|
||||
*u.LastLoginAt = timezone.Convert(tz, *u.LastLoginAt)
|
||||
}
|
||||
}
|
||||
|
||||
// Users represents a list of users.
|
||||
type Users []*User
|
||||
|
||||
// UseTimezone converts last login timestamp of all users to the given timezone.
|
||||
func (u Users) UseTimezone(tz string) {
|
||||
for _, user := range u {
|
||||
user.UseTimezone(tz)
|
||||
}
|
||||
}
|
40
internal/model/user_session.go
Normal file
40
internal/model/user_session.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package model // import "miniflux.app/v2/internal/model"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"miniflux.app/v2/internal/timezone"
|
||||
)
|
||||
|
||||
// UserSession represents a user session in the system.
|
||||
type UserSession struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
Token string
|
||||
CreatedAt time.Time
|
||||
UserAgent string
|
||||
IP string
|
||||
}
|
||||
|
||||
func (u *UserSession) String() string {
|
||||
return fmt.Sprintf(`ID="%d", UserID="%d", IP="%s", Token="%s"`, u.ID, u.UserID, u.IP, u.Token)
|
||||
}
|
||||
|
||||
// UseTimezone converts creation date to the given timezone.
|
||||
func (u *UserSession) UseTimezone(tz string) {
|
||||
u.CreatedAt = timezone.Convert(tz, u.CreatedAt)
|
||||
}
|
||||
|
||||
// UserSessions represents a list of sessions.
|
||||
type UserSessions []*UserSession
|
||||
|
||||
// UseTimezone converts creation date of all sessions to the given timezone.
|
||||
func (u UserSessions) UseTimezone(tz string) {
|
||||
for _, session := range u {
|
||||
session.UseTimezone(tz)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue