-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
core.go
198 lines (168 loc) · 4.95 KB
/
core.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
// package core is the collection of re-usable functions that primarily provides data (DB / CRUD) operations
// to the app. For instance, creating and mutating objects like lists, subscribers etc.
// All such methods return an echo.HTTPError{} (which implements error.error) that can be directly returned
// as a response to HTTP handlers without further processing.
package core
import (
"bytes"
"fmt"
"log"
"regexp"
"strings"
"github.com/ghostdevv/listmonk-tweaked/internal/i18n"
"github.com/ghostdevv/listmonk-tweaked/models"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
)
const (
SortAsc = "asc"
SortDesc = "desc"
matDashboardCharts = "mat_dashboard_charts"
matDashboardCounts = "mat_dashboard_counts"
matListSubStats = "mat_list_subscriber_stats"
)
// Core represents the listmonk core with all shared, global functions.
type Core struct {
h *Hooks
consts Constants
i18n *i18n.I18n
db *sqlx.DB
q *models.Queries
log *log.Logger
}
// Constants represents constant config.
type Constants struct {
SendOptinConfirmation bool
BounceActions map[string]struct {
Count int
Action string
}
CacheSlowQueries bool
}
// Hooks contains external function hooks that are required by the core package.
type Hooks struct {
SendOptinConfirmation func(models.Subscriber, []int) (int, error)
SendNewSubscriberWebhook func(int, []int) error
}
// Opt contains the controllers required to start the core.
type Opt struct {
Constants Constants
I18n *i18n.I18n
DB *sqlx.DB
Queries *models.Queries
Log *log.Logger
}
var (
regexFullTextQuery = regexp.MustCompile(`\s+`)
regexpSpaces = regexp.MustCompile(`[\s]+`)
campQuerySortFields = []string{"name", "status", "created_at", "updated_at"}
subQuerySortFields = []string{"email", "status", "name", "created_at", "updated_at"}
listQuerySortFields = []string{"name", "status", "created_at", "updated_at", "subscriber_count"}
)
// New returns a new instance of the core.
func New(o *Opt, h *Hooks) *Core {
return &Core{
h: h,
consts: o.Constants,
i18n: o.I18n,
db: o.DB,
q: o.Queries,
log: o.Log,
}
}
// RefreshMatViews refreshes all materialized views.
func (c *Core) RefreshMatViews(concurrent bool) error {
for _, v := range []string{matDashboardCharts, matDashboardCounts, matListSubStats} {
_ = c.RefreshMatView(v, true)
}
return nil
}
// RefreshMatView refreshes a Postgres materialized view.
func (c *Core) RefreshMatView(name string, concurrent bool) error {
q := "REFRESH MATERIALIZED VIEW %s %s"
if concurrent {
q = fmt.Sprintf(q, "CONCURRENTLY", name)
} else {
q = fmt.Sprintf(q, "", name)
}
if _, err := c.db.Exec(q); err != nil {
c.log.Printf("error refreshing materialized view: %s: %v", name, err)
return err
}
return nil
}
// refreshCache refreshes a Postgres materialized view if caching is disabled.
func (c *Core) refreshCache(name string, concurrent bool) error {
if c.consts.CacheSlowQueries {
return nil
}
return c.RefreshMatView(name, concurrent)
}
// Given an error, pqErrMsg will try to return pq error details
// if it's a pq error.
func pqErrMsg(err error) string {
if err, ok := err.(*pq.Error); ok {
if err.Detail != "" {
return fmt.Sprintf("%s. %s", err, err.Detail)
}
}
return err.Error()
}
// makeSearchQuery cleans an optional search string and prepares the
// query SQL statement (string interpolated) and returns the
// search query string along with the SQL expression.
func makeSearchQuery(searchStr, orderBy, order, query string, querySortFields []string) (string, string) {
if searchStr != "" {
searchStr = `%` + string(regexFullTextQuery.ReplaceAll([]byte(searchStr), []byte("&"))) + `%`
}
// Sort params.
if !strSliceContains(orderBy, querySortFields) {
orderBy = "created_at"
}
if order != SortAsc && order != SortDesc {
order = SortDesc
}
query = strings.ReplaceAll(query, "%order%", orderBy+" "+order)
return searchStr, query
}
// strSliceContains checks if a string is present in the string slice.
func strSliceContains(str string, sl []string) bool {
for _, s := range sl {
if s == str {
return true
}
}
return false
}
// normalizeTags takes a list of string tags and normalizes them by
// lower casing and removing all special characters except for dashes.
func normalizeTags(tags []string) []string {
var (
out []string
dash = []byte("-")
)
for _, t := range tags {
rep := regexpSpaces.ReplaceAll(bytes.TrimSpace([]byte(t)), dash)
if len(rep) > 0 {
out = append(out, string(rep))
}
}
return out
}
// sanitizeSQLExp does basic sanitisation on arbitrary
// SQL query expressions coming from the frontend.
func sanitizeSQLExp(q string) string {
if len(q) == 0 {
return ""
}
q = strings.TrimSpace(q)
// Remove semicolon suffix.
if q[len(q)-1] == ';' {
q = q[:len(q)-1]
}
return q
}
// strHasLen checks if the given string has a length within min-max.
func strHasLen(str string, min, max int) bool {
return len(str) >= min && len(str) <= max
}