+1
-3
internal/config/config.go
+1
-3
internal/config/config.go
+4
-6
internal/freshrss/client.go
+4
-6
internal/freshrss/client.go
···
34
34
}
35
35
36
36
func NewClient(host string) *Client {
37
-
// todo: validate host url
38
37
return &Client{
39
38
host: host,
40
39
client: &http.Client{
···
62
61
return "", ErrUnauthorized
63
62
}
64
63
65
-
func (g *Client) SetAuthToken(token string) {
66
-
// todo: validate token
67
-
g.authToken = token
68
-
}
64
+
func (g *Client) SetAuthToken(token string) { g.authToken = token }
69
65
70
66
func (g Client) GetWriteToken(ctx context.Context) (string, error) {
71
67
var resp string
···
255
251
}
256
252
257
253
func (g Client) SubscriptionEdit(ctx context.Context, token string, opts EditSubscription) (string, error) {
258
-
// todo: action is required
254
+
if opts.Action == "" {
255
+
return "", ErrInvalidRequest
256
+
}
259
257
260
258
body := url.Values{}
261
259
body.Set("T", token)
+1
-1
internal/store/sqlite.go
+1
-1
internal/store/sqlite.go
+31
-82
internal/store/sqlite_articles.go
+31
-82
internal/store/sqlite_articles.go
···
6
6
"strings"
7
7
)
8
8
9
-
func (s *Sqlite) UpsertArticle(ctx context.Context, timestampUsec, feedID, title, content, author, href string, publishedAt int) error {
10
-
tx, err := s.db.Begin()
9
+
func (s *Sqlite) UpsertArticle(
10
+
ctx context.Context,
11
+
timestampUsec, feedID, title, content, author, href string,
12
+
publishedAt int,
13
+
) error {
14
+
tx, err := s.db.BeginTx(ctx, nil)
11
15
if err != nil {
12
16
return err
13
17
}
···
26
30
return tx.Commit()
27
31
}
28
32
29
-
// can be done like this?
30
-
// --sql
31
-
// update article_statuses
32
-
// set is_starred = case
33
-
// when article_id in (%s) then 1
34
-
// else 0
35
-
// end
36
-
37
33
func (s *Sqlite) SyncReadStatus(ctx context.Context, ids []string) error {
38
-
if len(ids) == 0 {
39
-
_, err := s.db.ExecContext(ctx, "update article_statuses set is_read = 1")
40
-
return err
41
-
}
42
-
43
-
values := strings.Repeat("(?),", len(ids))
44
-
values = values[:len(values)-1]
45
-
46
-
args := make([]any, len(ids))
47
-
for i, v := range ids {
48
-
args[i] = v
49
-
}
50
-
51
-
tx, err := s.db.Begin()
52
-
if err != nil {
53
-
return err
54
-
}
55
-
defer tx.Rollback()
56
-
57
-
// make read those that are not in list
58
-
readQuery := fmt.Sprintf(`--sql
34
+
placeholders, args := buildPlaceholdersAndArgs(ids)
35
+
query := fmt.Sprintf(`--sql
59
36
update article_statuses
60
-
set is_read = true
61
-
where article_id not in (%s)`, values)
62
-
63
-
if _, err = tx.ExecContext(ctx, readQuery, args...); err != nil {
64
-
return err
65
-
}
66
-
67
-
// make unread those that are in list
68
-
unreadQuery := fmt.Sprintf(`--sql
69
-
update article_statuses
70
-
set is_read = false
71
-
where article_id in (%s)`, values)
37
+
set is_read = case when article_id in (%s)
38
+
then false
39
+
else true
40
+
end`, placeholders)
72
41
73
-
if _, err = tx.ExecContext(ctx, unreadQuery, args...); err != nil {
74
-
return err
75
-
}
76
-
77
-
return tx.Commit()
42
+
_, err := s.db.ExecContext(ctx, query, args...)
43
+
return err
78
44
}
79
45
80
46
func (s *Sqlite) SyncStarredStatus(ctx context.Context, ids []string) error {
81
-
if len(ids) == 0 {
82
-
_, err := s.db.ExecContext(ctx, "update article_statuses set is_starred = 0")
83
-
return err
84
-
}
85
-
86
-
values := strings.Repeat("(?),", len(ids))
87
-
values = values[:len(values)-1]
88
-
89
-
args := make([]any, len(ids))
90
-
for i, v := range ids {
91
-
args[i] = v
92
-
}
93
-
94
-
tx, err := s.db.Begin()
95
-
if err != nil {
96
-
return err
97
-
}
98
-
defer tx.Rollback()
99
-
100
-
// make read those that are not in list
101
-
readQuery := fmt.Sprintf(`--sql
47
+
placeholders, args := buildPlaceholdersAndArgs(ids)
48
+
query := fmt.Sprintf(`--sql
102
49
update article_statuses
103
-
set is_starred = false
104
-
where article_id not in (%s)`, values)
50
+
set is_starred = case when article_id in (%s)
51
+
then true
52
+
else false
53
+
end`, placeholders)
105
54
106
-
if _, err = tx.ExecContext(ctx, readQuery, args...); err != nil {
107
-
return err
108
-
}
55
+
_, err := s.db.ExecContext(ctx, query, args...)
56
+
return err
57
+
}
109
58
110
-
// make unread those that are in list
111
-
unreadQuery := fmt.Sprintf(`--sql
112
-
update article_statuses
113
-
set is_starred = true
114
-
where article_id in (%s)`, values)
59
+
func buildPlaceholdersAndArgs(in []string, prefixArgs ...any) (placeholders string, args []any) {
60
+
placeholders = strings.Repeat("?,", len(in))
61
+
placeholders = placeholders[:len(placeholders)-1] // trim trailing comma
115
62
116
-
if _, err = tx.ExecContext(ctx, unreadQuery, args...); err != nil {
117
-
return err
63
+
args = make([]any, len(prefixArgs)+len(in))
64
+
copy(args, prefixArgs)
65
+
for i, v := range in {
66
+
args[len(prefixArgs)+i] = v
118
67
}
119
68
120
-
return tx.Commit()
69
+
return placeholders, args
121
70
}
+2
-15
internal/store/sqlite_feeds.go
+2
-15
internal/store/sqlite_feeds.go
···
3
3
import (
4
4
"context"
5
5
"fmt"
6
-
"strings"
7
6
)
8
7
9
8
func (s *Sqlite) UpsertSubscription(ctx context.Context, id, title, url, htmlURL string) error {
···
23
22
}
24
23
25
24
func (s *Sqlite) RemoveNonExistentFeeds(ctx context.Context, currentFeedIDs []string) error {
26
-
if len(currentFeedIDs) == 0 {
27
-
_, err := s.db.ExecContext(ctx, "delete from feeds")
28
-
return err
29
-
}
30
-
31
-
values := strings.Repeat("(?),", len(currentFeedIDs))
32
-
values = values[:len(values)-1] // trim trailing comma
33
-
25
+
placeholders, args := buildPlaceholdersAndArgs(currentFeedIDs)
34
26
query := fmt.Sprintf(`--sql
35
27
DELETE FROM feeds
36
28
WHERE id NOT IN (%s)
37
-
`, values)
38
-
39
-
args := make([]any, len(currentFeedIDs))
40
-
for i, v := range currentFeedIDs {
41
-
args[i] = v
42
-
}
29
+
`, placeholders)
43
30
44
31
_, err := s.db.ExecContext(ctx, query, args...)
45
32
return err
+1
-3
internal/store/sqlite_folders.go
+1
-3
internal/store/sqlite_folders.go
···
3
3
import "context"
4
4
5
5
func (s *Sqlite) UpsertTag(ctx context.Context, id string) error {
6
-
_, err := s.db.ExecContext(ctx,
7
-
`insert or replace into folders (id) values (?)`,
8
-
id)
6
+
_, err := s.db.ExecContext(ctx, `insert or replace into folders (id) values (?)`, id)
9
7
return err
10
8
}
+15
-25
internal/store/sqlite_pendin_actions.go
+15
-25
internal/store/sqlite_pendin_actions.go
···
3
3
import (
4
4
"context"
5
5
"fmt"
6
-
"strings"
7
6
)
8
7
9
8
type Action int
···
30
29
}
31
30
}
32
31
32
+
var changeArticleStatusQuery = map[Action]string{
33
+
Read: `update article_statuses set is_read = 1 where article_id = ?`,
34
+
Unread: `update article_statuses set is_read = 0 where article_id = ?`,
35
+
Star: `update article_statuses set is_starred = 1 where article_id = ?`,
36
+
Unstar: `update article_statuses set is_starred = 0 where article_id = ?`,
37
+
}
38
+
33
39
func (s *Sqlite) ChangeArticleStatus(ctx context.Context, articleID string, action Action) error {
34
-
tx, err := s.db.Begin()
40
+
tx, err := s.db.BeginTx(ctx, nil)
35
41
if err != nil {
36
42
return err
37
43
}
38
44
defer tx.Rollback()
39
45
40
46
// update article status
41
-
var query string
42
-
switch action {
43
-
case Read:
44
-
query = `update article_statuses set is_read = 1 where article_id = ?`
45
-
case Unread:
46
-
query = `update article_statuses set is_read = 0 where article_id = ?`
47
-
case Star:
48
-
query = `update article_statuses set is_starred = 1 where article_id = ?`
49
-
case Unstar:
50
-
query = `update article_statuses set is_starred = 0 where article_id = ?`
51
-
}
52
-
53
-
e, err := tx.ExecContext(ctx, query, articleID)
47
+
e, err := tx.ExecContext(ctx, changeArticleStatusQuery[action], articleID)
54
48
if err != nil {
55
49
return err
56
50
}
···
97
91
return res, nil
98
92
}
99
93
100
-
func (s *Sqlite) DeletePendingActions(ctx context.Context, action Action, articleIDs []string) error {
101
-
placeholders := strings.Repeat("(?),", len(articleIDs))
102
-
placeholders = placeholders[:len(placeholders)-1]
103
-
104
-
args := make([]any, len(articleIDs)+1)
105
-
args[0] = action.String()
106
-
for i, v := range articleIDs {
107
-
args[i+1] = v
108
-
}
109
-
94
+
func (s *Sqlite) DeletePendingActions(
95
+
ctx context.Context,
96
+
action Action,
97
+
articleIDs []string,
98
+
) error {
99
+
placeholders, args := buildPlaceholdersAndArgs(articleIDs, action.String())
110
100
query := fmt.Sprintf(`--sql
111
101
delete from pending_actions
112
102
where action = ?
+7
-4
internal/store/sqlite_reader.go
+7
-4
internal/store/sqlite_reader.go
···
8
8
9
9
func (s *Sqlite) GetLastSyncTime(ctx context.Context) (int64, error) {
10
10
var lut int64
11
-
err := s.db.QueryRowContext(ctx, "select last_sync from reader where id = 1 and last_sync is not null").Scan(&lut)
11
+
err := s.db.QueryRowContext(ctx, "select last_sync from reader where id = 1 and last_sync is not null").
12
+
Scan(&lut)
12
13
if errors.Is(err, sql.ErrNoRows) {
13
14
return 0, ErrNotFound
14
15
}
···
25
26
26
27
func (s *Sqlite) GetToken(ctx context.Context) (string, error) {
27
28
var tok string
28
-
err := s.db.QueryRowContext(ctx, "select token from reader where id = 1 and token is not null").Scan(&tok)
29
+
err := s.db.QueryRowContext(ctx, "select token from reader where id = 1 and token is not null").
30
+
Scan(&tok)
29
31
if errors.Is(err, sql.ErrNoRows) {
30
32
return "", ErrNotFound
31
33
}
···
34
36
35
37
func (s *Sqlite) SetToken(ctx context.Context, token string) error {
36
38
_, err := s.db.ExecContext(ctx,
37
-
`insert into reader (id, write_token) values (1, ?)
39
+
`insert into reader (id, token) values (1, ?)
38
40
on conflict(id) do update set token = excluded.token`,
39
41
token)
40
42
return err
···
42
44
43
45
func (s *Sqlite) GetWriteToken(ctx context.Context) (string, error) {
44
46
var tok string
45
-
err := s.db.QueryRowContext(ctx, "select write_token from reader where id = 1 and write_token is not null").Scan(&tok)
47
+
err := s.db.QueryRowContext(ctx, "select write_token from reader where id = 1 and write_token is not null").
48
+
Scan(&tok)
46
49
if errors.Is(err, sql.ErrNoRows) {
47
50
return "", ErrNotFound
48
51
}