MM-27918 In-Product notices support (#15316)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
43ed6ad690
Коммит
4e9ddd4686
@@ -12,7 +12,6 @@ executors:
|
||||
image: "ubuntu-1604:201903-01"
|
||||
environment:
|
||||
COMPOSE_PROJECT_NAME: "circleci"
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
working_directory: ~/mattermost/mattermost-server
|
||||
@@ -174,6 +173,7 @@ jobs:
|
||||
export VERSION=$(sentry-cli releases propose-version)
|
||||
sentry-cli --log-level=debug releases new --finalize -p mattermost-server $VERSION
|
||||
sentry-cli --log-level=debug releases set-commits --auto $VERSION
|
||||
|
||||
- store_artifacts:
|
||||
path: ~/mattermost/mattermost-server/dist/mattermost-team-linux-amd64.tar.gz
|
||||
- store_artifacts:
|
||||
@@ -381,7 +381,7 @@ jobs:
|
||||
|
||||
upload-s3:
|
||||
docker:
|
||||
- image: 'circleci/python:2.7'
|
||||
- image: 'circleci/python:2.7'
|
||||
working_directory: ~/mattermost/enterprise
|
||||
steps:
|
||||
- attach_workspace:
|
||||
@@ -420,9 +420,9 @@ workflows:
|
||||
- setup:
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- master
|
||||
- /^release-.*/
|
||||
ignore:
|
||||
- master
|
||||
- /^release-.*/
|
||||
- check-deps:
|
||||
context: sast-webhook
|
||||
requires:
|
||||
|
||||
9
Makefile
9
Makefile
@@ -49,6 +49,11 @@ else
|
||||
BUILD_CLIENT = false
|
||||
endif
|
||||
|
||||
# these variables are used by QA to override location of InProduct Notices
|
||||
NOTICES_JSON_URL ?= https://notices.mattermost.com/
|
||||
NOTICES_FETCH_SECS ?= 3600
|
||||
NOTICES_SKIP_CACHE ?= false
|
||||
|
||||
# Go Flags
|
||||
GOFLAGS ?= $(GOFLAGS:)
|
||||
# We need to export GOBIN to allow it to be set
|
||||
@@ -61,6 +66,10 @@ LDFLAGS += -X "github.com/mattermost/mattermost-server/v5/model.BuildDate=$(BUIL
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost-server/v5/model.BuildHash=$(BUILD_HASH)"
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost-server/v5/model.BuildHashEnterprise=$(BUILD_HASH_ENTERPRISE)"
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost-server/v5/model.BuildEnterpriseReady=$(BUILD_ENTERPRISE_READY)"
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost-server/v5/app.NOTICES_JSON_URL=$(NOTICES_JSON_URL)"
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost-server/v5/app.NOTICES_JSON_FETCH_FREQUENCY_SECONDS=$(NOTICES_FETCH_SECS)"
|
||||
LDFLAGS += -X "github.com/mattermost/mattermost-server/v5/app.NOTICES_SKIP_CACHE=$(NOTICES_SKIP_CACHE)"
|
||||
|
||||
GO_MAJOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f1)
|
||||
GO_MINOR_VERSION = $(shell $(GO) version | cut -c 14- | cut -d' ' -f1 | cut -d'.' -f2)
|
||||
MINIMUM_SUPPORTED_GO_MAJOR_VERSION = 1
|
||||
|
||||
69
NOTICE.txt
69
NOTICE.txt
@@ -4205,3 +4205,72 @@ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
---
|
||||
|
||||
## semver
|
||||
|
||||
This product contains 'semver' by Masterminds.
|
||||
|
||||
The semver package provides the ability to work with Semantic Versions in Go.
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/Masterminds/semver
|
||||
|
||||
* LICENSE:
|
||||
|
||||
Copyright (C) 2014-2019, Matt Butcher and Matt Farina
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
## Date Constraints
|
||||
|
||||
This product contains 'dateconstraints' by Eli Yukelzon.
|
||||
|
||||
Go library to validate a date against constraints
|
||||
|
||||
* HOMEPAGE:
|
||||
* https://github.com/reflog/dateconstraints
|
||||
|
||||
* LICENSE:
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 Eli Yukelzon
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
||||
@@ -84,6 +84,8 @@ func setupTestHelper(dbStore store.Store, searchEngine *searchengine.Broker, ent
|
||||
*config.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp")
|
||||
config.ServiceSettings.EnableLocalMode = model.NewBool(true)
|
||||
*config.ServiceSettings.LocalModeSocketLocation = filepath.Join(tempWorkspace, "mattermost_local.sock")
|
||||
*config.AnnouncementSettings.AdminNoticesEnabled = false
|
||||
*config.AnnouncementSettings.UserNoticesEnabled = false
|
||||
if updateConfig != nil {
|
||||
updateConfig(config)
|
||||
}
|
||||
|
||||
@@ -62,6 +62,9 @@ func (api *API) InitSystem() {
|
||||
api.BaseRoutes.ApiRoot.Handle("/restart", api.ApiSessionRequired(restart)).Methods("POST")
|
||||
api.BaseRoutes.ApiRoot.Handle("/warn_metrics/status", api.ApiSessionRequired(getWarnMetricsStatus)).Methods("GET")
|
||||
api.BaseRoutes.ApiRoot.Handle("/warn_metrics/ack/{warn_metric_id:[A-Za-z0-9-_]+}", api.ApiHandler(sendWarnMetricAckEmail)).Methods("POST")
|
||||
|
||||
api.BaseRoutes.System.Handle("/notices/{team_id:[A-Za-z0-9]+}", api.ApiSessionRequired(getProductNotices)).Methods("GET")
|
||||
api.BaseRoutes.System.Handle("/notices/view", api.ApiSessionRequired(updateViewedProductNotices)).Methods("PUT")
|
||||
}
|
||||
|
||||
func getSystemPing(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
@@ -727,3 +730,42 @@ func sendWarnMetricAckEmail(c *Context, w http.ResponseWriter, r *http.Request)
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func getProductNotices(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.RequireTeamId()
|
||||
if c.Err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
client, parseError := model.NoticeClientTypeFromString(r.URL.Query().Get("client"))
|
||||
if parseError != nil {
|
||||
c.SetInvalidParam("client")
|
||||
return
|
||||
}
|
||||
clientVersion := r.URL.Query().Get("clientVersion")
|
||||
locale := r.URL.Query().Get("locale")
|
||||
|
||||
notices, err := c.App.GetProductNotices(c.App.Session().UserId, c.Params.TeamId, client, clientVersion, locale)
|
||||
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
result, _ := notices.Marshal()
|
||||
_, _ = w.Write(result)
|
||||
}
|
||||
|
||||
func updateViewedProductNotices(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
auditRec := c.MakeAuditRecord("updateViewedProductNotices", audit.Fail)
|
||||
defer c.LogAuditRec(auditRec)
|
||||
c.LogAudit("attempt")
|
||||
|
||||
ids := model.ArrayFromJson(r.Body)
|
||||
err := c.App.UpdateViewedProductNotices(c.App.Session().UserId, ids)
|
||||
if err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
auditRec.Success()
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
@@ -111,6 +111,10 @@ func (a *App) initJobs() {
|
||||
if jobsExpiryNotifyInterface != nil {
|
||||
a.srv.Jobs.ExpiryNotify = jobsExpiryNotifyInterface(a)
|
||||
}
|
||||
if productNoticesJobInterface != nil {
|
||||
a.srv.Jobs.ProductNotices = productNoticesJobInterface(a)
|
||||
}
|
||||
|
||||
if jobsActiveUsersInterface != nil {
|
||||
a.srv.Jobs.ActiveUsers = jobsActiveUsersInterface(a)
|
||||
}
|
||||
|
||||
@@ -627,6 +627,7 @@ type AppIface interface {
|
||||
GetPreferencesForUser(userId string) (model.Preferences, *model.AppError)
|
||||
GetPrevPostIdFromPostList(postList *model.PostList) string
|
||||
GetPrivateChannelsForTeam(teamId string, offset int, limit int) (*model.ChannelList, *model.AppError)
|
||||
GetProductNotices(userId, teamId string, client model.NoticeClientType, clientVersion string, locale string) (model.NoticeMessages, *model.AppError)
|
||||
GetProfileImage(user *model.User) ([]byte, bool, *model.AppError)
|
||||
GetPublicChannelsByIdsForTeam(teamId string, channelIds []string) (*model.ChannelList, *model.AppError)
|
||||
GetPublicChannelsForTeam(teamId string, offset int, limit int) (*model.ChannelList, *model.AppError)
|
||||
@@ -969,6 +970,7 @@ type AppIface interface {
|
||||
UpdatePasswordSendEmail(user *model.User, newPassword, method string) *model.AppError
|
||||
UpdatePost(post *model.Post, safeUpdate bool) (*model.Post, *model.AppError)
|
||||
UpdatePreferences(userId string, preferences model.Preferences) *model.AppError
|
||||
UpdateProductNotices() *model.AppError
|
||||
UpdateRole(role *model.Role) (*model.Role, *model.AppError)
|
||||
UpdateScheme(scheme *model.Scheme) (*model.Scheme, *model.AppError)
|
||||
UpdateSessionsIsGuest(userId string, isGuest bool)
|
||||
@@ -985,6 +987,7 @@ type AppIface interface {
|
||||
UpdateUserAuth(userId string, userAuth *model.UserAuth) (*model.UserAuth, *model.AppError)
|
||||
UpdateUserNotifyProps(userId string, props map[string]string) (*model.User, *model.AppError)
|
||||
UpdateUserRoles(userId string, newRoles string, sendWebSocketEvent bool) (*model.User, *model.AppError)
|
||||
UpdateViewedProductNotices(userId string, noticeIds []string) *model.AppError
|
||||
UploadData(us *model.UploadSession, rd io.Reader) (*model.FileInfo, *model.AppError)
|
||||
UploadEmojiImage(id string, imageData *multipart.FileHeader) *model.AppError
|
||||
UploadMultipartFiles(teamId string, channelId string, userId string, fileHeaders []*multipart.FileHeader, clientIds []string, now time.Time) (*model.FileUploadResponse, *model.AppError)
|
||||
|
||||
@@ -102,6 +102,12 @@ func RegisterJobsExpiryNotifyJobInterface(f func(*App) tjobs.ExpiryNotifyJobInte
|
||||
jobsExpiryNotifyInterface = f
|
||||
}
|
||||
|
||||
var productNoticesJobInterface func(*App) tjobs.ProductNoticesJobInterface
|
||||
|
||||
func RegisterProductNoticesJobInterface(f func(*App) tjobs.ProductNoticesJobInterface) {
|
||||
productNoticesJobInterface = f
|
||||
}
|
||||
|
||||
var ldapInterface func(*App) einterfaces.LdapInterface
|
||||
|
||||
func RegisterLdapInterface(f func(*App) einterfaces.LdapInterface) {
|
||||
|
||||
@@ -60,6 +60,8 @@ func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer boo
|
||||
*config.PluginSettings.Directory = filepath.Join(tempWorkspace, "plugins")
|
||||
*config.PluginSettings.ClientDirectory = filepath.Join(tempWorkspace, "webapp")
|
||||
*config.LogSettings.EnableSentry = false // disable error reporting during tests
|
||||
*config.AnnouncementSettings.AdminNoticesEnabled = false
|
||||
*config.AnnouncementSettings.UserNoticesEnabled = false
|
||||
memoryStore.Set(config)
|
||||
|
||||
buffer := &bytes.Buffer{}
|
||||
|
||||
@@ -7161,6 +7161,28 @@ func (a *OpenTracingAppLayer) GetPrivateChannelsForTeam(teamId string, offset in
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetProductNotices(userId string, teamId string, client model.NoticeClientType, clientVersion string, locale string) (model.NoticeMessages, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetProductNotices")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store.SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0, resultVar1 := a.app.GetProductNotices(userId, teamId, client, clientVersion, locale)
|
||||
|
||||
if resultVar1 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar1))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) GetProfileImage(user *model.User) ([]byte, bool, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.GetProfileImage")
|
||||
@@ -14747,6 +14769,28 @@ func (a *OpenTracingAppLayer) UpdatePreferences(userId string, preferences model
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateProductNotices() *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateProductNotices")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store.SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0 := a.app.UpdateProductNotices()
|
||||
|
||||
if resultVar0 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar0))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateRole(role *model.Role) (*model.Role, *model.AppError) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateRole")
|
||||
@@ -15092,6 +15136,28 @@ func (a *OpenTracingAppLayer) UpdateUserRoles(userId string, newRoles string, se
|
||||
return resultVar0, resultVar1
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateViewedProductNotices(userId string, noticeIds []string) *model.AppError {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateViewedProductNotices")
|
||||
|
||||
a.ctx = newCtx
|
||||
a.app.Srv().Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
a.app.Srv().Store.SetContext(origCtx)
|
||||
a.ctx = origCtx
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
resultVar0 := a.app.UpdateViewedProductNotices(userId, noticeIds)
|
||||
|
||||
if resultVar0 != nil {
|
||||
span.LogFields(spanlog.Error(resultVar0))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return resultVar0
|
||||
}
|
||||
|
||||
func (a *OpenTracingAppLayer) UpdateWebConnUserActivity(session model.Session, activityAt int64) {
|
||||
origCtx := a.ctx
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.UpdateWebConnUserActivity")
|
||||
|
||||
300
app/product_notices.go
Обычный файл
300
app/product_notices.go
Обычный файл
@@ -0,0 +1,300 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v5/store"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/Masterminds/semver/v3"
|
||||
"github.com/mattermost/mattermost-server/v5/config"
|
||||
"github.com/mattermost/mattermost-server/v5/mlog"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/utils"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/reflog/dateconstraints"
|
||||
)
|
||||
|
||||
const MAX_REPEAT_VIEWINGS = 3
|
||||
const MIN_SECONDS_BETWEEN_REPEAT_VIEWINGS = 60 * 60
|
||||
|
||||
// where to fetch notices from. setting as var to allow overriding during build/test
|
||||
var NOTICES_JSON_URL = "https://notices.mattermost.com/"
|
||||
|
||||
// notice.json fetch frequency in seconds. setting as var to allow overriding during build/test
|
||||
var NOTICES_JSON_FETCH_FREQUENCY_SECONDS = "3600" // one hour by default
|
||||
|
||||
// this variable can be set during build time for QA to skip caching JSON responses (to avoid CDN delay)
|
||||
var NOTICES_SKIP_CACHE = "false"
|
||||
|
||||
// http request cache
|
||||
var noticesCache = utils.RequestCache{}
|
||||
|
||||
// cached counts that are used during notice condition validation
|
||||
var cachedPostCount int64
|
||||
var cachedUserCount int64
|
||||
|
||||
// previously fetched notices
|
||||
var cachedNotices model.ProductNotices
|
||||
|
||||
func noticeMatchesConditions(config *model.Config, preferences store.PreferenceStore, userId string, client model.NoticeClientType, clientVersion, locale string, postCount, userCount int64, isSystemAdmin, isTeamAdmin bool, isCloud bool, sku string, notice *model.ProductNotice) (bool, error) {
|
||||
cnd := notice.Conditions
|
||||
|
||||
// check client type
|
||||
if cnd.ClientType != nil {
|
||||
if !cnd.ClientType.Matches(client) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check if client version is in notice range
|
||||
clientVersions := cnd.DesktopVersion
|
||||
if client == model.NoticeClientType_MobileAndroid || client == model.NoticeClientType_MobileIos {
|
||||
clientVersions = cnd.MobileVersion
|
||||
}
|
||||
|
||||
clientVersionParsed, err := semver.NewVersion(clientVersion)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "Cannot parse version range %s", clientVersion)
|
||||
}
|
||||
|
||||
for _, v := range clientVersions {
|
||||
c, err := semver.NewConstraint(v)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "Cannot parse version range %s", v)
|
||||
}
|
||||
if !c.Check(clientVersionParsed) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check if notice date range matches current
|
||||
if cnd.DisplayDate != nil {
|
||||
now := time.Now().UTC()
|
||||
c, err := date_constraints.NewConstraint(*cnd.DisplayDate)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "Cannot parse date range %s", *cnd.DisplayDate)
|
||||
}
|
||||
if !c.Check(&now) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check if current server version is notice range
|
||||
serverVersion, _ := semver.NewVersion(model.BuildNumber)
|
||||
for _, v := range cnd.ServerVersion {
|
||||
c, err := semver.NewConstraint(v)
|
||||
if err != nil {
|
||||
return false, errors.Wrapf(err, "Cannot parse version range %s", v)
|
||||
}
|
||||
if !c.Check(serverVersion) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check if sku matches our license
|
||||
if cnd.Sku != nil {
|
||||
if !cnd.Sku.Matches(sku) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check the target audience
|
||||
if cnd.Audience != nil {
|
||||
if !cnd.Audience.Matches(isSystemAdmin, isTeamAdmin) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// check user count condition against previously calculated total user count
|
||||
if cnd.NumberOfUsers != nil && userCount > 0 {
|
||||
if userCount < *cnd.NumberOfUsers {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check post count condition against previously calculated total post count
|
||||
if cnd.NumberOfPosts != nil && postCount > 0 {
|
||||
if postCount < *cnd.NumberOfPosts {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check if our server config matches the notice
|
||||
for k, v := range cnd.ServerConfig {
|
||||
if !validateConfigEntry(config, k, v) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check if user's config matches the notice
|
||||
for k, v := range cnd.UserConfig {
|
||||
res, err := validateUserConfigEntry(preferences, userId, k, v)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !res {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// check the type of installation
|
||||
if cnd.InstanceType != nil {
|
||||
if !cnd.InstanceType.Matches(isCloud) {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func validateUserConfigEntry(preferences store.PreferenceStore, userId string, key string, expectedValue interface{}) (bool, error) {
|
||||
parts := strings.Split(key, ".")
|
||||
if len(parts) != 2 {
|
||||
return false, errors.New("Invalid format of user config. Must be in form of Category.SettingName")
|
||||
}
|
||||
if _, ok := expectedValue.(string); !ok {
|
||||
return false, errors.New("Invalid format of user config. Value should be string")
|
||||
}
|
||||
pref, err := preferences.Get(userId, parts[0], parts[1])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return pref.Value == expectedValue, nil
|
||||
}
|
||||
|
||||
func validateConfigEntry(conf *model.Config, path string, expectedValue interface{}) bool {
|
||||
value, found := config.GetValueByPath(strings.Split(path, "."), *conf)
|
||||
if !found {
|
||||
return false
|
||||
}
|
||||
vt := reflect.ValueOf(value)
|
||||
if vt.IsNil() {
|
||||
return expectedValue == nil
|
||||
}
|
||||
if vt.Kind() == reflect.Ptr {
|
||||
vt = vt.Elem()
|
||||
}
|
||||
val := vt.Interface()
|
||||
return val == expectedValue
|
||||
}
|
||||
|
||||
func (a *App) GetProductNotices(userId, teamId string, client model.NoticeClientType, clientVersion string, locale string) (model.NoticeMessages, *model.AppError) {
|
||||
isSystemAdmin := a.SessionHasPermissionTo(*a.Session(), model.PERMISSION_MANAGE_SYSTEM)
|
||||
isTeamAdmin := a.SessionHasPermissionToTeam(*a.Session(), teamId, model.PERMISSION_MANAGE_TEAM)
|
||||
|
||||
// check if notices for regular users are disabled
|
||||
if !*a.Srv().Config().AnnouncementSettings.UserNoticesEnabled && !isTeamAdmin && !isSystemAdmin {
|
||||
return []model.NoticeMessage{}, nil
|
||||
}
|
||||
|
||||
// check if notices for admins are disabled
|
||||
if !*a.Srv().Config().AnnouncementSettings.AdminNoticesEnabled && (isTeamAdmin || isSystemAdmin) {
|
||||
return []model.NoticeMessage{}, nil
|
||||
}
|
||||
|
||||
views, err := a.Srv().Store.ProductNotices().GetViews(userId)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetProductNotices", "api.system.update_viewed_notices.failed", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
sku := a.Srv().ClientLicense()["SkuShortName"]
|
||||
isCloud := a.Srv().ClientLicense()["Cloud"] != ""
|
||||
|
||||
filteredNotices := make([]model.NoticeMessage, 0)
|
||||
|
||||
for noticeIndex, notice := range cachedNotices {
|
||||
// check if the notice has been viewed already
|
||||
var view *model.ProductNoticeViewState
|
||||
for viewIndex, v := range views {
|
||||
if v.NoticeId == notice.ID {
|
||||
view = &views[viewIndex]
|
||||
break
|
||||
}
|
||||
}
|
||||
if view != nil {
|
||||
repeatable := notice.Repeatable != nil && *notice.Repeatable
|
||||
if repeatable {
|
||||
if view.Viewed > MAX_REPEAT_VIEWINGS {
|
||||
continue
|
||||
}
|
||||
if (time.Now().UTC().Unix() - view.Timestamp) < MIN_SECONDS_BETWEEN_REPEAT_VIEWINGS {
|
||||
continue
|
||||
}
|
||||
} else if view.Viewed > 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
result, err := noticeMatchesConditions(a.Config(), a.Srv().Store.Preference(),
|
||||
userId,
|
||||
client,
|
||||
clientVersion,
|
||||
locale,
|
||||
cachedPostCount,
|
||||
cachedUserCount,
|
||||
isSystemAdmin,
|
||||
isTeamAdmin,
|
||||
isCloud,
|
||||
sku,
|
||||
&cachedNotices[noticeIndex])
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetProductNotices", "api.system.update_notices.validating_failed", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
if result {
|
||||
selectedLocale := "en"
|
||||
filteredNotices = append(filteredNotices, model.NoticeMessage{
|
||||
NoticeMessageInternal: notice.LocalizedMessages[selectedLocale],
|
||||
ID: notice.ID,
|
||||
TeamAdminOnly: notice.TeamAdminOnly(),
|
||||
SysAdminOnly: notice.SysAdminOnly(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return filteredNotices, nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateViewedProductNotices(userId string, noticeIds []string) *model.AppError {
|
||||
if err := a.Srv().Store.ProductNotices().View(userId, noticeIds); err != nil {
|
||||
return model.NewAppError("UpdateViewedProductNotices", "api.system.update_viewed_notices.failed", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateProductNotices() *model.AppError {
|
||||
skip, err := strconv.ParseBool(NOTICES_SKIP_CACHE)
|
||||
if err != nil {
|
||||
skip = false
|
||||
}
|
||||
mlog.Debug("Will fetch notices from", mlog.String("url", NOTICES_JSON_URL), mlog.Bool("skip_cache", skip))
|
||||
var appErr *model.AppError
|
||||
cachedPostCount, appErr = a.Srv().Store.Post().AnalyticsPostCount("", false, false)
|
||||
if appErr != nil {
|
||||
mlog.Error("Failed to fetch post count", mlog.String("error", appErr.Error()))
|
||||
}
|
||||
|
||||
cachedUserCount, appErr = a.Srv().Store.User().Count(model.UserCountOptions{IncludeDeleted: true})
|
||||
if appErr != nil {
|
||||
mlog.Error("Failed to fetch user count", mlog.String("error", appErr.Error()))
|
||||
}
|
||||
|
||||
data, err := utils.GetUrlWithCache(NOTICES_JSON_URL, ¬icesCache, skip)
|
||||
if err != nil {
|
||||
return model.NewAppError("UpdateProductNotices", "api.system.update_notices.fetch_failed", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
cachedNotices, err = model.UnmarshalProductNotices(data)
|
||||
if err != nil {
|
||||
return model.NewAppError("UpdateProductNotices", "api.system.update_notices.parse_failed", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if err := a.Srv().Store.ProductNotices().ClearOldNotices(&cachedNotices); err != nil {
|
||||
return model.NewAppError("UpdateProductNotices", "api.system.update_notices.clear_failed", nil, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
536
app/product_notices_test.go
Обычный файл
536
app/product_notices_test.go
Обычный файл
@@ -0,0 +1,536 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/store/storetest/mocks"
|
||||
"github.com/stretchr/testify/require"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNoticeValidation(t *testing.T) {
|
||||
th := SetupWithStoreMock(t)
|
||||
mockStore := th.App.Srv().Store.(*mocks.Store)
|
||||
mockRoleStore := mocks.RoleStore{}
|
||||
mockSystemStore := mocks.SystemStore{}
|
||||
mockUserStore := mocks.UserStore{}
|
||||
mockPostStore := mocks.PostStore{}
|
||||
mockPreferenceStore := mocks.PreferenceStore{}
|
||||
mockStore.On("Role").Return(&mockRoleStore)
|
||||
mockStore.On("System").Return(&mockSystemStore)
|
||||
mockStore.On("User").Return(&mockUserStore)
|
||||
mockStore.On("Post").Return(&mockPostStore)
|
||||
mockStore.On("Preference").Return(&mockPreferenceStore)
|
||||
mockSystemStore.On("SaveOrUpdate", &model.System{Name: "ActiveLicenseId", Value: ""}).Return(nil)
|
||||
mockSystemStore.On("GetByName", "UpgradedFromTE").Return(&model.System{Name: "UpgradedFromTE", Value: "false"}, nil)
|
||||
mockSystemStore.On("GetByName", "InstallationDate").Return(&model.System{Name: "InstallationDate", Value: "10"}, nil)
|
||||
mockSystemStore.On("GetByName", "FirstServerRunTimestamp").Return(&model.System{Name: "FirstServerRunTimestamp", Value: "10"}, nil)
|
||||
mockSystemStore.On("Get").Return(make(model.StringMap), nil)
|
||||
|
||||
mockUserStore.On("Count", model.UserCountOptions{IncludeBotAccounts: false, IncludeDeleted: true, ExcludeRegularUsers: false, TeamId: "", ChannelId: "", ViewRestrictions: (*model.ViewUsersRestrictions)(nil), Roles: []string(nil), ChannelRoles: []string(nil), TeamRoles: []string(nil)}).Return(int64(1), nil)
|
||||
mockPreferenceStore.On("Get", "test", "Stuff", "Data").Return(&model.Preference{Value: "test2"}, nil)
|
||||
mockPreferenceStore.On("Get", "test", "Stuff", "Data2").Return(&model.Preference{Value: "test"}, nil)
|
||||
mockPostStore.On("GetMaxPostSize").Return(65535, nil)
|
||||
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.AnnouncementSettings.AdminNoticesEnabled = true
|
||||
*cfg.AnnouncementSettings.UserNoticesEnabled = true
|
||||
})
|
||||
|
||||
defer th.TearDown()
|
||||
|
||||
type args struct {
|
||||
client model.NoticeClientType
|
||||
clientVersion string
|
||||
locale string
|
||||
sku string
|
||||
postCount, userCount int64
|
||||
cloud bool
|
||||
teamAdmin bool
|
||||
systemAdmin bool
|
||||
serverVersion string
|
||||
notice *model.ProductNotice
|
||||
}
|
||||
messages := map[string]model.NoticeMessageInternal{
|
||||
"en": {
|
||||
Description: "descr",
|
||||
Title: "title",
|
||||
},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantErr bool
|
||||
wantOk bool
|
||||
}{
|
||||
{
|
||||
name: "general notice",
|
||||
args: args{
|
||||
client: "mobile",
|
||||
clientVersion: "1.2.3",
|
||||
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{},
|
||||
ID: "123",
|
||||
LocalizedMessages: messages,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "mobile notice",
|
||||
args: args{
|
||||
client: "desktop",
|
||||
clientVersion: "1.2.3",
|
||||
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
ClientType: model.NewNoticeClientType(model.NoticeClientType_Mobile),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with config check",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
ServerConfig: map[string]interface{}{"ServiceSettings.LetsEncryptCertificateCacheFile": "./config/letsencrypt.cache"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with failing config check",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
ServerConfig: map[string]interface{}{"ServiceSettings.ZZ": "test"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with failing user check due to bad format",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
UserConfig: map[string]interface{}{"Stuff": "test"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with failing user check due to mismatch",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
UserConfig: map[string]interface{}{"Stuff.Data": "test"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with working user check",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
UserConfig: map[string]interface{}{"Stuff.Data2": "test"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with server version check",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
ServerVersion: []string{"> 4.0.0 < 99.0.0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with server version check that doesn't match",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
ServerVersion: []string{"> 99.0.0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with server version check that matches a const",
|
||||
args: args{
|
||||
serverVersion: "99.1.1",
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
ServerVersion: []string{"> 99.0.0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with server version check that is invalid",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
ServerVersion: []string{"99.0.0 + 1.0.0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with user count",
|
||||
args: args{
|
||||
userCount: 300,
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
NumberOfUsers: model.NewInt64(400),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with good user count and bad post count",
|
||||
args: args{
|
||||
userCount: 500,
|
||||
postCount: 2000,
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
NumberOfUsers: model.NewInt64(400),
|
||||
NumberOfPosts: model.NewInt64(3000),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with date check",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
DisplayDate: model.NewString("> 2000-03-01T00:00:00Z <= 2999-04-01T00:00:00Z"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
|
||||
{
|
||||
name: "notice with date check that doesn't match",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
DisplayDate: model.NewString("> 2999-03-01T00:00:00Z <= 3000-04-01T00:00:00Z"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with bad date check",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
DisplayDate: model.NewString("> 2000 -03-01T00:00:00Z <= 2999-04-01T00:00:00Z"),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with audience check (admin)",
|
||||
args: args{
|
||||
systemAdmin: true,
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Audience: model.NewNoticeAudience(model.NoticeAudience_Sysadmin),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with failing audience check (admin)",
|
||||
args: args{
|
||||
systemAdmin: false,
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Audience: model.NewNoticeAudience(model.NoticeAudience_Sysadmin),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with audience check (team)",
|
||||
args: args{
|
||||
teamAdmin: true,
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Audience: model.NewNoticeAudience(model.NoticeAudience_TeamAdmin),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with failing audience check (team)",
|
||||
args: args{
|
||||
teamAdmin: false,
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Audience: model.NewNoticeAudience(model.NoticeAudience_TeamAdmin),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with audience check (member)",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Audience: model.NewNoticeAudience(model.NoticeAudience_Member),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with failing audience check (member)",
|
||||
args: args{
|
||||
systemAdmin: true,
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Audience: model.NewNoticeAudience(model.NoticeAudience_Member),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with correct sku",
|
||||
args: args{
|
||||
sku: "e20",
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Sku: model.NewNoticeSKU(model.NoticeSKU_E20),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with incorrect sku",
|
||||
args: args{
|
||||
sku: "e20",
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Sku: model.NewNoticeSKU(model.NoticeSKU_E10),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: false,
|
||||
},
|
||||
{
|
||||
name: "notice with team sku",
|
||||
args: args{
|
||||
sku: "",
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Sku: model.NewNoticeSKU(model.NoticeSKU_Team),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with sku check for all",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
Sku: model.NewNoticeSKU(model.NoticeSKU_All),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with instance check cloud",
|
||||
args: args{
|
||||
cloud: true,
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
InstanceType: model.NewNoticeInstanceType(model.NoticeInstanceType_Cloud),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
{
|
||||
name: "notice with instance check both",
|
||||
args: args{
|
||||
notice: &model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
InstanceType: model.NewNoticeInstanceType(model.NoticeInstanceType_Both),
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantOk: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
clientVersion := tt.args.clientVersion
|
||||
if clientVersion == "" {
|
||||
clientVersion = "1.2.3"
|
||||
}
|
||||
model.BuildNumber = tt.args.serverVersion
|
||||
if model.BuildNumber == "" {
|
||||
model.BuildNumber = "5.26.1"
|
||||
}
|
||||
if ok, err := noticeMatchesConditions(th.App.Config(), th.App.Srv().Store.Preference(), "test", tt.args.client, clientVersion, tt.args.locale, tt.args.postCount, tt.args.userCount, tt.args.systemAdmin, tt.args.teamAdmin, tt.args.cloud, tt.args.sku, tt.args.notice); (err != nil) != tt.wantErr {
|
||||
t.Errorf("noticeMatchesConditions() error = %v, wantErr %v", err, tt.wantErr)
|
||||
} else if ok != tt.wantOk {
|
||||
t.Errorf("noticeMatchesConditions() result = %v, wantOk %v", ok, tt.wantOk)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNoticeFetch(t *testing.T) {
|
||||
th := Setup(t).InitBasic()
|
||||
defer th.TearDown()
|
||||
th.App.UpdateConfig(func(cfg *model.Config) {
|
||||
*cfg.AnnouncementSettings.AdminNoticesEnabled = true
|
||||
*cfg.AnnouncementSettings.UserNoticesEnabled = true
|
||||
})
|
||||
|
||||
notices := model.ProductNotices{model.ProductNotice{
|
||||
Conditions: model.Conditions{},
|
||||
ID: "123",
|
||||
LocalizedMessages: map[string]model.NoticeMessageInternal{
|
||||
"en": {
|
||||
Description: "description",
|
||||
Title: "title",
|
||||
},
|
||||
},
|
||||
Repeatable: nil,
|
||||
}}
|
||||
noticesBytes, appErr := notices.Marshal()
|
||||
require.NoError(t, appErr)
|
||||
|
||||
notices2 := model.ProductNotices{model.ProductNotice{
|
||||
Conditions: model.Conditions{
|
||||
NumberOfPosts: model.NewInt64(99999),
|
||||
},
|
||||
ID: "333",
|
||||
LocalizedMessages: map[string]model.NoticeMessageInternal{
|
||||
"en": {
|
||||
Description: "description",
|
||||
Title: "title",
|
||||
},
|
||||
},
|
||||
Repeatable: nil,
|
||||
}}
|
||||
noticesBytes2, appErr := notices2.Marshal()
|
||||
require.NoError(t, appErr)
|
||||
server1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasSuffix(r.URL.Path, "notices.json") {
|
||||
w.Write(noticesBytes)
|
||||
} else {
|
||||
w.Write(noticesBytes2)
|
||||
}
|
||||
}))
|
||||
defer server1.Close()
|
||||
|
||||
NOTICES_JSON_URL = fmt.Sprintf("http://%s/notices.json", server1.Listener.Addr().String())
|
||||
|
||||
// fetch fake notices
|
||||
appErr = th.App.UpdateProductNotices()
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// get them for specified user
|
||||
messages, appErr := th.App.GetProductNotices(th.BasicUser.Id, th.BasicTeam.Id, model.NoticeClientType_All, "1.2.3", "en")
|
||||
require.Nil(t, appErr)
|
||||
require.Len(t, messages, 1)
|
||||
|
||||
// mark notices as viewed
|
||||
appErr = th.App.UpdateViewedProductNotices(th.BasicUser.Id, []string{messages[0].ID})
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// get them again, see that none are returned
|
||||
messages, appErr = th.App.GetProductNotices(th.BasicUser.Id, th.BasicTeam.Id, model.NoticeClientType_All, "1.2.3", "en")
|
||||
require.Nil(t, appErr)
|
||||
require.Len(t, messages, 0)
|
||||
|
||||
// validate views table
|
||||
views, err := th.App.Srv().Store.ProductNotices().GetViews(th.BasicUser.Id)
|
||||
require.Nil(t, err)
|
||||
require.Len(t, views, 1)
|
||||
|
||||
// fetch another set
|
||||
NOTICES_JSON_URL = fmt.Sprintf("http://%s/notices2.json", server1.Listener.Addr().String())
|
||||
|
||||
// fetch fake notices
|
||||
appErr = th.App.UpdateProductNotices()
|
||||
require.Nil(t, appErr)
|
||||
|
||||
// get them again, since conditions don't match we should be zero
|
||||
messages, appErr = th.App.GetProductNotices(th.BasicUser.Id, th.BasicTeam.Id, model.NoticeClientType_All, "1.2.3", "en")
|
||||
require.Nil(t, appErr)
|
||||
require.Len(t, messages, 0)
|
||||
|
||||
// even though UpdateViewedProductNotices was called previously, the table should be empty, since there's cleanup done during UpdateProductNotices
|
||||
views, err = th.App.Srv().Store.ProductNotices().GetViews(th.BasicUser.Id)
|
||||
require.Nil(t, err)
|
||||
require.Len(t, views, 0)
|
||||
}
|
||||
@@ -513,6 +513,11 @@ func NewServer(options ...Option) (*Server, error) {
|
||||
s.searchConfigListenerId = searchConfigListenerId
|
||||
s.searchLicenseListenerId = searchLicenseListenerId
|
||||
|
||||
// if enabled - perform initial product notices fetch
|
||||
if *s.Config().AnnouncementSettings.AdminNoticesEnabled || *s.Config().AnnouncementSettings.UserNoticesEnabled {
|
||||
go fakeApp.UpdateProductNotices()
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/mlog"
|
||||
@@ -198,3 +199,48 @@ func JSONToLogTargetCfg(data []byte) (mlog.LogTargetCfg, error) {
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func GetValueByPath(path []string, obj interface{}) (interface{}, bool) {
|
||||
r := reflect.ValueOf(obj)
|
||||
var val reflect.Value
|
||||
if r.Kind() == reflect.Map {
|
||||
val = r.MapIndex(reflect.ValueOf(path[0]))
|
||||
if val.IsValid() {
|
||||
val = val.Elem()
|
||||
}
|
||||
} else {
|
||||
val = r.FieldByName(path[0])
|
||||
}
|
||||
|
||||
if !val.IsValid() {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(path) == 1:
|
||||
return val.Interface(), true
|
||||
case val.Kind() == reflect.Struct:
|
||||
return GetValueByPath(path[1:], val.Interface())
|
||||
case val.Kind() == reflect.Map:
|
||||
remainingPath := strings.Join(path[1:], ".")
|
||||
mapIter := val.MapRange()
|
||||
for mapIter.Next() {
|
||||
key := mapIter.Key().String()
|
||||
if strings.HasPrefix(remainingPath, key) {
|
||||
i := strings.Count(key, ".") + 2 // number of dots + a dot on each side
|
||||
mapVal := mapIter.Value()
|
||||
// if no sub field path specified, return the object
|
||||
if len(path[i:]) == 0 {
|
||||
return mapVal.Interface(), true
|
||||
}
|
||||
data := mapVal.Interface()
|
||||
if mapVal.Kind() == reflect.Ptr {
|
||||
data = mapVal.Elem().Interface() // if value is a pointer, dereference it
|
||||
}
|
||||
// pass subpath
|
||||
return GetValueByPath(path[i:], data)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
2
go.mod
2
go.mod
@@ -3,6 +3,7 @@ module github.com/mattermost/mattermost-server/v5
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/Masterminds/semver/v3 v3.1.0
|
||||
github.com/Masterminds/squirrel v1.4.0
|
||||
github.com/NYTimes/gziphandler v1.1.1
|
||||
github.com/RoaringBitmap/roaring v0.5.0 // indirect
|
||||
@@ -80,6 +81,7 @@ require (
|
||||
github.com/pelletier/go-toml v1.8.0 // indirect
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.7.1
|
||||
github.com/reflog/dateconstraints v0.2.1
|
||||
github.com/prometheus/client_model v0.2.0 // indirect
|
||||
github.com/prometheus/common v0.12.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 // indirect
|
||||
|
||||
8
go.sum
8
go.sum
@@ -22,7 +22,11 @@ github.com/Joker/hpp v1.0.0/go.mod h1:8x5n+M1Hp5hC0g8okX3sR3vFQwynaX/UgSOM9MeBKz
|
||||
github.com/Joker/jade v1.0.1-0.20190614124447-d475f43051e7/go.mod h1:6E6s8o2AE4KhCrqr6GRJjdC/gNfTdxkIXvuGZZda2VM=
|
||||
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
|
||||
github.com/Masterminds/glide v0.13.2/go.mod h1:STyF5vcenH/rUqTEv+/hBXlSTo7KYwg2oc2f4tzPWic=
|
||||
github.com/Masterminds/semver v1.4.2 h1:WBLTQ37jOCzSLtXNdoo8bNM8876KhNqOKvrlGITgsTc=
|
||||
github.com/Masterminds/semver v1.4.2/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y=
|
||||
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
|
||||
github.com/Masterminds/semver/v3 v3.1.0 h1:Y2lUDsFKVRSYGojLJ1yLxSXdMmMYTYls0rCvoqmMUQk=
|
||||
github.com/Masterminds/semver/v3 v3.1.0/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
|
||||
github.com/Masterminds/squirrel v1.4.0 h1:he5i/EXixZxrBUWcxzDYMiju9WZ3ld/l7QBNuo/eN3w=
|
||||
github.com/Masterminds/squirrel v1.4.0/go.mod h1:yaPeOnPG5ZRwL9oKdTsO/prlkPbXWZlRVMQ/gGlzIuA=
|
||||
github.com/Masterminds/vcs v1.13.0/go.mod h1:N09YCmOQr6RLxC6UNHzuVwAdodYbbnycGHSmwVJjcKA=
|
||||
@@ -707,6 +711,10 @@ github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4O
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/reflog/dateconstraints v0.2.0 h1:ixLrqJVRul/rsms7i3+/xBC1ybaFohl372jiq1/bLzI=
|
||||
github.com/reflog/dateconstraints v0.2.0/go.mod h1:Ax8AxTBcJc3E/oVS2hd2j7RDM/5MDtuPwuR7lIHtPLo=
|
||||
github.com/reflog/dateconstraints v0.2.1 h1:Hz1n2Q1vEm0Rj5gciDQcCN1iPBwfFjxUJy32NknGP/s=
|
||||
github.com/reflog/dateconstraints v0.2.1/go.mod h1:Ax8AxTBcJc3E/oVS2hd2j7RDM/5MDtuPwuR7lIHtPLo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237 h1:HQagqIiBmr8YXawX/le3+O26N+vPPC1PtjaF3mwnook=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
|
||||
20
i18n/en.json
20
i18n/en.json
@@ -2154,6 +2154,26 @@
|
||||
"id": "api.system.id_loaded.not_available.app_error",
|
||||
"translation": "ID Loaded Push Notifications are not configured or supported on this server."
|
||||
},
|
||||
{
|
||||
"id": "api.system.update_notices.clear_failed",
|
||||
"translation": "Clearing old product notices failed"
|
||||
},
|
||||
{
|
||||
"id": "api.system.update_notices.fetch_failed",
|
||||
"translation": "Fetching product notices failed"
|
||||
},
|
||||
{
|
||||
"id": "api.system.update_notices.parse_failed",
|
||||
"translation": "Parsing product notices failed"
|
||||
},
|
||||
{
|
||||
"id": "api.system.update_notices.validating_failed",
|
||||
"translation": "Validating product notice conditions failed"
|
||||
},
|
||||
{
|
||||
"id": "api.system.update_viewed_notices.failed",
|
||||
"translation": ""
|
||||
},
|
||||
{
|
||||
"id": "api.team.add_members.error",
|
||||
"translation": "Error adding team member(s)."
|
||||
|
||||
@@ -18,4 +18,7 @@ import (
|
||||
|
||||
// This is a placeholder so this package can be imported in Team Edition when it will be otherwise empty.
|
||||
_ "github.com/mattermost/mattermost-server/v5/jobs/active_users"
|
||||
|
||||
// This is a placeholder so this package can be imported in Team Edition when it will be otherwise empty.
|
||||
_ "github.com/mattermost/mattermost-server/v5/jobs/product_notices"
|
||||
)
|
||||
|
||||
11
jobs/interfaces/product_notices_interface.go
Обычный файл
11
jobs/interfaces/product_notices_interface.go
Обычный файл
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package interfaces
|
||||
|
||||
import "github.com/mattermost/mattermost-server/v5/model"
|
||||
|
||||
type ProductNoticesJobInterface interface {
|
||||
MakeWorker() model.Worker
|
||||
MakeScheduler() model.Scheduler
|
||||
}
|
||||
@@ -135,6 +135,13 @@ func (watcher *Watcher) PollAndNotify() {
|
||||
default:
|
||||
}
|
||||
}
|
||||
} else if job.Type == model.JOB_TYPE_PRODUCT_NOTICES {
|
||||
if watcher.workers.ProductNotices != nil {
|
||||
select {
|
||||
case watcher.workers.ProductNotices.JobChannel() <- *job:
|
||||
default:
|
||||
}
|
||||
}
|
||||
} else if job.Type == model.JOB_TYPE_ACTIVE_USERS {
|
||||
if watcher.workers.ActiveUsers != nil {
|
||||
select {
|
||||
|
||||
19
jobs/product_notices/product_notices.go
Обычный файл
19
jobs/product_notices/product_notices.go
Обычный файл
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package product_notices
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v5/app"
|
||||
tjobs "github.com/mattermost/mattermost-server/v5/jobs/interfaces"
|
||||
)
|
||||
|
||||
type ProductNoticesJobInterfaceImpl struct {
|
||||
App *app.App
|
||||
}
|
||||
|
||||
func init() {
|
||||
app.RegisterProductNoticesJobInterface(func(a *app.App) tjobs.ProductNoticesJobInterface {
|
||||
return &ProductNoticesJobInterfaceImpl{a}
|
||||
})
|
||||
}
|
||||
54
jobs/product_notices/scheduler.go
Обычный файл
54
jobs/product_notices/scheduler.go
Обычный файл
@@ -0,0 +1,54 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package product_notices
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v5/mlog"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/app"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
)
|
||||
|
||||
type Scheduler struct {
|
||||
App *app.App
|
||||
}
|
||||
|
||||
func (m *ProductNoticesJobInterfaceImpl) MakeScheduler() model.Scheduler {
|
||||
return &Scheduler{m.App}
|
||||
}
|
||||
|
||||
func (scheduler *Scheduler) Name() string {
|
||||
return JobName + "Scheduler"
|
||||
}
|
||||
|
||||
func (scheduler *Scheduler) JobType() string {
|
||||
return model.JOB_TYPE_PRODUCT_NOTICES
|
||||
}
|
||||
|
||||
func (scheduler *Scheduler) Enabled(cfg *model.Config) bool {
|
||||
// Only enabled when ExtendSessionLengthWithActivity is enabled.
|
||||
return *cfg.AnnouncementSettings.AdminNoticesEnabled || *cfg.AnnouncementSettings.UserNoticesEnabled
|
||||
}
|
||||
|
||||
func (scheduler *Scheduler) NextScheduleTime(cfg *model.Config, now time.Time, pendingJobs bool, lastSuccessfulJob *model.Job) *time.Time {
|
||||
freq, err := strconv.ParseInt(app.NOTICES_JSON_FETCH_FREQUENCY_SECONDS, 10, 32)
|
||||
if err != nil {
|
||||
mlog.Debug("Invalid NOTICES_JSON_FETCH_FREQUENCY_SECONDS variable provided!", mlog.String("value", app.NOTICES_JSON_FETCH_FREQUENCY_SECONDS))
|
||||
freq = 3600
|
||||
}
|
||||
nextTime := time.Now().Add(time.Duration(freq) * time.Second)
|
||||
return &nextTime
|
||||
}
|
||||
|
||||
func (scheduler *Scheduler) ScheduleJob(cfg *model.Config, pendingJobs bool, lastSuccessfulJob *model.Job) (*model.Job, *model.AppError) {
|
||||
data := map[string]string{}
|
||||
|
||||
if job, err := scheduler.App.Srv().Jobs.CreateJob(model.JOB_TYPE_PRODUCT_NOTICES, data); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
return job, nil
|
||||
}
|
||||
}
|
||||
100
jobs/product_notices/worker.go
Обычный файл
100
jobs/product_notices/worker.go
Обычный файл
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package product_notices
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v5/app"
|
||||
"github.com/mattermost/mattermost-server/v5/jobs"
|
||||
"github.com/mattermost/mattermost-server/v5/mlog"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
)
|
||||
|
||||
const (
|
||||
JobName = "ProductNotices"
|
||||
)
|
||||
|
||||
type Worker struct {
|
||||
name string
|
||||
stop chan bool
|
||||
stopped chan bool
|
||||
jobs chan model.Job
|
||||
jobServer *jobs.JobServer
|
||||
app *app.App
|
||||
}
|
||||
|
||||
func (m *ProductNoticesJobInterfaceImpl) MakeWorker() model.Worker {
|
||||
worker := Worker{
|
||||
name: JobName,
|
||||
stop: make(chan bool, 1),
|
||||
stopped: make(chan bool, 1),
|
||||
jobs: make(chan model.Job),
|
||||
jobServer: m.App.Srv().Jobs,
|
||||
app: m.App,
|
||||
}
|
||||
return &worker
|
||||
}
|
||||
|
||||
func (worker *Worker) Run() {
|
||||
mlog.Debug("Worker started", mlog.String("worker", worker.name))
|
||||
|
||||
defer func() {
|
||||
mlog.Debug("Worker finished", mlog.String("worker", worker.name))
|
||||
worker.stopped <- true
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-worker.stop:
|
||||
mlog.Debug("Worker received stop signal", mlog.String("worker", worker.name))
|
||||
return
|
||||
case job := <-worker.jobs:
|
||||
mlog.Debug("Worker received a new candidate job.", mlog.String("worker", worker.name))
|
||||
worker.DoJob(&job)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (worker *Worker) Stop() {
|
||||
mlog.Debug("Worker stopping", mlog.String("worker", worker.name))
|
||||
worker.stop <- true
|
||||
<-worker.stopped
|
||||
}
|
||||
|
||||
func (worker *Worker) JobChannel() chan<- model.Job {
|
||||
return worker.jobs
|
||||
}
|
||||
|
||||
func (worker *Worker) DoJob(job *model.Job) {
|
||||
if claimed, err := worker.jobServer.ClaimJob(job); err != nil {
|
||||
mlog.Warn("Worker experienced an error while trying to claim job",
|
||||
mlog.String("worker", worker.name),
|
||||
mlog.String("job_id", job.Id),
|
||||
mlog.String("error", err.Error()))
|
||||
return
|
||||
} else if !claimed {
|
||||
return
|
||||
}
|
||||
|
||||
if err := worker.app.UpdateProductNotices(); err != nil {
|
||||
mlog.Error("Worker: Failed to fetch product notices", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
|
||||
worker.setJobError(job, err)
|
||||
return
|
||||
}
|
||||
|
||||
mlog.Info("Worker: Job is complete", mlog.String("worker", worker.name), mlog.String("job_id", job.Id))
|
||||
worker.setJobSuccess(job)
|
||||
}
|
||||
|
||||
func (worker *Worker) setJobSuccess(job *model.Job) {
|
||||
if err := worker.app.Srv().Jobs.SetJobSuccess(job); err != nil {
|
||||
mlog.Error("Worker: Failed to set success for job", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
|
||||
worker.setJobError(job, err)
|
||||
}
|
||||
}
|
||||
|
||||
func (worker *Worker) setJobError(job *model.Job, appError *model.AppError) {
|
||||
if err := worker.app.Srv().Jobs.SetJobError(job, appError); err != nil {
|
||||
mlog.Error("Worker: Failed to set job error", mlog.String("worker", worker.name), mlog.String("job_id", job.Id), mlog.String("error", err.Error()))
|
||||
}
|
||||
}
|
||||
@@ -69,6 +69,10 @@ func (srv *JobServer) InitSchedulers() *Schedulers {
|
||||
if activeUsersInterface := srv.ActiveUsers; activeUsersInterface != nil {
|
||||
schedulers.schedulers = append(schedulers.schedulers, activeUsersInterface.MakeScheduler())
|
||||
}
|
||||
if productNoticesInterface := srv.ProductNotices; productNoticesInterface != nil {
|
||||
schedulers.schedulers = append(schedulers.schedulers, productNoticesInterface.MakeScheduler())
|
||||
}
|
||||
|
||||
schedulers.nextRunTimes = make([]*time.Time, len(schedulers.schedulers))
|
||||
return schedulers
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ type JobServer struct {
|
||||
Plugins tjobs.PluginsJobInterface
|
||||
BleveIndexer tjobs.IndexerJobInterface
|
||||
ExpiryNotify tjobs.ExpiryNotifyJobInterface
|
||||
ProductNotices tjobs.ProductNoticesJobInterface
|
||||
ActiveUsers tjobs.ActiveUsersJobInterface
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ type Workers struct {
|
||||
Plugins model.Worker
|
||||
BleveIndexing model.Worker
|
||||
ExpiryNotify model.Worker
|
||||
ProductNotices model.Worker
|
||||
ActiveUsers model.Worker
|
||||
|
||||
listenerId string
|
||||
@@ -75,6 +76,10 @@ func (srv *JobServer) InitWorkers() *Workers {
|
||||
if activeUsersInterface := srv.ActiveUsers; activeUsersInterface != nil {
|
||||
workers.ActiveUsers = activeUsersInterface.MakeWorker()
|
||||
}
|
||||
|
||||
if productNoticesInterface := srv.ProductNotices; productNoticesInterface != nil {
|
||||
workers.ProductNotices = productNoticesInterface.MakeWorker()
|
||||
}
|
||||
return workers
|
||||
}
|
||||
|
||||
@@ -122,6 +127,10 @@ func (workers *Workers) Start() *Workers {
|
||||
go workers.ActiveUsers.Run()
|
||||
}
|
||||
|
||||
if workers.ProductNotices != nil {
|
||||
go workers.ProductNotices.Run()
|
||||
}
|
||||
|
||||
go workers.Watcher.Start()
|
||||
})
|
||||
|
||||
@@ -226,6 +235,10 @@ func (workers *Workers) Stop() *Workers {
|
||||
if workers.ActiveUsers != nil {
|
||||
workers.ActiveUsers.Stop()
|
||||
}
|
||||
if workers.ProductNotices != nil {
|
||||
workers.ProductNotices.Stop()
|
||||
}
|
||||
|
||||
mlog.Info("Stopped workers")
|
||||
|
||||
return workers
|
||||
|
||||
@@ -5540,6 +5540,29 @@ func (c *Client4) CheckIntegrity() ([]IntegrityCheckResult, *Response) {
|
||||
return results, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client4) GetNotices(lastViewed int64, teamId string, client NoticeClientType, clientVersion, locale, etag string) (NoticeMessages, *Response) {
|
||||
url := fmt.Sprintf("/system/notices/%s?lastViewed=%d&client=%s&clientVersion=%s&locale=%s", teamId, lastViewed, client, clientVersion, locale)
|
||||
r, appErr := c.DoApiGet(url, etag)
|
||||
if appErr != nil {
|
||||
return nil, BuildErrorResponse(r, appErr)
|
||||
}
|
||||
defer closeBody(r)
|
||||
notices, err := UnmarshalProductNoticeMessages(r.Body)
|
||||
if err != nil {
|
||||
return nil, &Response{StatusCode: http.StatusBadRequest, Error: NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), http.StatusForbidden)}
|
||||
}
|
||||
return notices, BuildResponse(r)
|
||||
}
|
||||
|
||||
func (c *Client4) MarkNoticesViewed(ids []string) *Response {
|
||||
r, err := c.DoApiPut("/system/notices/view", ArrayToJson(ids))
|
||||
if err != nil {
|
||||
return BuildErrorResponse(r, err)
|
||||
}
|
||||
defer closeBody(r)
|
||||
return BuildResponse(r)
|
||||
}
|
||||
|
||||
// CreateUpload creates a new upload session.
|
||||
func (c *Client4) CreateUpload(us *UploadSession) (*UploadSession, *Response) {
|
||||
r, err := c.DoApiPost(c.GetUploadsRoute(), us.ToJson())
|
||||
|
||||
@@ -1665,6 +1665,8 @@ type AnnouncementSettings struct {
|
||||
BannerColor *string `access:"site"`
|
||||
BannerTextColor *string `access:"site"`
|
||||
AllowBannerDismissal *bool `access:"site"`
|
||||
AdminNoticesEnabled *bool `access:"site"`
|
||||
UserNoticesEnabled *bool `access:"site"`
|
||||
}
|
||||
|
||||
func (s *AnnouncementSettings) SetDefaults() {
|
||||
@@ -1687,6 +1689,14 @@ func (s *AnnouncementSettings) SetDefaults() {
|
||||
if s.AllowBannerDismissal == nil {
|
||||
s.AllowBannerDismissal = NewBool(true)
|
||||
}
|
||||
|
||||
if s.AdminNoticesEnabled == nil {
|
||||
s.AdminNoticesEnabled = NewBool(true)
|
||||
}
|
||||
|
||||
if s.UserNoticesEnabled == nil {
|
||||
s.UserNoticesEnabled = NewBool(true)
|
||||
}
|
||||
}
|
||||
|
||||
type ThemeSettings struct {
|
||||
|
||||
@@ -20,6 +20,7 @@ const (
|
||||
JOB_TYPE_MIGRATIONS = "migrations"
|
||||
JOB_TYPE_PLUGINS = "plugins"
|
||||
JOB_TYPE_EXPIRY_NOTIFY = "expiry_notify"
|
||||
JOB_TYPE_PRODUCT_NOTICES = "product_notices"
|
||||
JOB_TYPE_ACTIVE_USERS = "active_users"
|
||||
|
||||
JOB_STATUS_PENDING = "pending"
|
||||
@@ -61,6 +62,7 @@ func (j *Job) IsValid() *AppError {
|
||||
case JOB_TYPE_MESSAGE_EXPORT:
|
||||
case JOB_TYPE_MIGRATIONS:
|
||||
case JOB_TYPE_PLUGINS:
|
||||
case JOB_TYPE_PRODUCT_NOTICES:
|
||||
case JOB_TYPE_EXPIRY_NOTIFY:
|
||||
case JOB_TYPE_ACTIVE_USERS:
|
||||
default:
|
||||
|
||||
210
model/product_notices.go
Обычный файл
210
model/product_notices.go
Обычный файл
@@ -0,0 +1,210 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
type ProductNotices []ProductNotice
|
||||
|
||||
func (r *ProductNotices) Marshal() ([]byte, error) {
|
||||
return json.Marshal(r)
|
||||
}
|
||||
|
||||
func UnmarshalProductNotices(data []byte) (ProductNotices, error) {
|
||||
var r ProductNotices
|
||||
err := json.Unmarshal(data, &r)
|
||||
return r, err
|
||||
}
|
||||
|
||||
// List of product notices. Order is important and is used to resolve priorities.
|
||||
// Each notice will only be show if conditions are met.
|
||||
type ProductNotice struct {
|
||||
Conditions Conditions `json:"conditions"`
|
||||
ID string `json:"id"` // Unique identifier for this notice. Can be a running number. Used for storing 'viewed'; state on the server.
|
||||
LocalizedMessages map[string]NoticeMessageInternal `json:"localizedMessages"` // Notice message data, organized by locale.; Example:; "localizedMessages": {; "en": { "title": "English", description: "English description"},; "frFR": { "title": "Frances", description: "French description"}; }
|
||||
Repeatable *bool `json:"repeatable,omitempty"` // Configurable flag if the notice should reappear after it’s seen and dismissed
|
||||
}
|
||||
|
||||
func (n *ProductNotice) SysAdminOnly() bool {
|
||||
return n.Conditions.Audience != nil && *n.Conditions.Audience == NoticeAudience_Sysadmin
|
||||
}
|
||||
|
||||
func (n *ProductNotice) TeamAdminOnly() bool {
|
||||
return n.Conditions.Audience != nil && *n.Conditions.Audience == NoticeAudience_TeamAdmin
|
||||
}
|
||||
|
||||
type Conditions struct {
|
||||
Audience *NoticeAudience `json:"audience,omitempty"`
|
||||
ClientType *NoticeClientType `json:"clientType,omitempty"` // Only show the notice on specific clients. Defaults to 'all'
|
||||
DesktopVersion []string `json:"desktopVersion,omitempty"` // What desktop client versions does this notice apply to.; Format: semver ranges (https://devhints.io/semver); Example: [">=1.2.3 < ~2.4.x"]; Example: ["<v5.19", "v5.20-v5.22"]
|
||||
DisplayDate *string `json:"displayDate,omitempty"` // When to display the notice.; Examples:; "2020-03-01T00:00:00Z" - show on specified date; ">= 2020-03-01T00:00:00Z" - show after specified date; "< 2020-03-01T00:00:00Z" - show before the specified date; "> 2020-03-01T00:00:00Z <= 2020-04-01T00:00:00Z" - show only between the specified dates
|
||||
InstanceType *NoticeInstanceType `json:"instanceType,omitempty"`
|
||||
MobileVersion []string `json:"mobileVersion,omitempty"` // What mobile client versions does this notice apply to.; Format: semver ranges (https://devhints.io/semver); Example: [">=1.2.3 < ~2.4.x"]; Example: ["<v5.19", "v5.20-v5.22"]
|
||||
NumberOfPosts *int64 `json:"numberOfPosts,omitempty"` // Only show the notice when server has more than specified number of posts
|
||||
NumberOfUsers *int64 `json:"numberOfUsers,omitempty"` // Only show the notice when server has more than specified number of users
|
||||
ServerConfig map[string]interface{} `json:"serverConfig,omitempty"` // Map of mattermost server config paths and their values. Notice will be displayed only if; the values match the target server config; Example: serverConfig: { "PluginSettings.Enable": true, "GuestAccountsSettings.Enable":; false }
|
||||
ServerVersion []string `json:"serverVersion,omitempty"` // What server versions does this notice apply to.; Format: semver ranges (https://devhints.io/semver); Example: [">=1.2.3 < ~2.4.x"]; Example: ["<v5.19", "v5.20-v5.22"]
|
||||
Sku *NoticeSKU `json:"sku,omitempty"`
|
||||
UserConfig map[string]interface{} `json:"userConfig,omitempty"` // Map of user's settings and their values. Notice will be displayed only if the values; match the viewing users' config; Example: userConfig: { "new_sidebar.disabled": true }
|
||||
}
|
||||
|
||||
type NoticeMessageInternal struct {
|
||||
Action *NoticeAction `json:"action,omitempty"` // Optional action to perform on action button click. (defaults to closing the notice)
|
||||
ActionParam *string `json:"actionParam,omitempty"` // Optional action parameter.; Example: {"action": "url", actionParam: "/console/some-page"}
|
||||
ActionText *string `json:"actionText,omitempty"` // Optional override for the action button text (defaults to OK)
|
||||
Description string `json:"description"` // Notice content. Use {{Mattermost}} instead of plain text to support white-labeling. Text; supports Markdown.
|
||||
Image *string `json:"image,omitempty"`
|
||||
Title string `json:"title"` // Notice title. Use {{Mattermost}} instead of plain text to support white-labeling. Text; supports Markdown.
|
||||
}
|
||||
type NoticeMessages []NoticeMessage
|
||||
|
||||
type NoticeMessage struct {
|
||||
NoticeMessageInternal
|
||||
ID string `json:"id"`
|
||||
SysAdminOnly bool `json:"sysAdminOnly"`
|
||||
TeamAdminOnly bool `json:"teamAdminOnly"`
|
||||
}
|
||||
|
||||
func (r *NoticeMessages) Marshal() ([]byte, error) {
|
||||
return json.Marshal(r)
|
||||
}
|
||||
|
||||
func UnmarshalProductNoticeMessages(data io.Reader) (NoticeMessages, error) {
|
||||
var r NoticeMessages
|
||||
err := json.NewDecoder(data).Decode(&r)
|
||||
return r, err
|
||||
}
|
||||
|
||||
// User role, i.e. who will see the notice. Defaults to "all"
|
||||
type NoticeAudience string
|
||||
|
||||
func NewNoticeAudience(s NoticeAudience) *NoticeAudience {
|
||||
return &s
|
||||
}
|
||||
|
||||
func (a *NoticeAudience) Matches(sysAdmin bool, teamAdmin bool) bool {
|
||||
switch *a {
|
||||
case NoticeAudience_All:
|
||||
return true
|
||||
case NoticeAudience_Member:
|
||||
return !sysAdmin && !teamAdmin
|
||||
case NoticeAudience_Sysadmin:
|
||||
return sysAdmin
|
||||
case NoticeAudience_TeamAdmin:
|
||||
return teamAdmin
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const (
|
||||
NoticeAudience_All NoticeAudience = "all"
|
||||
NoticeAudience_Member NoticeAudience = "member"
|
||||
NoticeAudience_Sysadmin NoticeAudience = "sysadmin"
|
||||
NoticeAudience_TeamAdmin NoticeAudience = "teamadmin"
|
||||
)
|
||||
|
||||
// Only show the notice on specific clients. Defaults to 'all'
|
||||
//
|
||||
// Client type. Defaults to "all"
|
||||
type NoticeClientType string
|
||||
|
||||
func NewNoticeClientType(s NoticeClientType) *NoticeClientType { return &s }
|
||||
|
||||
func (c *NoticeClientType) Matches(other NoticeClientType) bool {
|
||||
switch *c {
|
||||
case NoticeClientType_All:
|
||||
return true
|
||||
case NoticeClientType_Mobile:
|
||||
return other == NoticeClientType_MobileIos || other == NoticeClientType_MobileAndroid
|
||||
default:
|
||||
return *c == other
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
NoticeClientType_All NoticeClientType = "all"
|
||||
NoticeClientType_Desktop NoticeClientType = "desktop"
|
||||
NoticeClientType_Mobile NoticeClientType = "mobile"
|
||||
NoticeClientType_MobileAndroid NoticeClientType = "mobile-android"
|
||||
NoticeClientType_MobileIos NoticeClientType = "mobile-ios"
|
||||
NoticeClientType_Web NoticeClientType = "web"
|
||||
)
|
||||
|
||||
func NoticeClientTypeFromString(s string) (NoticeClientType, error) {
|
||||
switch s {
|
||||
case "web":
|
||||
return NoticeClientType_Web, nil
|
||||
case "mobile-ios":
|
||||
return NoticeClientType_MobileIos, nil
|
||||
case "mobile-android":
|
||||
return NoticeClientType_MobileAndroid, nil
|
||||
case "desktop":
|
||||
return NoticeClientType_Desktop, nil
|
||||
}
|
||||
return NoticeClientType_All, errors.New("Invalid client type supplied")
|
||||
}
|
||||
|
||||
// Instance type. Defaults to "both"
|
||||
type NoticeInstanceType string
|
||||
|
||||
func NewNoticeInstanceType(n NoticeInstanceType) *NoticeInstanceType { return &n }
|
||||
func (t *NoticeInstanceType) Matches(isCloud bool) bool {
|
||||
if *t == NoticeInstanceType_Both {
|
||||
return true
|
||||
}
|
||||
if *t == NoticeInstanceType_Cloud && !isCloud {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const (
|
||||
NoticeInstanceType_Both NoticeInstanceType = "both"
|
||||
NoticeInstanceType_Cloud NoticeInstanceType = "cloud"
|
||||
NoticeInstanceType_OnPrem NoticeInstanceType = "onprem"
|
||||
)
|
||||
|
||||
// SKU. Defaults to "all"
|
||||
type NoticeSKU string
|
||||
|
||||
func NewNoticeSKU(s NoticeSKU) *NoticeSKU { return &s }
|
||||
func (c *NoticeSKU) Matches(s string) bool {
|
||||
switch *c {
|
||||
case NoticeSKU_All:
|
||||
return true
|
||||
case NoticeSKU_E0, NoticeSKU_Team:
|
||||
return s == ""
|
||||
default:
|
||||
return s == string(*c)
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
NoticeSKU_E0 NoticeSKU = "e0"
|
||||
NoticeSKU_E10 NoticeSKU = "e10"
|
||||
NoticeSKU_E20 NoticeSKU = "e20"
|
||||
NoticeSKU_All NoticeSKU = "all"
|
||||
NoticeSKU_Team NoticeSKU = "team"
|
||||
)
|
||||
|
||||
// Optional action to perform on action button click. (defaults to closing the notice)
|
||||
//
|
||||
// Possible actions to execute on button press
|
||||
type NoticeAction string
|
||||
|
||||
const (
|
||||
URL NoticeAction = "url"
|
||||
)
|
||||
|
||||
// Definition of the table keeping the 'viewed' state of each in-product notice per user
|
||||
type ProductNoticeViewState struct {
|
||||
UserId string
|
||||
NoticeId string
|
||||
Viewed int32
|
||||
Timestamp int64
|
||||
}
|
||||
@@ -36,6 +36,7 @@ type OpenTracingLayer struct {
|
||||
PluginStore store.PluginStore
|
||||
PostStore store.PostStore
|
||||
PreferenceStore store.PreferenceStore
|
||||
ProductNoticesStore store.ProductNoticesStore
|
||||
ReactionStore store.ReactionStore
|
||||
RoleStore store.RoleStore
|
||||
SchemeStore store.SchemeStore
|
||||
@@ -124,6 +125,10 @@ func (s *OpenTracingLayer) Preference() store.PreferenceStore {
|
||||
return s.PreferenceStore
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayer) ProductNotices() store.ProductNoticesStore {
|
||||
return s.ProductNoticesStore
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayer) Reaction() store.ReactionStore {
|
||||
return s.ReactionStore
|
||||
}
|
||||
@@ -270,6 +275,11 @@ type OpenTracingLayerPreferenceStore struct {
|
||||
Root *OpenTracingLayer
|
||||
}
|
||||
|
||||
type OpenTracingLayerProductNoticesStore struct {
|
||||
store.ProductNoticesStore
|
||||
Root *OpenTracingLayer
|
||||
}
|
||||
|
||||
type OpenTracingLayerReactionStore struct {
|
||||
store.ReactionStore
|
||||
Root *OpenTracingLayer
|
||||
@@ -5537,6 +5547,78 @@ func (s *OpenTracingLayerPreferenceStore) Save(preferences *model.Preferences) e
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerProductNoticesStore) Clear(notices []string) error {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ProductNoticesStore.Clear")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
err := s.ProductNoticesStore.Clear(notices)
|
||||
if err != nil {
|
||||
span.LogFields(spanlog.Error(err))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerProductNoticesStore) ClearOldNotices(currentNotices *model.ProductNotices) error {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ProductNoticesStore.ClearOldNotices")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
err := s.ProductNoticesStore.ClearOldNotices(currentNotices)
|
||||
if err != nil {
|
||||
span.LogFields(spanlog.Error(err))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerProductNoticesStore) GetViews(userId string) ([]model.ProductNoticeViewState, error) {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ProductNoticesStore.GetViews")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
result, err := s.ProductNoticesStore.GetViews(userId)
|
||||
if err != nil {
|
||||
span.LogFields(spanlog.Error(err))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerProductNoticesStore) View(userId string, notices []string) error {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ProductNoticesStore.View")
|
||||
s.Root.Store.SetContext(newCtx)
|
||||
defer func() {
|
||||
s.Root.Store.SetContext(origCtx)
|
||||
}()
|
||||
|
||||
defer span.Finish()
|
||||
err := s.ProductNoticesStore.View(userId, notices)
|
||||
if err != nil {
|
||||
span.LogFields(spanlog.Error(err))
|
||||
ext.Error.Set(span, true)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *OpenTracingLayerReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction, error) {
|
||||
origCtx := s.Root.Store.Context()
|
||||
span, newCtx := tracing.StartSpanWithParentByContext(s.Root.Store.Context(), "ReactionStore.BulkGetForPosts")
|
||||
@@ -9585,6 +9667,7 @@ func New(childStore store.Store, ctx context.Context) *OpenTracingLayer {
|
||||
newStore.PluginStore = &OpenTracingLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
|
||||
newStore.PostStore = &OpenTracingLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
|
||||
newStore.PreferenceStore = &OpenTracingLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore}
|
||||
newStore.ProductNoticesStore = &OpenTracingLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore}
|
||||
newStore.ReactionStore = &OpenTracingLayerReactionStore{ReactionStore: childStore.Reaction(), Root: &newStore}
|
||||
newStore.RoleStore = &OpenTracingLayerRoleStore{RoleStore: childStore.Role(), Root: &newStore}
|
||||
newStore.SchemeStore = &OpenTracingLayerSchemeStore{SchemeStore: childStore.Scheme(), Root: &newStore}
|
||||
|
||||
@@ -38,6 +38,7 @@ type RetryLayer struct {
|
||||
PluginStore store.PluginStore
|
||||
PostStore store.PostStore
|
||||
PreferenceStore store.PreferenceStore
|
||||
ProductNoticesStore store.ProductNoticesStore
|
||||
ReactionStore store.ReactionStore
|
||||
RoleStore store.RoleStore
|
||||
SchemeStore store.SchemeStore
|
||||
@@ -126,6 +127,10 @@ func (s *RetryLayer) Preference() store.PreferenceStore {
|
||||
return s.PreferenceStore
|
||||
}
|
||||
|
||||
func (s *RetryLayer) ProductNotices() store.ProductNoticesStore {
|
||||
return s.ProductNoticesStore
|
||||
}
|
||||
|
||||
func (s *RetryLayer) Reaction() store.ReactionStore {
|
||||
return s.ReactionStore
|
||||
}
|
||||
@@ -272,6 +277,11 @@ type RetryLayerPreferenceStore struct {
|
||||
Root *RetryLayer
|
||||
}
|
||||
|
||||
type RetryLayerProductNoticesStore struct {
|
||||
store.ProductNoticesStore
|
||||
Root *RetryLayer
|
||||
}
|
||||
|
||||
type RetryLayerReactionStore struct {
|
||||
store.ReactionStore
|
||||
Root *RetryLayer
|
||||
@@ -4706,6 +4716,86 @@ func (s *RetryLayerPreferenceStore) Save(preferences *model.Preferences) error {
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerProductNoticesStore) Clear(notices []string) error {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
err := s.ProductNoticesStore.Clear(notices)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !isRepeatableError(err) {
|
||||
return err
|
||||
}
|
||||
tries++
|
||||
if tries >= 3 {
|
||||
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerProductNoticesStore) ClearOldNotices(currentNotices *model.ProductNotices) error {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
err := s.ProductNoticesStore.ClearOldNotices(currentNotices)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !isRepeatableError(err) {
|
||||
return err
|
||||
}
|
||||
tries++
|
||||
if tries >= 3 {
|
||||
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerProductNoticesStore) GetViews(userId string) ([]model.ProductNoticeViewState, error) {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
result, err := s.ProductNoticesStore.GetViews(userId)
|
||||
if err == nil {
|
||||
return result, nil
|
||||
}
|
||||
if !isRepeatableError(err) {
|
||||
return result, err
|
||||
}
|
||||
tries++
|
||||
if tries >= 3 {
|
||||
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerProductNoticesStore) View(userId string, notices []string) error {
|
||||
|
||||
tries := 0
|
||||
for {
|
||||
err := s.ProductNoticesStore.View(userId, notices)
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !isRepeatableError(err) {
|
||||
return err
|
||||
}
|
||||
tries++
|
||||
if tries >= 3 {
|
||||
err = errors.Wrap(err, "giving up after 3 consecutive repeatable transaction failures")
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *RetryLayerReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction, error) {
|
||||
|
||||
tries := 0
|
||||
@@ -8261,6 +8351,7 @@ func New(childStore store.Store) *RetryLayer {
|
||||
newStore.PluginStore = &RetryLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
|
||||
newStore.PostStore = &RetryLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
|
||||
newStore.PreferenceStore = &RetryLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore}
|
||||
newStore.ProductNoticesStore = &RetryLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore}
|
||||
newStore.ReactionStore = &RetryLayerReactionStore{ReactionStore: childStore.Reaction(), Root: &newStore}
|
||||
newStore.RoleStore = &RetryLayerRoleStore{RoleStore: childStore.Role(), Root: &newStore}
|
||||
newStore.SchemeStore = &RetryLayerSchemeStore{SchemeStore: childStore.Scheme(), Root: &newStore}
|
||||
|
||||
@@ -34,6 +34,7 @@ func genStore() *mocks.Store {
|
||||
mock.On("Plugin").Return(&mocks.PluginStore{})
|
||||
mock.On("Post").Return(&mocks.PostStore{})
|
||||
mock.On("Preference").Return(&mocks.PreferenceStore{})
|
||||
mock.On("ProductNotices").Return(&mocks.ProductNoticesStore{})
|
||||
mock.On("Reaction").Return(&mocks.ReactionStore{})
|
||||
mock.On("Role").Return(&mocks.RoleStore{})
|
||||
mock.On("Scheme").Return(&mocks.SchemeStore{})
|
||||
|
||||
125
store/sqlstore/product_notices_store.go
Обычный файл
125
store/sqlstore/product_notices_store.go
Обычный файл
@@ -0,0 +1,125 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"github.com/mattermost/mattermost-server/v5/store"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type SqlProductNoticesStore struct {
|
||||
SqlStore
|
||||
}
|
||||
|
||||
func newSqlProductNoticesStore(sqlStore SqlStore) store.ProductNoticesStore {
|
||||
s := SqlProductNoticesStore{sqlStore}
|
||||
|
||||
for _, db := range sqlStore.GetAllConns() {
|
||||
table := db.AddTableWithName(model.ProductNoticeViewState{}, "ProductNoticeViewState").SetKeys(false, "UserId", "NoticeId")
|
||||
table.ColMap("UserId").SetMaxSize(26)
|
||||
table.ColMap("NoticeId").SetMaxSize(26)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s SqlProductNoticesStore) createIndexesIfNotExists() {
|
||||
s.CreateIndexIfNotExists("idx_notice_views_timestamp", "ProductNoticeViewState", "Timestamp")
|
||||
s.CreateIndexIfNotExists("idx_notice_views_user_id", "ProductNoticeViewState", "UserId")
|
||||
s.CreateIndexIfNotExists("idx_notice_views_notice_id", "ProductNoticeViewState", "NoticeId")
|
||||
|
||||
s.CreateCompositeIndexIfNotExists("idx_notice_views_user_notice", "ProductNoticeViewState", []string{"UserId", "NoticeId"})
|
||||
|
||||
}
|
||||
|
||||
func (s SqlProductNoticesStore) Clear(notices []string) error {
|
||||
sql, args, _ := s.getQueryBuilder().Delete("ProductNoticeViewState").Where(sq.Eq{"NoticeId": notices}).ToSql()
|
||||
if _, err := s.GetMaster().Exec(sql, args...); err != nil {
|
||||
return errors.Wrapf(err, "failed to delete records from ProductNoticeViewState")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s SqlProductNoticesStore) ClearOldNotices(currentNotices *model.ProductNotices) error {
|
||||
var notices []string
|
||||
for _, currentNotice := range *currentNotices {
|
||||
notices = append(notices, currentNotice.ID)
|
||||
}
|
||||
sql, args, _ := s.getQueryBuilder().Delete("ProductNoticeViewState").Where(sq.NotEq{"NoticeId": notices}).ToSql()
|
||||
if _, err := s.GetMaster().Exec(sql, args...); err != nil {
|
||||
return errors.Wrapf(err, "failed to delete records from ProductNoticeViewState")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s SqlProductNoticesStore) View(userId string, notices []string) error {
|
||||
transaction, err := s.GetMaster().Begin()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "begin_transaction")
|
||||
}
|
||||
defer finalizeTransaction(transaction)
|
||||
|
||||
var noticeStates []model.ProductNoticeViewState
|
||||
sql, args, _ := s.getQueryBuilder().
|
||||
Select("*").
|
||||
From("ProductNoticeViewState").
|
||||
Where(sq.And{sq.Eq{"UserId": userId}, sq.Eq{"NoticeId": notices}}).
|
||||
ToSql()
|
||||
if _, err := transaction.Select(¬iceStates, sql, args...); err != nil {
|
||||
return errors.Wrapf(err, "failed to get ProductNoticeViewState with userId=%s", userId)
|
||||
}
|
||||
|
||||
now := time.Now().UTC().Unix()
|
||||
|
||||
// update existing records
|
||||
for i := range noticeStates {
|
||||
noticeStates[i].Viewed += 1
|
||||
noticeStates[i].Timestamp = now
|
||||
if _, err := transaction.Update(¬iceStates[i]); err != nil {
|
||||
return errors.Wrapf(err, "failed to update ProductNoticeViewState")
|
||||
}
|
||||
}
|
||||
|
||||
// add new ones
|
||||
haveNoticeState := func(n string) bool {
|
||||
for _, ns := range noticeStates {
|
||||
if ns.NoticeId == n {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
for _, noticeId := range notices {
|
||||
if !haveNoticeState(noticeId) {
|
||||
if err := transaction.Insert(&model.ProductNoticeViewState{
|
||||
UserId: userId,
|
||||
NoticeId: noticeId,
|
||||
Viewed: 1,
|
||||
Timestamp: now,
|
||||
}); err != nil {
|
||||
return errors.Wrapf(err, "failed to insert ProductNoticeViewState")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := transaction.Commit(); err != nil {
|
||||
return errors.Wrap(err, "commit_transaction")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s SqlProductNoticesStore) GetViews(userId string) ([]model.ProductNoticeViewState, error) {
|
||||
var noticeStates []model.ProductNoticeViewState
|
||||
sql, args, _ := s.getQueryBuilder().Select("*").From("ProductNoticeViewState").Where(sq.Eq{"UserId": userId}).ToSql()
|
||||
if _, err := s.GetReplica().Select(¬iceStates, sql, args...); err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get ProductNoticeViewState with userId=%s", userId)
|
||||
}
|
||||
return noticeStates, nil
|
||||
}
|
||||
14
store/sqlstore/product_notices_store_test.go
Обычный файл
14
store/sqlstore/product_notices_store_test.go
Обычный файл
@@ -0,0 +1,14 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package sqlstore
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/store/storetest"
|
||||
)
|
||||
|
||||
func TestProductNoticesStore(t *testing.T) {
|
||||
StoreTest(t, storetest.TestProductNoticesStore)
|
||||
}
|
||||
@@ -98,6 +98,7 @@ type SqlSupplierStores struct {
|
||||
role store.RoleStore
|
||||
scheme store.SchemeStore
|
||||
TermsOfService store.TermsOfServiceStore
|
||||
productNotices store.ProductNoticesStore
|
||||
group store.GroupStore
|
||||
UserTermsOfService store.UserTermsOfServiceStore
|
||||
linkMetadata store.LinkMetadataStore
|
||||
@@ -170,7 +171,7 @@ func NewSqlSupplier(settings model.SqlSettings, metrics einterfaces.MetricsInter
|
||||
supplier.stores.role = newSqlRoleStore(supplier)
|
||||
supplier.stores.scheme = newSqlSchemeStore(supplier)
|
||||
supplier.stores.group = newSqlGroupStore(supplier)
|
||||
|
||||
supplier.stores.productNotices = newSqlProductNoticesStore(supplier)
|
||||
err := supplier.GetMaster().CreateTablesIfNotExists()
|
||||
if err != nil {
|
||||
mlog.Critical("Error creating database tables.", mlog.Err(err))
|
||||
@@ -209,6 +210,7 @@ func NewSqlSupplier(settings model.SqlSettings, metrics einterfaces.MetricsInter
|
||||
supplier.stores.userAccessToken.(*SqlUserAccessTokenStore).createIndexesIfNotExists()
|
||||
supplier.stores.plugin.(*SqlPluginStore).createIndexesIfNotExists()
|
||||
supplier.stores.TermsOfService.(SqlTermsOfServiceStore).createIndexesIfNotExists()
|
||||
supplier.stores.productNotices.(SqlProductNoticesStore).createIndexesIfNotExists()
|
||||
supplier.stores.UserTermsOfService.(SqlUserTermsOfServiceStore).createIndexesIfNotExists()
|
||||
supplier.stores.linkMetadata.(*SqlLinkMetadataStore).createIndexesIfNotExists()
|
||||
supplier.stores.group.(*SqlGroupStore).createIndexesIfNotExists()
|
||||
@@ -1169,6 +1171,10 @@ func (ss *SqlSupplier) TermsOfService() store.TermsOfServiceStore {
|
||||
return ss.stores.TermsOfService
|
||||
}
|
||||
|
||||
func (ss *SqlSupplier) ProductNotices() store.ProductNoticesStore {
|
||||
return ss.stores.productNotices
|
||||
}
|
||||
|
||||
func (ss *SqlSupplier) UserTermsOfService() store.UserTermsOfServiceStore {
|
||||
return ss.stores.UserTermsOfService
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ type Store interface {
|
||||
ChannelMemberHistory() ChannelMemberHistoryStore
|
||||
Plugin() PluginStore
|
||||
TermsOfService() TermsOfServiceStore
|
||||
ProductNotices() ProductNoticesStore
|
||||
Group() GroupStore
|
||||
UserTermsOfService() UserTermsOfServiceStore
|
||||
LinkMetadata() LinkMetadataStore
|
||||
@@ -646,6 +647,13 @@ type TermsOfServiceStore interface {
|
||||
Get(id string, allowFromCache bool) (*model.TermsOfService, error)
|
||||
}
|
||||
|
||||
type ProductNoticesStore interface {
|
||||
View(userId string, notices []string) error
|
||||
Clear(notices []string) error
|
||||
ClearOldNotices(currentNotices *model.ProductNotices) error
|
||||
GetViews(userId string) ([]model.ProductNoticeViewState, error)
|
||||
}
|
||||
|
||||
type UserTermsOfServiceStore interface {
|
||||
GetByUser(userId string) (*model.UserTermsOfService, error)
|
||||
Save(userTermsOfService *model.UserTermsOfService) (*model.UserTermsOfService, error)
|
||||
|
||||
80
store/storetest/mocks/ProductNoticesStore.go
Обычный файл
80
store/storetest/mocks/ProductNoticesStore.go
Обычный файл
@@ -0,0 +1,80 @@
|
||||
// Code generated by mockery v1.0.0. DO NOT EDIT.
|
||||
|
||||
// Regenerate this file using `make store-mocks`.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
model "github.com/mattermost/mattermost-server/v5/model"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// ProductNoticesStore is an autogenerated mock type for the ProductNoticesStore type
|
||||
type ProductNoticesStore struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Clear provides a mock function with given fields: notices
|
||||
func (_m *ProductNoticesStore) Clear(notices []string) error {
|
||||
ret := _m.Called(notices)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func([]string) error); ok {
|
||||
r0 = rf(notices)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// ClearOldNotices provides a mock function with given fields: currentNotices
|
||||
func (_m *ProductNoticesStore) ClearOldNotices(currentNotices *model.ProductNotices) error {
|
||||
ret := _m.Called(currentNotices)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*model.ProductNotices) error); ok {
|
||||
r0 = rf(currentNotices)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// GetViews provides a mock function with given fields: userId
|
||||
func (_m *ProductNoticesStore) GetViews(userId string) ([]model.ProductNoticeViewState, error) {
|
||||
ret := _m.Called(userId)
|
||||
|
||||
var r0 []model.ProductNoticeViewState
|
||||
if rf, ok := ret.Get(0).(func(string) []model.ProductNoticeViewState); ok {
|
||||
r0 = rf(userId)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]model.ProductNoticeViewState)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string) error); ok {
|
||||
r1 = rf(userId)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// View provides a mock function with given fields: userId, notices
|
||||
func (_m *ProductNoticesStore) View(userId string, notices []string) error {
|
||||
ret := _m.Called(userId, notices)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, []string) error); ok {
|
||||
r0 = rf(userId, notices)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
@@ -395,6 +395,22 @@ func (_m *Store) Preference() store.PreferenceStore {
|
||||
return r0
|
||||
}
|
||||
|
||||
// ProductNotices provides a mock function with given fields:
|
||||
func (_m *Store) ProductNotices() store.ProductNoticesStore {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 store.ProductNoticesStore
|
||||
if rf, ok := ret.Get(0).(func() store.ProductNoticesStore); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(store.ProductNoticesStore)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Reaction provides a mock function with given fields:
|
||||
func (_m *Store) Reaction() store.ReactionStore {
|
||||
ret := _m.Called()
|
||||
|
||||
79
store/storetest/product_notices_store.go
Обычный файл
79
store/storetest/product_notices_store.go
Обычный файл
@@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
package storetest
|
||||
|
||||
import (
|
||||
"github.com/mattermost/mattermost-server/v5/model"
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/mattermost-server/v5/store"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestProductNoticesStore(t *testing.T, ss store.Store) {
|
||||
t.Run("TestAddViewed", func(t *testing.T) { testAddViewed(t, ss) })
|
||||
t.Run("TestUpdateViewed", func(t *testing.T) { testUpdateViewed(t, ss) })
|
||||
t.Run("TestClearOld", func(t *testing.T) { testClearOld(t, ss) })
|
||||
}
|
||||
|
||||
func testAddViewed(t *testing.T, ss store.Store) {
|
||||
notices := []string{"noticeA", "noticeB"}
|
||||
defer ss.ProductNotices().Clear(notices)
|
||||
|
||||
err := ss.ProductNotices().View("testuser", notices)
|
||||
require.NoError(t, err)
|
||||
err = ss.ProductNotices().View("testuser2", notices)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := ss.ProductNotices().GetViews("testuser")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 2)
|
||||
}
|
||||
|
||||
func testUpdateViewed(t *testing.T, ss store.Store) {
|
||||
noticesA := []string{"noticeA", "noticeB"}
|
||||
noticesB := []string{"noticeB", "noticeC"}
|
||||
defer ss.ProductNotices().Clear(noticesA)
|
||||
defer ss.ProductNotices().Clear(noticesB)
|
||||
// mark two notices
|
||||
err := ss.ProductNotices().View("testuser", noticesA)
|
||||
require.NoError(t, err)
|
||||
// mark one old and one new
|
||||
err = ss.ProductNotices().View("testuser", noticesB)
|
||||
require.NoError(t, err)
|
||||
|
||||
res, err := ss.ProductNotices().GetViews("testuser")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 3)
|
||||
|
||||
// make sure that one B has two views
|
||||
require.Equal(t, res[0].Viewed, int32(1))
|
||||
require.Equal(t, res[1].Viewed, int32(2))
|
||||
require.Equal(t, res[2].Viewed, int32(1))
|
||||
|
||||
// make sure that B's timestamp was updated
|
||||
require.GreaterOrEqual(t, res[1].Timestamp, res[0].Timestamp)
|
||||
}
|
||||
|
||||
func testClearOld(t *testing.T, ss store.Store) {
|
||||
noticesA := []string{"noticeA", "noticeB"}
|
||||
defer ss.ProductNotices().Clear(noticesA)
|
||||
// mark two notices
|
||||
err := ss.ProductNotices().View("testuser", noticesA)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = ss.ProductNotices().ClearOldNotices(&model.ProductNotices{
|
||||
{
|
||||
ID: "noticeA",
|
||||
},
|
||||
{
|
||||
ID: "noticeC",
|
||||
},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
res, err := ss.ProductNotices().GetViews("testuser")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, res, 1)
|
||||
|
||||
}
|
||||
@@ -47,6 +47,7 @@ type Store struct {
|
||||
GroupStore mocks.GroupStore
|
||||
UserTermsOfServiceStore mocks.UserTermsOfServiceStore
|
||||
LinkMetadataStore mocks.LinkMetadataStore
|
||||
ProductNoticesStore mocks.ProductNoticesStore
|
||||
context context.Context
|
||||
}
|
||||
|
||||
@@ -57,6 +58,7 @@ func (s *Store) Channel() store.ChannelStore { return &s.C
|
||||
func (s *Store) Post() store.PostStore { return &s.PostStore }
|
||||
func (s *Store) User() store.UserStore { return &s.UserStore }
|
||||
func (s *Store) Bot() store.BotStore { return &s.BotStore }
|
||||
func (s *Store) ProductNotices() store.ProductNoticesStore { return &s.ProductNoticesStore }
|
||||
func (s *Store) Audit() store.AuditStore { return &s.AuditStore }
|
||||
func (s *Store) ClusterDiscovery() store.ClusterDiscoveryStore { return &s.ClusterDiscoveryStore }
|
||||
func (s *Store) Compliance() store.ComplianceStore { return &s.ComplianceStore }
|
||||
@@ -131,5 +133,6 @@ func (s *Store) AssertExpectations(t mock.TestingT) bool {
|
||||
&s.PluginStore,
|
||||
&s.RoleStore,
|
||||
&s.SchemeStore,
|
||||
&s.ProductNoticesStore,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ type TimerLayer struct {
|
||||
PluginStore store.PluginStore
|
||||
PostStore store.PostStore
|
||||
PreferenceStore store.PreferenceStore
|
||||
ProductNoticesStore store.ProductNoticesStore
|
||||
ReactionStore store.ReactionStore
|
||||
RoleStore store.RoleStore
|
||||
SchemeStore store.SchemeStore
|
||||
@@ -124,6 +125,10 @@ func (s *TimerLayer) Preference() store.PreferenceStore {
|
||||
return s.PreferenceStore
|
||||
}
|
||||
|
||||
func (s *TimerLayer) ProductNotices() store.ProductNoticesStore {
|
||||
return s.ProductNoticesStore
|
||||
}
|
||||
|
||||
func (s *TimerLayer) Reaction() store.ReactionStore {
|
||||
return s.ReactionStore
|
||||
}
|
||||
@@ -270,6 +275,11 @@ type TimerLayerPreferenceStore struct {
|
||||
Root *TimerLayer
|
||||
}
|
||||
|
||||
type TimerLayerProductNoticesStore struct {
|
||||
store.ProductNoticesStore
|
||||
Root *TimerLayer
|
||||
}
|
||||
|
||||
type TimerLayerReactionStore struct {
|
||||
store.ReactionStore
|
||||
Root *TimerLayer
|
||||
@@ -5015,6 +5025,70 @@ func (s *TimerLayerPreferenceStore) Save(preferences *model.Preferences) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TimerLayerProductNoticesStore) Clear(notices []string) error {
|
||||
start := timemodule.Now()
|
||||
|
||||
err := s.ProductNoticesStore.Clear(notices)
|
||||
|
||||
elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("ProductNoticesStore.Clear", success, elapsed)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TimerLayerProductNoticesStore) ClearOldNotices(currentNotices *model.ProductNotices) error {
|
||||
start := timemodule.Now()
|
||||
|
||||
err := s.ProductNoticesStore.ClearOldNotices(currentNotices)
|
||||
|
||||
elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("ProductNoticesStore.ClearOldNotices", success, elapsed)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TimerLayerProductNoticesStore) GetViews(userId string) ([]model.ProductNoticeViewState, error) {
|
||||
start := timemodule.Now()
|
||||
|
||||
result, err := s.ProductNoticesStore.GetViews(userId)
|
||||
|
||||
elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("ProductNoticesStore.GetViews", success, elapsed)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *TimerLayerProductNoticesStore) View(userId string, notices []string) error {
|
||||
start := timemodule.Now()
|
||||
|
||||
err := s.ProductNoticesStore.View(userId, notices)
|
||||
|
||||
elapsed := float64(timemodule.Since(start)) / float64(timemodule.Second)
|
||||
if s.Root.Metrics != nil {
|
||||
success := "false"
|
||||
if err == nil {
|
||||
success = "true"
|
||||
}
|
||||
s.Root.Metrics.ObserveStoreMethodDuration("ProductNoticesStore.View", success, elapsed)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *TimerLayerReactionStore) BulkGetForPosts(postIds []string) ([]*model.Reaction, error) {
|
||||
start := timemodule.Now()
|
||||
|
||||
@@ -8669,6 +8743,7 @@ func New(childStore store.Store, metrics einterfaces.MetricsInterface) *TimerLay
|
||||
newStore.PluginStore = &TimerLayerPluginStore{PluginStore: childStore.Plugin(), Root: &newStore}
|
||||
newStore.PostStore = &TimerLayerPostStore{PostStore: childStore.Post(), Root: &newStore}
|
||||
newStore.PreferenceStore = &TimerLayerPreferenceStore{PreferenceStore: childStore.Preference(), Root: &newStore}
|
||||
newStore.ProductNoticesStore = &TimerLayerProductNoticesStore{ProductNoticesStore: childStore.ProductNotices(), Root: &newStore}
|
||||
newStore.ReactionStore = &TimerLayerReactionStore{ReactionStore: childStore.Reaction(), Root: &newStore}
|
||||
newStore.RoleStore = &TimerLayerRoleStore{RoleStore: childStore.Role(), Root: &newStore}
|
||||
newStore.SchemeStore = &TimerLayerSchemeStore{SchemeStore: childStore.Scheme(), Root: &newStore}
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -121,3 +123,52 @@ func GetHostnameFromSiteURL(siteURL string) string {
|
||||
|
||||
return u.Hostname()
|
||||
}
|
||||
|
||||
type RequestCache struct {
|
||||
Data []byte
|
||||
Date string
|
||||
Key string
|
||||
}
|
||||
|
||||
// Fetch JSON data from the notices server
|
||||
// if skip is passed, does a fetch without touching the cache
|
||||
func GetUrlWithCache(url string, cache *RequestCache, skip bool) ([]byte, error) {
|
||||
// Build a GET Request, including optional If-None-Match header.
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
cache.Data = nil
|
||||
return nil, err
|
||||
}
|
||||
if !skip && cache.Data != nil {
|
||||
req.Header.Add("If-None-Match", cache.Key)
|
||||
req.Header.Add("If-Modified-Since", cache.Date)
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
cache.Data = nil
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
// No change from latest known Etag?
|
||||
if resp.StatusCode == http.StatusNotModified {
|
||||
return cache.Data, nil
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
cache.Data = nil
|
||||
return nil, errors.Errorf("Fetching notices failed with status code %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
cache.Data, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
cache.Data = nil
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If etags headers are missing, ignore.
|
||||
cache.Key = resp.Header.Get("ETag")
|
||||
cache.Date = resp.Header.Get("Date")
|
||||
return cache.Data, err
|
||||
}
|
||||
|
||||
1
vendor/github.com/Masterminds/semver/v3/.gitignore
сгенерированный
поставляемый
Обычный файл
1
vendor/github.com/Masterminds/semver/v3/.gitignore
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1 @@
|
||||
_fuzz/
|
||||
26
vendor/github.com/Masterminds/semver/v3/.golangci.yml
сгенерированный
поставляемый
Обычный файл
26
vendor/github.com/Masterminds/semver/v3/.golangci.yml
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,26 @@
|
||||
run:
|
||||
deadline: 2m
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- deadcode
|
||||
- dupl
|
||||
- errcheck
|
||||
- gofmt
|
||||
- goimports
|
||||
- golint
|
||||
- gosimple
|
||||
- govet
|
||||
- ineffassign
|
||||
- misspell
|
||||
- nakedret
|
||||
- structcheck
|
||||
- unused
|
||||
- varcheck
|
||||
|
||||
linters-settings:
|
||||
gofmt:
|
||||
simplify: true
|
||||
dupl:
|
||||
threshold: 400
|
||||
188
vendor/github.com/Masterminds/semver/v3/CHANGELOG.md
сгенерированный
поставляемый
Обычный файл
188
vendor/github.com/Masterminds/semver/v3/CHANGELOG.md
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,188 @@
|
||||
# Changelog
|
||||
|
||||
## 3.1.0 (2020-04-15)
|
||||
|
||||
### Added
|
||||
|
||||
- #131: Add support for serializing/deserializing SQL (thanks @ryancurrah)
|
||||
|
||||
### Changed
|
||||
|
||||
- #148: More accurate validation messages on constraints
|
||||
|
||||
## 3.0.3 (2019-12-13)
|
||||
|
||||
### Fixed
|
||||
|
||||
- #141: Fixed issue with <= comparison
|
||||
|
||||
## 3.0.2 (2019-11-14)
|
||||
|
||||
### Fixed
|
||||
|
||||
- #134: Fixed broken constraint checking with ^0.0 (thanks @krmichelos)
|
||||
|
||||
## 3.0.1 (2019-09-13)
|
||||
|
||||
### Fixed
|
||||
|
||||
- #125: Fixes issue with module path for v3
|
||||
|
||||
## 3.0.0 (2019-09-12)
|
||||
|
||||
This is a major release of the semver package which includes API changes. The Go
|
||||
API is compatible with ^1. The Go API was not changed because many people are using
|
||||
`go get` without Go modules for their applications and API breaking changes cause
|
||||
errors which we have or would need to support.
|
||||
|
||||
The changes in this release are the handling based on the data passed into the
|
||||
functions. These are described in the added and changed sections below.
|
||||
|
||||
### Added
|
||||
|
||||
- StrictNewVersion function. This is similar to NewVersion but will return an
|
||||
error if the version passed in is not a strict semantic version. For example,
|
||||
1.2.3 would pass but v1.2.3 or 1.2 would fail because they are not strictly
|
||||
speaking semantic versions. This function is faster, performs fewer operations,
|
||||
and uses fewer allocations than NewVersion.
|
||||
- Fuzzing has been performed on NewVersion, StrictNewVersion, and NewConstraint.
|
||||
The Makefile contains the operations used. For more information on you can start
|
||||
on Wikipedia at https://en.wikipedia.org/wiki/Fuzzing
|
||||
- Now using Go modules
|
||||
|
||||
### Changed
|
||||
|
||||
- NewVersion has proper prerelease and metadata validation with error messages
|
||||
to signal an issue with either of them
|
||||
- ^ now operates using a similar set of rules to npm/js and Rust/Cargo. If the
|
||||
version is >=1 the ^ ranges works the same as v1. For major versions of 0 the
|
||||
rules have changed. The minor version is treated as the stable version unless
|
||||
a patch is specified and then it is equivalent to =. One difference from npm/js
|
||||
is that prereleases there are only to a specific version (e.g. 1.2.3).
|
||||
Prereleases here look over multiple versions and follow semantic version
|
||||
ordering rules. This pattern now follows along with the expected and requested
|
||||
handling of this packaged by numerous users.
|
||||
|
||||
## 1.5.0 (2019-09-11)
|
||||
|
||||
### Added
|
||||
|
||||
- #103: Add basic fuzzing for `NewVersion()` (thanks @jesse-c)
|
||||
|
||||
### Changed
|
||||
|
||||
- #82: Clarify wildcard meaning in range constraints and update tests for it (thanks @greysteil)
|
||||
- #83: Clarify caret operator range for pre-1.0.0 dependencies (thanks @greysteil)
|
||||
- #72: Adding docs comment pointing to vert for a cli
|
||||
- #71: Update the docs on pre-release comparator handling
|
||||
- #89: Test with new go versions (thanks @thedevsaddam)
|
||||
- #87: Added $ to ValidPrerelease for better validation (thanks @jeremycarroll)
|
||||
|
||||
### Fixed
|
||||
|
||||
- #78: Fix unchecked error in example code (thanks @ravron)
|
||||
- #70: Fix the handling of pre-releases and the 0.0.0 release edge case
|
||||
- #97: Fixed copyright file for proper display on GitHub
|
||||
- #107: Fix handling prerelease when sorting alphanum and num
|
||||
- #109: Fixed where Validate sometimes returns wrong message on error
|
||||
|
||||
## 1.4.2 (2018-04-10)
|
||||
|
||||
### Changed
|
||||
|
||||
- #72: Updated the docs to point to vert for a console appliaction
|
||||
- #71: Update the docs on pre-release comparator handling
|
||||
|
||||
### Fixed
|
||||
|
||||
- #70: Fix the handling of pre-releases and the 0.0.0 release edge case
|
||||
|
||||
## 1.4.1 (2018-04-02)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed #64: Fix pre-release precedence issue (thanks @uudashr)
|
||||
|
||||
## 1.4.0 (2017-10-04)
|
||||
|
||||
### Changed
|
||||
|
||||
- #61: Update NewVersion to parse ints with a 64bit int size (thanks @zknill)
|
||||
|
||||
## 1.3.1 (2017-07-10)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed #57: number comparisons in prerelease sometimes inaccurate
|
||||
|
||||
## 1.3.0 (2017-05-02)
|
||||
|
||||
### Added
|
||||
|
||||
- #45: Added json (un)marshaling support (thanks @mh-cbon)
|
||||
- Stability marker. See https://masterminds.github.io/stability/
|
||||
|
||||
### Fixed
|
||||
|
||||
- #51: Fix handling of single digit tilde constraint (thanks @dgodd)
|
||||
|
||||
### Changed
|
||||
|
||||
- #55: The godoc icon moved from png to svg
|
||||
|
||||
## 1.2.3 (2017-04-03)
|
||||
|
||||
### Fixed
|
||||
|
||||
- #46: Fixed 0.x.x and 0.0.x in constraints being treated as *
|
||||
|
||||
## Release 1.2.2 (2016-12-13)
|
||||
|
||||
### Fixed
|
||||
|
||||
- #34: Fixed issue where hyphen range was not working with pre-release parsing.
|
||||
|
||||
## Release 1.2.1 (2016-11-28)
|
||||
|
||||
### Fixed
|
||||
|
||||
- #24: Fixed edge case issue where constraint "> 0" does not handle "0.0.1-alpha"
|
||||
properly.
|
||||
|
||||
## Release 1.2.0 (2016-11-04)
|
||||
|
||||
### Added
|
||||
|
||||
- #20: Added MustParse function for versions (thanks @adamreese)
|
||||
- #15: Added increment methods on versions (thanks @mh-cbon)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Issue #21: Per the SemVer spec (section 9) a pre-release is unstable and
|
||||
might not satisfy the intended compatibility. The change here ignores pre-releases
|
||||
on constraint checks (e.g., ~ or ^) when a pre-release is not part of the
|
||||
constraint. For example, `^1.2.3` will ignore pre-releases while
|
||||
`^1.2.3-alpha` will include them.
|
||||
|
||||
## Release 1.1.1 (2016-06-30)
|
||||
|
||||
### Changed
|
||||
|
||||
- Issue #9: Speed up version comparison performance (thanks @sdboyer)
|
||||
- Issue #8: Added benchmarks (thanks @sdboyer)
|
||||
- Updated Go Report Card URL to new location
|
||||
- Updated Readme to add code snippet formatting (thanks @mh-cbon)
|
||||
- Updating tagging to v[SemVer] structure for compatibility with other tools.
|
||||
|
||||
## Release 1.1.0 (2016-03-11)
|
||||
|
||||
- Issue #2: Implemented validation to provide reasons a versions failed a
|
||||
constraint.
|
||||
|
||||
## Release 1.0.1 (2015-12-31)
|
||||
|
||||
- Fixed #1: * constraint failing on valid versions.
|
||||
|
||||
## Release 1.0.0 (2015-10-20)
|
||||
|
||||
- Initial release
|
||||
19
vendor/github.com/Masterminds/semver/v3/LICENSE.txt
сгенерированный
поставляемый
Обычный файл
19
vendor/github.com/Masterminds/semver/v3/LICENSE.txt
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,19 @@
|
||||
Copyright (C) 2014-2019, Matt Butcher and Matt Farina
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
37
vendor/github.com/Masterminds/semver/v3/Makefile
сгенерированный
поставляемый
Обычный файл
37
vendor/github.com/Masterminds/semver/v3/Makefile
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,37 @@
|
||||
GOPATH=$(shell go env GOPATH)
|
||||
GOLANGCI_LINT=$(GOPATH)/bin/golangci-lint
|
||||
GOFUZZBUILD = $(GOPATH)/bin/go-fuzz-build
|
||||
GOFUZZ = $(GOPATH)/bin/go-fuzz
|
||||
|
||||
.PHONY: lint
|
||||
lint: $(GOLANGCI_LINT)
|
||||
@echo "==> Linting codebase"
|
||||
@$(GOLANGCI_LINT) run
|
||||
|
||||
.PHONY: test
|
||||
test:
|
||||
@echo "==> Running tests"
|
||||
GO111MODULE=on go test -v
|
||||
|
||||
.PHONY: test-cover
|
||||
test-cover:
|
||||
@echo "==> Running Tests with coverage"
|
||||
GO111MODULE=on go test -cover .
|
||||
|
||||
.PHONY: fuzz
|
||||
fuzz: $(GOFUZZBUILD) $(GOFUZZ)
|
||||
@echo "==> Fuzz testing"
|
||||
$(GOFUZZBUILD)
|
||||
$(GOFUZZ) -workdir=_fuzz
|
||||
|
||||
$(GOLANGCI_LINT):
|
||||
# Install golangci-lint. The configuration for it is in the .golangci.yml
|
||||
# file in the root of the repository
|
||||
echo ${GOPATH}
|
||||
curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(GOPATH)/bin v1.17.1
|
||||
|
||||
$(GOFUZZBUILD):
|
||||
cd / && go get -u github.com/dvyukov/go-fuzz/go-fuzz-build
|
||||
|
||||
$(GOFUZZ):
|
||||
cd / && go get -u github.com/dvyukov/go-fuzz/go-fuzz github.com/dvyukov/go-fuzz/go-fuzz-dep
|
||||
244
vendor/github.com/Masterminds/semver/v3/README.md
сгенерированный
поставляемый
Обычный файл
244
vendor/github.com/Masterminds/semver/v3/README.md
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,244 @@
|
||||
# SemVer
|
||||
|
||||
The `semver` package provides the ability to work with [Semantic Versions](http://semver.org) in Go. Specifically it provides the ability to:
|
||||
|
||||
* Parse semantic versions
|
||||
* Sort semantic versions
|
||||
* Check if a semantic version fits within a set of constraints
|
||||
* Optionally work with a `v` prefix
|
||||
|
||||
[](https://masterminds.github.io/stability/active.html)
|
||||
[](https://github.com/Masterminds/semver/actions)
|
||||
[](https://pkg.go.dev/github.com/Masterminds/semver/v3)
|
||||
[](https://goreportcard.com/report/github.com/Masterminds/semver)
|
||||
|
||||
If you are looking for a command line tool for version comparisons please see
|
||||
[vert](https://github.com/Masterminds/vert) which uses this library.
|
||||
|
||||
## Package Versions
|
||||
|
||||
There are three major versions fo the `semver` package.
|
||||
|
||||
* 3.x.x is the new stable and active version. This version is focused on constraint
|
||||
compatibility for range handling in other tools from other languages. It has
|
||||
a similar API to the v1 releases. The development of this version is on the master
|
||||
branch. The documentation for this version is below.
|
||||
* 2.x was developed primarily for [dep](https://github.com/golang/dep). There are
|
||||
no tagged releases and the development was performed by [@sdboyer](https://github.com/sdboyer).
|
||||
There are API breaking changes from v1. This version lives on the [2.x branch](https://github.com/Masterminds/semver/tree/2.x).
|
||||
* 1.x.x is the most widely used version with numerous tagged releases. This is the
|
||||
previous stable and is still maintained for bug fixes. The development, to fix
|
||||
bugs, occurs on the release-1 branch. You can read the documentation [here](https://github.com/Masterminds/semver/blob/release-1/README.md).
|
||||
|
||||
## Parsing Semantic Versions
|
||||
|
||||
There are two functions that can parse semantic versions. The `StrictNewVersion`
|
||||
function only parses valid version 2 semantic versions as outlined in the
|
||||
specification. The `NewVersion` function attempts to coerce a version into a
|
||||
semantic version and parse it. For example, if there is a leading v or a version
|
||||
listed without all 3 parts (e.g. `v1.2`) it will attempt to coerce it into a valid
|
||||
semantic version (e.g., 1.2.0). In both cases a `Version` object is returned
|
||||
that can be sorted, compared, and used in constraints.
|
||||
|
||||
When parsing a version an error is returned if there is an issue parsing the
|
||||
version. For example,
|
||||
|
||||
v, err := semver.NewVersion("1.2.3-beta.1+build345")
|
||||
|
||||
The version object has methods to get the parts of the version, compare it to
|
||||
other versions, convert the version back into a string, and get the original
|
||||
string. Getting the original string is useful if the semantic version was coerced
|
||||
into a valid form.
|
||||
|
||||
## Sorting Semantic Versions
|
||||
|
||||
A set of versions can be sorted using the `sort` package from the standard library.
|
||||
For example,
|
||||
|
||||
```go
|
||||
raw := []string{"1.2.3", "1.0", "1.3", "2", "0.4.2",}
|
||||
vs := make([]*semver.Version, len(raw))
|
||||
for i, r := range raw {
|
||||
v, err := semver.NewVersion(r)
|
||||
if err != nil {
|
||||
t.Errorf("Error parsing version: %s", err)
|
||||
}
|
||||
|
||||
vs[i] = v
|
||||
}
|
||||
|
||||
sort.Sort(semver.Collection(vs))
|
||||
```
|
||||
|
||||
## Checking Version Constraints
|
||||
|
||||
There are two methods for comparing versions. One uses comparison methods on
|
||||
`Version` instances and the other uses `Constraints`. There are some important
|
||||
differences to notes between these two methods of comparison.
|
||||
|
||||
1. When two versions are compared using functions such as `Compare`, `LessThan`,
|
||||
and others it will follow the specification and always include prereleases
|
||||
within the comparison. It will provide an answer that is valid with the
|
||||
comparison section of the spec at https://semver.org/#spec-item-11
|
||||
2. When constraint checking is used for checks or validation it will follow a
|
||||
different set of rules that are common for ranges with tools like npm/js
|
||||
and Rust/Cargo. This includes considering prereleases to be invalid if the
|
||||
ranges does not include one. If you want to have it include pre-releases a
|
||||
simple solution is to include `-0` in your range.
|
||||
3. Constraint ranges can have some complex rules including the shorthand use of
|
||||
~ and ^. For more details on those see the options below.
|
||||
|
||||
There are differences between the two methods or checking versions because the
|
||||
comparison methods on `Version` follow the specification while comparison ranges
|
||||
are not part of the specification. Different packages and tools have taken it
|
||||
upon themselves to come up with range rules. This has resulted in differences.
|
||||
For example, npm/js and Cargo/Rust follow similar patterns while PHP has a
|
||||
different pattern for ^. The comparison features in this package follow the
|
||||
npm/js and Cargo/Rust lead because applications using it have followed similar
|
||||
patters with their versions.
|
||||
|
||||
Checking a version against version constraints is one of the most featureful
|
||||
parts of the package.
|
||||
|
||||
```go
|
||||
c, err := semver.NewConstraint(">= 1.2.3")
|
||||
if err != nil {
|
||||
// Handle constraint not being parsable.
|
||||
}
|
||||
|
||||
v, err := semver.NewVersion("1.3")
|
||||
if err != nil {
|
||||
// Handle version not being parsable.
|
||||
}
|
||||
// Check if the version meets the constraints. The a variable will be true.
|
||||
a := c.Check(v)
|
||||
```
|
||||
|
||||
### Basic Comparisons
|
||||
|
||||
There are two elements to the comparisons. First, a comparison string is a list
|
||||
of space or comma separated AND comparisons. These are then separated by || (OR)
|
||||
comparisons. For example, `">= 1.2 < 3.0.0 || >= 4.2.3"` is looking for a
|
||||
comparison that's greater than or equal to 1.2 and less than 3.0.0 or is
|
||||
greater than or equal to 4.2.3.
|
||||
|
||||
The basic comparisons are:
|
||||
|
||||
* `=`: equal (aliased to no operator)
|
||||
* `!=`: not equal
|
||||
* `>`: greater than
|
||||
* `<`: less than
|
||||
* `>=`: greater than or equal to
|
||||
* `<=`: less than or equal to
|
||||
|
||||
### Working With Prerelease Versions
|
||||
|
||||
Pre-releases, for those not familiar with them, are used for software releases
|
||||
prior to stable or generally available releases. Examples of prereleases include
|
||||
development, alpha, beta, and release candidate releases. A prerelease may be
|
||||
a version such as `1.2.3-beta.1` while the stable release would be `1.2.3`. In the
|
||||
order of precedence, prereleases come before their associated releases. In this
|
||||
example `1.2.3-beta.1 < 1.2.3`.
|
||||
|
||||
According to the Semantic Version specification prereleases may not be
|
||||
API compliant with their release counterpart. It says,
|
||||
|
||||
> A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility requirements as denoted by its associated normal version.
|
||||
|
||||
SemVer comparisons using constraints without a prerelease comparator will skip
|
||||
prerelease versions. For example, `>=1.2.3` will skip prereleases when looking
|
||||
at a list of releases while `>=1.2.3-0` will evaluate and find prereleases.
|
||||
|
||||
The reason for the `0` as a pre-release version in the example comparison is
|
||||
because pre-releases can only contain ASCII alphanumerics and hyphens (along with
|
||||
`.` separators), per the spec. Sorting happens in ASCII sort order, again per the
|
||||
spec. The lowest character is a `0` in ASCII sort order
|
||||
(see an [ASCII Table](http://www.asciitable.com/))
|
||||
|
||||
Understanding ASCII sort ordering is important because A-Z comes before a-z. That
|
||||
means `>=1.2.3-BETA` will return `1.2.3-alpha`. What you might expect from case
|
||||
sensitivity doesn't apply here. This is due to ASCII sort ordering which is what
|
||||
the spec specifies.
|
||||
|
||||
### Hyphen Range Comparisons
|
||||
|
||||
There are multiple methods to handle ranges and the first is hyphens ranges.
|
||||
These look like:
|
||||
|
||||
* `1.2 - 1.4.5` which is equivalent to `>= 1.2 <= 1.4.5`
|
||||
* `2.3.4 - 4.5` which is equivalent to `>= 2.3.4 <= 4.5`
|
||||
|
||||
### Wildcards In Comparisons
|
||||
|
||||
The `x`, `X`, and `*` characters can be used as a wildcard character. This works
|
||||
for all comparison operators. When used on the `=` operator it falls
|
||||
back to the patch level comparison (see tilde below). For example,
|
||||
|
||||
* `1.2.x` is equivalent to `>= 1.2.0, < 1.3.0`
|
||||
* `>= 1.2.x` is equivalent to `>= 1.2.0`
|
||||
* `<= 2.x` is equivalent to `< 3`
|
||||
* `*` is equivalent to `>= 0.0.0`
|
||||
|
||||
### Tilde Range Comparisons (Patch)
|
||||
|
||||
The tilde (`~`) comparison operator is for patch level ranges when a minor
|
||||
version is specified and major level changes when the minor number is missing.
|
||||
For example,
|
||||
|
||||
* `~1.2.3` is equivalent to `>= 1.2.3, < 1.3.0`
|
||||
* `~1` is equivalent to `>= 1, < 2`
|
||||
* `~2.3` is equivalent to `>= 2.3, < 2.4`
|
||||
* `~1.2.x` is equivalent to `>= 1.2.0, < 1.3.0`
|
||||
* `~1.x` is equivalent to `>= 1, < 2`
|
||||
|
||||
### Caret Range Comparisons (Major)
|
||||
|
||||
The caret (`^`) comparison operator is for major level changes once a stable
|
||||
(1.0.0) release has occurred. Prior to a 1.0.0 release the minor versions acts
|
||||
as the API stability level. This is useful when comparisons of API versions as a
|
||||
major change is API breaking. For example,
|
||||
|
||||
* `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0`
|
||||
* `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0`
|
||||
* `^2.3` is equivalent to `>= 2.3, < 3`
|
||||
* `^2.x` is equivalent to `>= 2.0.0, < 3`
|
||||
* `^0.2.3` is equivalent to `>=0.2.3 <0.3.0`
|
||||
* `^0.2` is equivalent to `>=0.2.0 <0.3.0`
|
||||
* `^0.0.3` is equivalent to `>=0.0.3 <0.0.4`
|
||||
* `^0.0` is equivalent to `>=0.0.0 <0.1.0`
|
||||
* `^0` is equivalent to `>=0.0.0 <1.0.0`
|
||||
|
||||
## Validation
|
||||
|
||||
In addition to testing a version against a constraint, a version can be validated
|
||||
against a constraint. When validation fails a slice of errors containing why a
|
||||
version didn't meet the constraint is returned. For example,
|
||||
|
||||
```go
|
||||
c, err := semver.NewConstraint("<= 1.2.3, >= 1.4")
|
||||
if err != nil {
|
||||
// Handle constraint not being parseable.
|
||||
}
|
||||
|
||||
v, err := semver.NewVersion("1.3")
|
||||
if err != nil {
|
||||
// Handle version not being parseable.
|
||||
}
|
||||
|
||||
// Validate a version against a constraint.
|
||||
a, msgs := c.Validate(v)
|
||||
// a is false
|
||||
for _, m := range msgs {
|
||||
fmt.Println(m)
|
||||
|
||||
// Loops over the errors which would read
|
||||
// "1.3 is greater than 1.2.3"
|
||||
// "1.3 is less than 1.4"
|
||||
}
|
||||
```
|
||||
|
||||
## Contribute
|
||||
|
||||
If you find an issue or want to contribute please file an [issue](https://github.com/Masterminds/semver/issues)
|
||||
or [create a pull request](https://github.com/Masterminds/semver/pulls).
|
||||
24
vendor/github.com/Masterminds/semver/v3/collection.go
сгенерированный
поставляемый
Обычный файл
24
vendor/github.com/Masterminds/semver/v3/collection.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,24 @@
|
||||
package semver
|
||||
|
||||
// Collection is a collection of Version instances and implements the sort
|
||||
// interface. See the sort package for more details.
|
||||
// https://golang.org/pkg/sort/
|
||||
type Collection []*Version
|
||||
|
||||
// Len returns the length of a collection. The number of Version instances
|
||||
// on the slice.
|
||||
func (c Collection) Len() int {
|
||||
return len(c)
|
||||
}
|
||||
|
||||
// Less is needed for the sort interface to compare two Version objects on the
|
||||
// slice. If checks if one is less than the other.
|
||||
func (c Collection) Less(i, j int) bool {
|
||||
return c[i].LessThan(c[j])
|
||||
}
|
||||
|
||||
// Swap is needed for the sort interface to replace the Version objects
|
||||
// at two different positions in the slice.
|
||||
func (c Collection) Swap(i, j int) {
|
||||
c[i], c[j] = c[j], c[i]
|
||||
}
|
||||
571
vendor/github.com/Masterminds/semver/v3/constraints.go
сгенерированный
поставляемый
Обычный файл
571
vendor/github.com/Masterminds/semver/v3/constraints.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,571 @@
|
||||
package semver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Constraints is one or more constraint that a semantic version can be
|
||||
// checked against.
|
||||
type Constraints struct {
|
||||
constraints [][]*constraint
|
||||
}
|
||||
|
||||
// NewConstraint returns a Constraints instance that a Version instance can
|
||||
// be checked against. If there is a parse error it will be returned.
|
||||
func NewConstraint(c string) (*Constraints, error) {
|
||||
|
||||
// Rewrite - ranges into a comparison operation.
|
||||
c = rewriteRange(c)
|
||||
|
||||
ors := strings.Split(c, "||")
|
||||
or := make([][]*constraint, len(ors))
|
||||
for k, v := range ors {
|
||||
|
||||
// TODO: Find a way to validate and fetch all the constraints in a simpler form
|
||||
|
||||
// Validate the segment
|
||||
if !validConstraintRegex.MatchString(v) {
|
||||
return nil, fmt.Errorf("improper constraint: %s", v)
|
||||
}
|
||||
|
||||
cs := findConstraintRegex.FindAllString(v, -1)
|
||||
if cs == nil {
|
||||
cs = append(cs, v)
|
||||
}
|
||||
result := make([]*constraint, len(cs))
|
||||
for i, s := range cs {
|
||||
pc, err := parseConstraint(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result[i] = pc
|
||||
}
|
||||
or[k] = result
|
||||
}
|
||||
|
||||
o := &Constraints{constraints: or}
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// Check tests if a version satisfies the constraints.
|
||||
func (cs Constraints) Check(v *Version) bool {
|
||||
// TODO(mattfarina): For v4 of this library consolidate the Check and Validate
|
||||
// functions as the underlying functions make that possible now.
|
||||
// loop over the ORs and check the inner ANDs
|
||||
for _, o := range cs.constraints {
|
||||
joy := true
|
||||
for _, c := range o {
|
||||
if check, _ := c.check(v); !check {
|
||||
joy = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if joy {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate checks if a version satisfies a constraint. If not a slice of
|
||||
// reasons for the failure are returned in addition to a bool.
|
||||
func (cs Constraints) Validate(v *Version) (bool, []error) {
|
||||
// loop over the ORs and check the inner ANDs
|
||||
var e []error
|
||||
|
||||
// Capture the prerelease message only once. When it happens the first time
|
||||
// this var is marked
|
||||
var prerelesase bool
|
||||
for _, o := range cs.constraints {
|
||||
joy := true
|
||||
for _, c := range o {
|
||||
// Before running the check handle the case there the version is
|
||||
// a prerelease and the check is not searching for prereleases.
|
||||
if c.con.pre == "" && v.pre != "" {
|
||||
if !prerelesase {
|
||||
em := fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
|
||||
e = append(e, em)
|
||||
prerelesase = true
|
||||
}
|
||||
joy = false
|
||||
|
||||
} else {
|
||||
|
||||
if _, err := c.check(v); err != nil {
|
||||
e = append(e, err)
|
||||
joy = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if joy {
|
||||
return true, []error{}
|
||||
}
|
||||
}
|
||||
|
||||
return false, e
|
||||
}
|
||||
|
||||
func (cs Constraints) String() string {
|
||||
buf := make([]string, len(cs.constraints))
|
||||
var tmp bytes.Buffer
|
||||
|
||||
for k, v := range cs.constraints {
|
||||
tmp.Reset()
|
||||
vlen := len(v)
|
||||
for kk, c := range v {
|
||||
tmp.WriteString(c.string())
|
||||
|
||||
// Space separate the AND conditions
|
||||
if vlen > 1 && kk < vlen-1 {
|
||||
tmp.WriteString(" ")
|
||||
}
|
||||
}
|
||||
buf[k] = tmp.String()
|
||||
}
|
||||
|
||||
return strings.Join(buf, " || ")
|
||||
}
|
||||
|
||||
var constraintOps map[string]cfunc
|
||||
var constraintRegex *regexp.Regexp
|
||||
var constraintRangeRegex *regexp.Regexp
|
||||
|
||||
// Used to find individual constraints within a multi-constraint string
|
||||
var findConstraintRegex *regexp.Regexp
|
||||
|
||||
// Used to validate an segment of ANDs is valid
|
||||
var validConstraintRegex *regexp.Regexp
|
||||
|
||||
const cvRegex string = `v?([0-9|x|X|\*]+)(\.[0-9|x|X|\*]+)?(\.[0-9|x|X|\*]+)?` +
|
||||
`(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` +
|
||||
`(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?`
|
||||
|
||||
func init() {
|
||||
constraintOps = map[string]cfunc{
|
||||
"": constraintTildeOrEqual,
|
||||
"=": constraintTildeOrEqual,
|
||||
"!=": constraintNotEqual,
|
||||
">": constraintGreaterThan,
|
||||
"<": constraintLessThan,
|
||||
">=": constraintGreaterThanEqual,
|
||||
"=>": constraintGreaterThanEqual,
|
||||
"<=": constraintLessThanEqual,
|
||||
"=<": constraintLessThanEqual,
|
||||
"~": constraintTilde,
|
||||
"~>": constraintTilde,
|
||||
"^": constraintCaret,
|
||||
}
|
||||
|
||||
ops := make([]string, 0, len(constraintOps))
|
||||
for k := range constraintOps {
|
||||
ops = append(ops, regexp.QuoteMeta(k))
|
||||
}
|
||||
|
||||
constraintRegex = regexp.MustCompile(fmt.Sprintf(
|
||||
`^\s*(%s)\s*(%s)\s*$`,
|
||||
strings.Join(ops, "|"),
|
||||
cvRegex))
|
||||
|
||||
constraintRangeRegex = regexp.MustCompile(fmt.Sprintf(
|
||||
`\s*(%s)\s+-\s+(%s)\s*`,
|
||||
cvRegex, cvRegex))
|
||||
|
||||
findConstraintRegex = regexp.MustCompile(fmt.Sprintf(
|
||||
`(%s)\s*(%s)`,
|
||||
strings.Join(ops, "|"),
|
||||
cvRegex))
|
||||
|
||||
validConstraintRegex = regexp.MustCompile(fmt.Sprintf(
|
||||
`^(\s*(%s)\s*(%s)\s*\,?)+$`,
|
||||
strings.Join(ops, "|"),
|
||||
cvRegex))
|
||||
}
|
||||
|
||||
// An individual constraint
|
||||
type constraint struct {
|
||||
// The version used in the constraint check. For example, if a constraint
|
||||
// is '<= 2.0.0' the con a version instance representing 2.0.0.
|
||||
con *Version
|
||||
|
||||
// The original parsed version (e.g., 4.x from != 4.x)
|
||||
orig string
|
||||
|
||||
// The original operator for the constraint
|
||||
origfunc string
|
||||
|
||||
// When an x is used as part of the version (e.g., 1.x)
|
||||
minorDirty bool
|
||||
dirty bool
|
||||
patchDirty bool
|
||||
}
|
||||
|
||||
// Check if a version meets the constraint
|
||||
func (c *constraint) check(v *Version) (bool, error) {
|
||||
return constraintOps[c.origfunc](v, c)
|
||||
}
|
||||
|
||||
// String prints an individual constraint into a string
|
||||
func (c *constraint) string() string {
|
||||
return c.origfunc + c.orig
|
||||
}
|
||||
|
||||
type cfunc func(v *Version, c *constraint) (bool, error)
|
||||
|
||||
func parseConstraint(c string) (*constraint, error) {
|
||||
if len(c) > 0 {
|
||||
m := constraintRegex.FindStringSubmatch(c)
|
||||
if m == nil {
|
||||
return nil, fmt.Errorf("improper constraint: %s", c)
|
||||
}
|
||||
|
||||
cs := &constraint{
|
||||
orig: m[2],
|
||||
origfunc: m[1],
|
||||
}
|
||||
|
||||
ver := m[2]
|
||||
minorDirty := false
|
||||
patchDirty := false
|
||||
dirty := false
|
||||
if isX(m[3]) || m[3] == "" {
|
||||
ver = "0.0.0"
|
||||
dirty = true
|
||||
} else if isX(strings.TrimPrefix(m[4], ".")) || m[4] == "" {
|
||||
minorDirty = true
|
||||
dirty = true
|
||||
ver = fmt.Sprintf("%s.0.0%s", m[3], m[6])
|
||||
} else if isX(strings.TrimPrefix(m[5], ".")) || m[5] == "" {
|
||||
dirty = true
|
||||
patchDirty = true
|
||||
ver = fmt.Sprintf("%s%s.0%s", m[3], m[4], m[6])
|
||||
}
|
||||
|
||||
con, err := NewVersion(ver)
|
||||
if err != nil {
|
||||
|
||||
// The constraintRegex should catch any regex parsing errors. So,
|
||||
// we should never get here.
|
||||
return nil, errors.New("constraint Parser Error")
|
||||
}
|
||||
|
||||
cs.con = con
|
||||
cs.minorDirty = minorDirty
|
||||
cs.patchDirty = patchDirty
|
||||
cs.dirty = dirty
|
||||
|
||||
return cs, nil
|
||||
}
|
||||
|
||||
// The rest is the special case where an empty string was passed in which
|
||||
// is equivalent to * or >=0.0.0
|
||||
con, err := StrictNewVersion("0.0.0")
|
||||
if err != nil {
|
||||
|
||||
// The constraintRegex should catch any regex parsing errors. So,
|
||||
// we should never get here.
|
||||
return nil, errors.New("constraint Parser Error")
|
||||
}
|
||||
|
||||
cs := &constraint{
|
||||
con: con,
|
||||
orig: c,
|
||||
origfunc: "",
|
||||
minorDirty: false,
|
||||
patchDirty: false,
|
||||
dirty: true,
|
||||
}
|
||||
return cs, nil
|
||||
}
|
||||
|
||||
// Constraint functions
|
||||
func constraintNotEqual(v *Version, c *constraint) (bool, error) {
|
||||
if c.dirty {
|
||||
|
||||
// If there is a pre-release on the version but the constraint isn't looking
|
||||
// for them assume that pre-releases are not compatible. See issue 21 for
|
||||
// more details.
|
||||
if v.Prerelease() != "" && c.con.Prerelease() == "" {
|
||||
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
|
||||
}
|
||||
|
||||
if c.con.Major() != v.Major() {
|
||||
return true, nil
|
||||
}
|
||||
if c.con.Minor() != v.Minor() && !c.minorDirty {
|
||||
return true, nil
|
||||
} else if c.minorDirty {
|
||||
return false, fmt.Errorf("%s is equal to %s", v, c.orig)
|
||||
} else if c.con.Patch() != v.Patch() && !c.patchDirty {
|
||||
return true, nil
|
||||
} else if c.patchDirty {
|
||||
// Need to handle prereleases if present
|
||||
if v.Prerelease() != "" || c.con.Prerelease() != "" {
|
||||
eq := comparePrerelease(v.Prerelease(), c.con.Prerelease()) != 0
|
||||
if eq {
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("%s is equal to %s", v, c.orig)
|
||||
}
|
||||
return false, fmt.Errorf("%s is equal to %s", v, c.orig)
|
||||
}
|
||||
}
|
||||
|
||||
eq := v.Equal(c.con)
|
||||
if eq {
|
||||
return false, fmt.Errorf("%s is equal to %s", v, c.orig)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func constraintGreaterThan(v *Version, c *constraint) (bool, error) {
|
||||
|
||||
// If there is a pre-release on the version but the constraint isn't looking
|
||||
// for them assume that pre-releases are not compatible. See issue 21 for
|
||||
// more details.
|
||||
if v.Prerelease() != "" && c.con.Prerelease() == "" {
|
||||
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
|
||||
}
|
||||
|
||||
var eq bool
|
||||
|
||||
if !c.dirty {
|
||||
eq = v.Compare(c.con) == 1
|
||||
if eq {
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig)
|
||||
}
|
||||
|
||||
if v.Major() > c.con.Major() {
|
||||
return true, nil
|
||||
} else if v.Major() < c.con.Major() {
|
||||
return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig)
|
||||
} else if c.minorDirty {
|
||||
// This is a range case such as >11. When the version is something like
|
||||
// 11.1.0 is it not > 11. For that we would need 12 or higher
|
||||
return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig)
|
||||
} else if c.patchDirty {
|
||||
// This is for ranges such as >11.1. A version of 11.1.1 is not greater
|
||||
// which one of 11.2.1 is greater
|
||||
eq = v.Minor() > c.con.Minor()
|
||||
if eq {
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig)
|
||||
}
|
||||
|
||||
// If we have gotten here we are not comparing pre-preleases and can use the
|
||||
// Compare function to accomplish that.
|
||||
eq = v.Compare(c.con) == 1
|
||||
if eq {
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig)
|
||||
}
|
||||
|
||||
func constraintLessThan(v *Version, c *constraint) (bool, error) {
|
||||
// If there is a pre-release on the version but the constraint isn't looking
|
||||
// for them assume that pre-releases are not compatible. See issue 21 for
|
||||
// more details.
|
||||
if v.Prerelease() != "" && c.con.Prerelease() == "" {
|
||||
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
|
||||
}
|
||||
|
||||
eq := v.Compare(c.con) < 0
|
||||
if eq {
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("%s is greater than or equal to %s", v, c.orig)
|
||||
}
|
||||
|
||||
func constraintGreaterThanEqual(v *Version, c *constraint) (bool, error) {
|
||||
|
||||
// If there is a pre-release on the version but the constraint isn't looking
|
||||
// for them assume that pre-releases are not compatible. See issue 21 for
|
||||
// more details.
|
||||
if v.Prerelease() != "" && c.con.Prerelease() == "" {
|
||||
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
|
||||
}
|
||||
|
||||
eq := v.Compare(c.con) >= 0
|
||||
if eq {
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("%s is less than %s", v, c.orig)
|
||||
}
|
||||
|
||||
func constraintLessThanEqual(v *Version, c *constraint) (bool, error) {
|
||||
// If there is a pre-release on the version but the constraint isn't looking
|
||||
// for them assume that pre-releases are not compatible. See issue 21 for
|
||||
// more details.
|
||||
if v.Prerelease() != "" && c.con.Prerelease() == "" {
|
||||
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
|
||||
}
|
||||
|
||||
var eq bool
|
||||
|
||||
if !c.dirty {
|
||||
eq = v.Compare(c.con) <= 0
|
||||
if eq {
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("%s is greater than %s", v, c.orig)
|
||||
}
|
||||
|
||||
if v.Major() > c.con.Major() {
|
||||
return false, fmt.Errorf("%s is greater than %s", v, c.orig)
|
||||
} else if v.Major() == c.con.Major() && v.Minor() > c.con.Minor() && !c.minorDirty {
|
||||
return false, fmt.Errorf("%s is greater than %s", v, c.orig)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// ~*, ~>* --> >= 0.0.0 (any)
|
||||
// ~2, ~2.x, ~2.x.x, ~>2, ~>2.x ~>2.x.x --> >=2.0.0, <3.0.0
|
||||
// ~2.0, ~2.0.x, ~>2.0, ~>2.0.x --> >=2.0.0, <2.1.0
|
||||
// ~1.2, ~1.2.x, ~>1.2, ~>1.2.x --> >=1.2.0, <1.3.0
|
||||
// ~1.2.3, ~>1.2.3 --> >=1.2.3, <1.3.0
|
||||
// ~1.2.0, ~>1.2.0 --> >=1.2.0, <1.3.0
|
||||
func constraintTilde(v *Version, c *constraint) (bool, error) {
|
||||
// If there is a pre-release on the version but the constraint isn't looking
|
||||
// for them assume that pre-releases are not compatible. See issue 21 for
|
||||
// more details.
|
||||
if v.Prerelease() != "" && c.con.Prerelease() == "" {
|
||||
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
|
||||
}
|
||||
|
||||
if v.LessThan(c.con) {
|
||||
return false, fmt.Errorf("%s is less than %s", v, c.orig)
|
||||
}
|
||||
|
||||
// ~0.0.0 is a special case where all constraints are accepted. It's
|
||||
// equivalent to >= 0.0.0.
|
||||
if c.con.Major() == 0 && c.con.Minor() == 0 && c.con.Patch() == 0 &&
|
||||
!c.minorDirty && !c.patchDirty {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
if v.Major() != c.con.Major() {
|
||||
return false, fmt.Errorf("%s does not have same major version as %s", v, c.orig)
|
||||
}
|
||||
|
||||
if v.Minor() != c.con.Minor() && !c.minorDirty {
|
||||
return false, fmt.Errorf("%s does not have same major and minor version as %s", v, c.orig)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// When there is a .x (dirty) status it automatically opts in to ~. Otherwise
|
||||
// it's a straight =
|
||||
func constraintTildeOrEqual(v *Version, c *constraint) (bool, error) {
|
||||
// If there is a pre-release on the version but the constraint isn't looking
|
||||
// for them assume that pre-releases are not compatible. See issue 21 for
|
||||
// more details.
|
||||
if v.Prerelease() != "" && c.con.Prerelease() == "" {
|
||||
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
|
||||
}
|
||||
|
||||
if c.dirty {
|
||||
return constraintTilde(v, c)
|
||||
}
|
||||
|
||||
eq := v.Equal(c.con)
|
||||
if eq {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("%s is not equal to %s", v, c.orig)
|
||||
}
|
||||
|
||||
// ^* --> (any)
|
||||
// ^1.2.3 --> >=1.2.3 <2.0.0
|
||||
// ^1.2 --> >=1.2.0 <2.0.0
|
||||
// ^1 --> >=1.0.0 <2.0.0
|
||||
// ^0.2.3 --> >=0.2.3 <0.3.0
|
||||
// ^0.2 --> >=0.2.0 <0.3.0
|
||||
// ^0.0.3 --> >=0.0.3 <0.0.4
|
||||
// ^0.0 --> >=0.0.0 <0.1.0
|
||||
// ^0 --> >=0.0.0 <1.0.0
|
||||
func constraintCaret(v *Version, c *constraint) (bool, error) {
|
||||
// If there is a pre-release on the version but the constraint isn't looking
|
||||
// for them assume that pre-releases are not compatible. See issue 21 for
|
||||
// more details.
|
||||
if v.Prerelease() != "" && c.con.Prerelease() == "" {
|
||||
return false, fmt.Errorf("%s is a prerelease version and the constraint is only looking for release versions", v)
|
||||
}
|
||||
|
||||
// This less than handles prereleases
|
||||
if v.LessThan(c.con) {
|
||||
return false, fmt.Errorf("%s is less than %s", v, c.orig)
|
||||
}
|
||||
|
||||
var eq bool
|
||||
|
||||
// ^ when the major > 0 is >=x.y.z < x+1
|
||||
if c.con.Major() > 0 || c.minorDirty {
|
||||
|
||||
// ^ has to be within a major range for > 0. Everything less than was
|
||||
// filtered out with the LessThan call above. This filters out those
|
||||
// that greater but not within the same major range.
|
||||
eq = v.Major() == c.con.Major()
|
||||
if eq {
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("%s does not have same major version as %s", v, c.orig)
|
||||
}
|
||||
|
||||
// ^ when the major is 0 and minor > 0 is >=0.y.z < 0.y+1
|
||||
if c.con.Major() == 0 && v.Major() > 0 {
|
||||
return false, fmt.Errorf("%s does not have same major version as %s", v, c.orig)
|
||||
}
|
||||
// If the con Minor is > 0 it is not dirty
|
||||
if c.con.Minor() > 0 || c.patchDirty {
|
||||
eq = v.Minor() == c.con.Minor()
|
||||
if eq {
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("%s does not have same minor version as %s. Expected minor versions to match when constraint major version is 0", v, c.orig)
|
||||
}
|
||||
|
||||
// At this point the major is 0 and the minor is 0 and not dirty. The patch
|
||||
// is not dirty so we need to check if they are equal. If they are not equal
|
||||
eq = c.con.Patch() == v.Patch()
|
||||
if eq {
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("%s does not equal %s. Expect version and constraint to equal when major and minor versions are 0", v, c.orig)
|
||||
}
|
||||
|
||||
func isX(x string) bool {
|
||||
switch x {
|
||||
case "x", "*", "X":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func rewriteRange(i string) string {
|
||||
m := constraintRangeRegex.FindAllStringSubmatch(i, -1)
|
||||
if m == nil {
|
||||
return i
|
||||
}
|
||||
o := i
|
||||
for _, v := range m {
|
||||
t := fmt.Sprintf(">= %s, <= %s", v[1], v[11])
|
||||
o = strings.Replace(o, v[0], t, 1)
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
184
vendor/github.com/Masterminds/semver/v3/doc.go
сгенерированный
поставляемый
Обычный файл
184
vendor/github.com/Masterminds/semver/v3/doc.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
Package semver provides the ability to work with Semantic Versions (http://semver.org) in Go.
|
||||
|
||||
Specifically it provides the ability to:
|
||||
|
||||
* Parse semantic versions
|
||||
* Sort semantic versions
|
||||
* Check if a semantic version fits within a set of constraints
|
||||
* Optionally work with a `v` prefix
|
||||
|
||||
Parsing Semantic Versions
|
||||
|
||||
There are two functions that can parse semantic versions. The `StrictNewVersion`
|
||||
function only parses valid version 2 semantic versions as outlined in the
|
||||
specification. The `NewVersion` function attempts to coerce a version into a
|
||||
semantic version and parse it. For example, if there is a leading v or a version
|
||||
listed without all 3 parts (e.g. 1.2) it will attempt to coerce it into a valid
|
||||
semantic version (e.g., 1.2.0). In both cases a `Version` object is returned
|
||||
that can be sorted, compared, and used in constraints.
|
||||
|
||||
When parsing a version an optional error can be returned if there is an issue
|
||||
parsing the version. For example,
|
||||
|
||||
v, err := semver.NewVersion("1.2.3-beta.1+b345")
|
||||
|
||||
The version object has methods to get the parts of the version, compare it to
|
||||
other versions, convert the version back into a string, and get the original
|
||||
string. For more details please see the documentation
|
||||
at https://godoc.org/github.com/Masterminds/semver.
|
||||
|
||||
Sorting Semantic Versions
|
||||
|
||||
A set of versions can be sorted using the `sort` package from the standard library.
|
||||
For example,
|
||||
|
||||
raw := []string{"1.2.3", "1.0", "1.3", "2", "0.4.2",}
|
||||
vs := make([]*semver.Version, len(raw))
|
||||
for i, r := range raw {
|
||||
v, err := semver.NewVersion(r)
|
||||
if err != nil {
|
||||
t.Errorf("Error parsing version: %s", err)
|
||||
}
|
||||
|
||||
vs[i] = v
|
||||
}
|
||||
|
||||
sort.Sort(semver.Collection(vs))
|
||||
|
||||
Checking Version Constraints and Comparing Versions
|
||||
|
||||
There are two methods for comparing versions. One uses comparison methods on
|
||||
`Version` instances and the other is using Constraints. There are some important
|
||||
differences to notes between these two methods of comparison.
|
||||
|
||||
1. When two versions are compared using functions such as `Compare`, `LessThan`,
|
||||
and others it will follow the specification and always include prereleases
|
||||
within the comparison. It will provide an answer valid with the comparison
|
||||
spec section at https://semver.org/#spec-item-11
|
||||
2. When constraint checking is used for checks or validation it will follow a
|
||||
different set of rules that are common for ranges with tools like npm/js
|
||||
and Rust/Cargo. This includes considering prereleases to be invalid if the
|
||||
ranges does not include on. If you want to have it include pre-releases a
|
||||
simple solution is to include `-0` in your range.
|
||||
3. Constraint ranges can have some complex rules including the shorthard use of
|
||||
~ and ^. For more details on those see the options below.
|
||||
|
||||
There are differences between the two methods or checking versions because the
|
||||
comparison methods on `Version` follow the specification while comparison ranges
|
||||
are not part of the specification. Different packages and tools have taken it
|
||||
upon themselves to come up with range rules. This has resulted in differences.
|
||||
For example, npm/js and Cargo/Rust follow similar patterns which PHP has a
|
||||
different pattern for ^. The comparison features in this package follow the
|
||||
npm/js and Cargo/Rust lead because applications using it have followed similar
|
||||
patters with their versions.
|
||||
|
||||
Checking a version against version constraints is one of the most featureful
|
||||
parts of the package.
|
||||
|
||||
c, err := semver.NewConstraint(">= 1.2.3")
|
||||
if err != nil {
|
||||
// Handle constraint not being parsable.
|
||||
}
|
||||
|
||||
v, err := semver.NewVersion("1.3")
|
||||
if err != nil {
|
||||
// Handle version not being parsable.
|
||||
}
|
||||
// Check if the version meets the constraints. The a variable will be true.
|
||||
a := c.Check(v)
|
||||
|
||||
Basic Comparisons
|
||||
|
||||
There are two elements to the comparisons. First, a comparison string is a list
|
||||
of comma or space separated AND comparisons. These are then separated by || (OR)
|
||||
comparisons. For example, `">= 1.2 < 3.0.0 || >= 4.2.3"` is looking for a
|
||||
comparison that's greater than or equal to 1.2 and less than 3.0.0 or is
|
||||
greater than or equal to 4.2.3. This can also be written as
|
||||
`">= 1.2, < 3.0.0 || >= 4.2.3"`
|
||||
|
||||
The basic comparisons are:
|
||||
|
||||
* `=`: equal (aliased to no operator)
|
||||
* `!=`: not equal
|
||||
* `>`: greater than
|
||||
* `<`: less than
|
||||
* `>=`: greater than or equal to
|
||||
* `<=`: less than or equal to
|
||||
|
||||
Hyphen Range Comparisons
|
||||
|
||||
There are multiple methods to handle ranges and the first is hyphens ranges.
|
||||
These look like:
|
||||
|
||||
* `1.2 - 1.4.5` which is equivalent to `>= 1.2, <= 1.4.5`
|
||||
* `2.3.4 - 4.5` which is equivalent to `>= 2.3.4 <= 4.5`
|
||||
|
||||
Wildcards In Comparisons
|
||||
|
||||
The `x`, `X`, and `*` characters can be used as a wildcard character. This works
|
||||
for all comparison operators. When used on the `=` operator it falls
|
||||
back to the tilde operation. For example,
|
||||
|
||||
* `1.2.x` is equivalent to `>= 1.2.0 < 1.3.0`
|
||||
* `>= 1.2.x` is equivalent to `>= 1.2.0`
|
||||
* `<= 2.x` is equivalent to `<= 3`
|
||||
* `*` is equivalent to `>= 0.0.0`
|
||||
|
||||
Tilde Range Comparisons (Patch)
|
||||
|
||||
The tilde (`~`) comparison operator is for patch level ranges when a minor
|
||||
version is specified and major level changes when the minor number is missing.
|
||||
For example,
|
||||
|
||||
* `~1.2.3` is equivalent to `>= 1.2.3 < 1.3.0`
|
||||
* `~1` is equivalent to `>= 1, < 2`
|
||||
* `~2.3` is equivalent to `>= 2.3 < 2.4`
|
||||
* `~1.2.x` is equivalent to `>= 1.2.0 < 1.3.0`
|
||||
* `~1.x` is equivalent to `>= 1 < 2`
|
||||
|
||||
Caret Range Comparisons (Major)
|
||||
|
||||
The caret (`^`) comparison operator is for major level changes once a stable
|
||||
(1.0.0) release has occurred. Prior to a 1.0.0 release the minor versions acts
|
||||
as the API stability level. This is useful when comparisons of API versions as a
|
||||
major change is API breaking. For example,
|
||||
|
||||
* `^1.2.3` is equivalent to `>= 1.2.3, < 2.0.0`
|
||||
* `^1.2.x` is equivalent to `>= 1.2.0, < 2.0.0`
|
||||
* `^2.3` is equivalent to `>= 2.3, < 3`
|
||||
* `^2.x` is equivalent to `>= 2.0.0, < 3`
|
||||
* `^0.2.3` is equivalent to `>=0.2.3 <0.3.0`
|
||||
* `^0.2` is equivalent to `>=0.2.0 <0.3.0`
|
||||
* `^0.0.3` is equivalent to `>=0.0.3 <0.0.4`
|
||||
* `^0.0` is equivalent to `>=0.0.0 <0.1.0`
|
||||
* `^0` is equivalent to `>=0.0.0 <1.0.0`
|
||||
|
||||
Validation
|
||||
|
||||
In addition to testing a version against a constraint, a version can be validated
|
||||
against a constraint. When validation fails a slice of errors containing why a
|
||||
version didn't meet the constraint is returned. For example,
|
||||
|
||||
c, err := semver.NewConstraint("<= 1.2.3, >= 1.4")
|
||||
if err != nil {
|
||||
// Handle constraint not being parseable.
|
||||
}
|
||||
|
||||
v, _ := semver.NewVersion("1.3")
|
||||
if err != nil {
|
||||
// Handle version not being parseable.
|
||||
}
|
||||
|
||||
// Validate a version against a constraint.
|
||||
a, msgs := c.Validate(v)
|
||||
// a is false
|
||||
for _, m := range msgs {
|
||||
fmt.Println(m)
|
||||
|
||||
// Loops over the errors which would read
|
||||
// "1.3 is greater than 1.2.3"
|
||||
// "1.3 is less than 1.4"
|
||||
}
|
||||
*/
|
||||
package semver
|
||||
22
vendor/github.com/Masterminds/semver/v3/fuzz.go
сгенерированный
поставляемый
Обычный файл
22
vendor/github.com/Masterminds/semver/v3/fuzz.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,22 @@
|
||||
// +build gofuzz
|
||||
|
||||
package semver
|
||||
|
||||
func Fuzz(data []byte) int {
|
||||
d := string(data)
|
||||
|
||||
// Test NewVersion
|
||||
_, _ = NewVersion(d)
|
||||
|
||||
// Test StrictNewVersion
|
||||
_, _ = StrictNewVersion(d)
|
||||
|
||||
// Test NewConstraint
|
||||
_, _ = NewConstraint(d)
|
||||
|
||||
// The return value should be 0 normally, 1 if the priority in future tests
|
||||
// should be increased, and -1 if future tests should skip passing in that
|
||||
// data. We do not have a reason to change priority so 0 is always returned.
|
||||
// There are example tests that do this.
|
||||
return 0
|
||||
}
|
||||
3
vendor/github.com/Masterminds/semver/v3/go.mod
сгенерированный
поставляемый
Обычный файл
3
vendor/github.com/Masterminds/semver/v3/go.mod
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,3 @@
|
||||
module github.com/Masterminds/semver/v3
|
||||
|
||||
go 1.12
|
||||
606
vendor/github.com/Masterminds/semver/v3/version.go
сгенерированный
поставляемый
Обычный файл
606
vendor/github.com/Masterminds/semver/v3/version.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,606 @@
|
||||
package semver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// The compiled version of the regex created at init() is cached here so it
|
||||
// only needs to be created once.
|
||||
var versionRegex *regexp.Regexp
|
||||
|
||||
var (
|
||||
// ErrInvalidSemVer is returned a version is found to be invalid when
|
||||
// being parsed.
|
||||
ErrInvalidSemVer = errors.New("Invalid Semantic Version")
|
||||
|
||||
// ErrEmptyString is returned when an empty string is passed in for parsing.
|
||||
ErrEmptyString = errors.New("Version string empty")
|
||||
|
||||
// ErrInvalidCharacters is returned when invalid characters are found as
|
||||
// part of a version
|
||||
ErrInvalidCharacters = errors.New("Invalid characters in version")
|
||||
|
||||
// ErrSegmentStartsZero is returned when a version segment starts with 0.
|
||||
// This is invalid in SemVer.
|
||||
ErrSegmentStartsZero = errors.New("Version segment starts with 0")
|
||||
|
||||
// ErrInvalidMetadata is returned when the metadata is an invalid format
|
||||
ErrInvalidMetadata = errors.New("Invalid Metadata string")
|
||||
|
||||
// ErrInvalidPrerelease is returned when the pre-release is an invalid format
|
||||
ErrInvalidPrerelease = errors.New("Invalid Prerelease string")
|
||||
)
|
||||
|
||||
// semVerRegex is the regular expression used to parse a semantic version.
|
||||
const semVerRegex string = `v?([0-9]+)(\.[0-9]+)?(\.[0-9]+)?` +
|
||||
`(-([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?` +
|
||||
`(\+([0-9A-Za-z\-]+(\.[0-9A-Za-z\-]+)*))?`
|
||||
|
||||
// Version represents a single semantic version.
|
||||
type Version struct {
|
||||
major, minor, patch uint64
|
||||
pre string
|
||||
metadata string
|
||||
original string
|
||||
}
|
||||
|
||||
func init() {
|
||||
versionRegex = regexp.MustCompile("^" + semVerRegex + "$")
|
||||
}
|
||||
|
||||
const num string = "0123456789"
|
||||
const allowed string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-" + num
|
||||
|
||||
// StrictNewVersion parses a given version and returns an instance of Version or
|
||||
// an error if unable to parse the version. Only parses valid semantic versions.
|
||||
// Performs checking that can find errors within the version.
|
||||
// If you want to coerce a version, such as 1 or 1.2, and perse that as the 1.x
|
||||
// releases of semver provided use the NewSemver() function.
|
||||
func StrictNewVersion(v string) (*Version, error) {
|
||||
// Parsing here does not use RegEx in order to increase performance and reduce
|
||||
// allocations.
|
||||
|
||||
if len(v) == 0 {
|
||||
return nil, ErrEmptyString
|
||||
}
|
||||
|
||||
// Split the parts into [0]major, [1]minor, and [2]patch,prerelease,build
|
||||
parts := strings.SplitN(v, ".", 3)
|
||||
if len(parts) != 3 {
|
||||
return nil, ErrInvalidSemVer
|
||||
}
|
||||
|
||||
sv := &Version{
|
||||
original: v,
|
||||
}
|
||||
|
||||
// check for prerelease or build metadata
|
||||
var extra []string
|
||||
if strings.ContainsAny(parts[2], "-+") {
|
||||
// Start with the build metadata first as it needs to be on the right
|
||||
extra = strings.SplitN(parts[2], "+", 2)
|
||||
if len(extra) > 1 {
|
||||
// build metadata found
|
||||
sv.metadata = extra[1]
|
||||
parts[2] = extra[0]
|
||||
}
|
||||
|
||||
extra = strings.SplitN(parts[2], "-", 2)
|
||||
if len(extra) > 1 {
|
||||
// prerelease found
|
||||
sv.pre = extra[1]
|
||||
parts[2] = extra[0]
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the number segments are valid. This includes only having positive
|
||||
// numbers and no leading 0's.
|
||||
for _, p := range parts {
|
||||
if !containsOnly(p, num) {
|
||||
return nil, ErrInvalidCharacters
|
||||
}
|
||||
|
||||
if len(p) > 1 && p[0] == '0' {
|
||||
return nil, ErrSegmentStartsZero
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the major, minor, and patch elements onto the returned Version
|
||||
var err error
|
||||
sv.major, err = strconv.ParseUint(parts[0], 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sv.minor, err = strconv.ParseUint(parts[1], 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sv.patch, err = strconv.ParseUint(parts[2], 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// No prerelease or build metadata found so returning now as a fastpath.
|
||||
if sv.pre == "" && sv.metadata == "" {
|
||||
return sv, nil
|
||||
}
|
||||
|
||||
if sv.pre != "" {
|
||||
if err = validatePrerelease(sv.pre); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if sv.metadata != "" {
|
||||
if err = validateMetadata(sv.metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return sv, nil
|
||||
}
|
||||
|
||||
// NewVersion parses a given version and returns an instance of Version or
|
||||
// an error if unable to parse the version. If the version is SemVer-ish it
|
||||
// attempts to convert it to SemVer. If you want to validate it was a strict
|
||||
// semantic version at parse time see StrictNewVersion().
|
||||
func NewVersion(v string) (*Version, error) {
|
||||
m := versionRegex.FindStringSubmatch(v)
|
||||
if m == nil {
|
||||
return nil, ErrInvalidSemVer
|
||||
}
|
||||
|
||||
sv := &Version{
|
||||
metadata: m[8],
|
||||
pre: m[5],
|
||||
original: v,
|
||||
}
|
||||
|
||||
var err error
|
||||
sv.major, err = strconv.ParseUint(m[1], 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error parsing version segment: %s", err)
|
||||
}
|
||||
|
||||
if m[2] != "" {
|
||||
sv.minor, err = strconv.ParseUint(strings.TrimPrefix(m[2], "."), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error parsing version segment: %s", err)
|
||||
}
|
||||
} else {
|
||||
sv.minor = 0
|
||||
}
|
||||
|
||||
if m[3] != "" {
|
||||
sv.patch, err = strconv.ParseUint(strings.TrimPrefix(m[3], "."), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error parsing version segment: %s", err)
|
||||
}
|
||||
} else {
|
||||
sv.patch = 0
|
||||
}
|
||||
|
||||
// Perform some basic due diligence on the extra parts to ensure they are
|
||||
// valid.
|
||||
|
||||
if sv.pre != "" {
|
||||
if err = validatePrerelease(sv.pre); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if sv.metadata != "" {
|
||||
if err = validateMetadata(sv.metadata); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return sv, nil
|
||||
}
|
||||
|
||||
// MustParse parses a given version and panics on error.
|
||||
func MustParse(v string) *Version {
|
||||
sv, err := NewVersion(v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return sv
|
||||
}
|
||||
|
||||
// String converts a Version object to a string.
|
||||
// Note, if the original version contained a leading v this version will not.
|
||||
// See the Original() method to retrieve the original value. Semantic Versions
|
||||
// don't contain a leading v per the spec. Instead it's optional on
|
||||
// implementation.
|
||||
func (v Version) String() string {
|
||||
var buf bytes.Buffer
|
||||
|
||||
fmt.Fprintf(&buf, "%d.%d.%d", v.major, v.minor, v.patch)
|
||||
if v.pre != "" {
|
||||
fmt.Fprintf(&buf, "-%s", v.pre)
|
||||
}
|
||||
if v.metadata != "" {
|
||||
fmt.Fprintf(&buf, "+%s", v.metadata)
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// Original returns the original value passed in to be parsed.
|
||||
func (v *Version) Original() string {
|
||||
return v.original
|
||||
}
|
||||
|
||||
// Major returns the major version.
|
||||
func (v Version) Major() uint64 {
|
||||
return v.major
|
||||
}
|
||||
|
||||
// Minor returns the minor version.
|
||||
func (v Version) Minor() uint64 {
|
||||
return v.minor
|
||||
}
|
||||
|
||||
// Patch returns the patch version.
|
||||
func (v Version) Patch() uint64 {
|
||||
return v.patch
|
||||
}
|
||||
|
||||
// Prerelease returns the pre-release version.
|
||||
func (v Version) Prerelease() string {
|
||||
return v.pre
|
||||
}
|
||||
|
||||
// Metadata returns the metadata on the version.
|
||||
func (v Version) Metadata() string {
|
||||
return v.metadata
|
||||
}
|
||||
|
||||
// originalVPrefix returns the original 'v' prefix if any.
|
||||
func (v Version) originalVPrefix() string {
|
||||
|
||||
// Note, only lowercase v is supported as a prefix by the parser.
|
||||
if v.original != "" && v.original[:1] == "v" {
|
||||
return v.original[:1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// IncPatch produces the next patch version.
|
||||
// If the current version does not have prerelease/metadata information,
|
||||
// it unsets metadata and prerelease values, increments patch number.
|
||||
// If the current version has any of prerelease or metadata information,
|
||||
// it unsets both values and keeps current patch value
|
||||
func (v Version) IncPatch() Version {
|
||||
vNext := v
|
||||
// according to http://semver.org/#spec-item-9
|
||||
// Pre-release versions have a lower precedence than the associated normal version.
|
||||
// according to http://semver.org/#spec-item-10
|
||||
// Build metadata SHOULD be ignored when determining version precedence.
|
||||
if v.pre != "" {
|
||||
vNext.metadata = ""
|
||||
vNext.pre = ""
|
||||
} else {
|
||||
vNext.metadata = ""
|
||||
vNext.pre = ""
|
||||
vNext.patch = v.patch + 1
|
||||
}
|
||||
vNext.original = v.originalVPrefix() + "" + vNext.String()
|
||||
return vNext
|
||||
}
|
||||
|
||||
// IncMinor produces the next minor version.
|
||||
// Sets patch to 0.
|
||||
// Increments minor number.
|
||||
// Unsets metadata.
|
||||
// Unsets prerelease status.
|
||||
func (v Version) IncMinor() Version {
|
||||
vNext := v
|
||||
vNext.metadata = ""
|
||||
vNext.pre = ""
|
||||
vNext.patch = 0
|
||||
vNext.minor = v.minor + 1
|
||||
vNext.original = v.originalVPrefix() + "" + vNext.String()
|
||||
return vNext
|
||||
}
|
||||
|
||||
// IncMajor produces the next major version.
|
||||
// Sets patch to 0.
|
||||
// Sets minor to 0.
|
||||
// Increments major number.
|
||||
// Unsets metadata.
|
||||
// Unsets prerelease status.
|
||||
func (v Version) IncMajor() Version {
|
||||
vNext := v
|
||||
vNext.metadata = ""
|
||||
vNext.pre = ""
|
||||
vNext.patch = 0
|
||||
vNext.minor = 0
|
||||
vNext.major = v.major + 1
|
||||
vNext.original = v.originalVPrefix() + "" + vNext.String()
|
||||
return vNext
|
||||
}
|
||||
|
||||
// SetPrerelease defines the prerelease value.
|
||||
// Value must not include the required 'hyphen' prefix.
|
||||
func (v Version) SetPrerelease(prerelease string) (Version, error) {
|
||||
vNext := v
|
||||
if len(prerelease) > 0 {
|
||||
if err := validatePrerelease(prerelease); err != nil {
|
||||
return vNext, err
|
||||
}
|
||||
}
|
||||
vNext.pre = prerelease
|
||||
vNext.original = v.originalVPrefix() + "" + vNext.String()
|
||||
return vNext, nil
|
||||
}
|
||||
|
||||
// SetMetadata defines metadata value.
|
||||
// Value must not include the required 'plus' prefix.
|
||||
func (v Version) SetMetadata(metadata string) (Version, error) {
|
||||
vNext := v
|
||||
if len(metadata) > 0 {
|
||||
if err := validateMetadata(metadata); err != nil {
|
||||
return vNext, err
|
||||
}
|
||||
}
|
||||
vNext.metadata = metadata
|
||||
vNext.original = v.originalVPrefix() + "" + vNext.String()
|
||||
return vNext, nil
|
||||
}
|
||||
|
||||
// LessThan tests if one version is less than another one.
|
||||
func (v *Version) LessThan(o *Version) bool {
|
||||
return v.Compare(o) < 0
|
||||
}
|
||||
|
||||
// GreaterThan tests if one version is greater than another one.
|
||||
func (v *Version) GreaterThan(o *Version) bool {
|
||||
return v.Compare(o) > 0
|
||||
}
|
||||
|
||||
// Equal tests if two versions are equal to each other.
|
||||
// Note, versions can be equal with different metadata since metadata
|
||||
// is not considered part of the comparable version.
|
||||
func (v *Version) Equal(o *Version) bool {
|
||||
return v.Compare(o) == 0
|
||||
}
|
||||
|
||||
// Compare compares this version to another one. It returns -1, 0, or 1 if
|
||||
// the version smaller, equal, or larger than the other version.
|
||||
//
|
||||
// Versions are compared by X.Y.Z. Build metadata is ignored. Prerelease is
|
||||
// lower than the version without a prerelease. Compare always takes into account
|
||||
// prereleases. If you want to work with ranges using typical range syntaxes that
|
||||
// skip prereleases if the range is not looking for them use constraints.
|
||||
func (v *Version) Compare(o *Version) int {
|
||||
// Compare the major, minor, and patch version for differences. If a
|
||||
// difference is found return the comparison.
|
||||
if d := compareSegment(v.Major(), o.Major()); d != 0 {
|
||||
return d
|
||||
}
|
||||
if d := compareSegment(v.Minor(), o.Minor()); d != 0 {
|
||||
return d
|
||||
}
|
||||
if d := compareSegment(v.Patch(), o.Patch()); d != 0 {
|
||||
return d
|
||||
}
|
||||
|
||||
// At this point the major, minor, and patch versions are the same.
|
||||
ps := v.pre
|
||||
po := o.Prerelease()
|
||||
|
||||
if ps == "" && po == "" {
|
||||
return 0
|
||||
}
|
||||
if ps == "" {
|
||||
return 1
|
||||
}
|
||||
if po == "" {
|
||||
return -1
|
||||
}
|
||||
|
||||
return comparePrerelease(ps, po)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements JSON.Unmarshaler interface.
|
||||
func (v *Version) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
temp, err := NewVersion(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.major = temp.major
|
||||
v.minor = temp.minor
|
||||
v.patch = temp.patch
|
||||
v.pre = temp.pre
|
||||
v.metadata = temp.metadata
|
||||
v.original = temp.original
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON implements JSON.Marshaler interface.
|
||||
func (v Version) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(v.String())
|
||||
}
|
||||
|
||||
// Scan implements the SQL.Scanner interface.
|
||||
func (v *Version) Scan(value interface{}) error {
|
||||
var s string
|
||||
s, _ = value.(string)
|
||||
temp, err := NewVersion(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
v.major = temp.major
|
||||
v.minor = temp.minor
|
||||
v.patch = temp.patch
|
||||
v.pre = temp.pre
|
||||
v.metadata = temp.metadata
|
||||
v.original = temp.original
|
||||
return nil
|
||||
}
|
||||
|
||||
// Value implements the Driver.Valuer interface.
|
||||
func (v Version) Value() (driver.Value, error) {
|
||||
return v.String(), nil
|
||||
}
|
||||
|
||||
func compareSegment(v, o uint64) int {
|
||||
if v < o {
|
||||
return -1
|
||||
}
|
||||
if v > o {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func comparePrerelease(v, o string) int {
|
||||
|
||||
// split the prelease versions by their part. The separator, per the spec,
|
||||
// is a .
|
||||
sparts := strings.Split(v, ".")
|
||||
oparts := strings.Split(o, ".")
|
||||
|
||||
// Find the longer length of the parts to know how many loop iterations to
|
||||
// go through.
|
||||
slen := len(sparts)
|
||||
olen := len(oparts)
|
||||
|
||||
l := slen
|
||||
if olen > slen {
|
||||
l = olen
|
||||
}
|
||||
|
||||
// Iterate over each part of the prereleases to compare the differences.
|
||||
for i := 0; i < l; i++ {
|
||||
// Since the lentgh of the parts can be different we need to create
|
||||
// a placeholder. This is to avoid out of bounds issues.
|
||||
stemp := ""
|
||||
if i < slen {
|
||||
stemp = sparts[i]
|
||||
}
|
||||
|
||||
otemp := ""
|
||||
if i < olen {
|
||||
otemp = oparts[i]
|
||||
}
|
||||
|
||||
d := comparePrePart(stemp, otemp)
|
||||
if d != 0 {
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
// Reaching here means two versions are of equal value but have different
|
||||
// metadata (the part following a +). They are not identical in string form
|
||||
// but the version comparison finds them to be equal.
|
||||
return 0
|
||||
}
|
||||
|
||||
func comparePrePart(s, o string) int {
|
||||
// Fastpath if they are equal
|
||||
if s == o {
|
||||
return 0
|
||||
}
|
||||
|
||||
// When s or o are empty we can use the other in an attempt to determine
|
||||
// the response.
|
||||
if s == "" {
|
||||
if o != "" {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
if o == "" {
|
||||
if s != "" {
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// When comparing strings "99" is greater than "103". To handle
|
||||
// cases like this we need to detect numbers and compare them. According
|
||||
// to the semver spec, numbers are always positive. If there is a - at the
|
||||
// start like -99 this is to be evaluated as an alphanum. numbers always
|
||||
// have precedence over alphanum. Parsing as Uints because negative numbers
|
||||
// are ignored.
|
||||
|
||||
oi, n1 := strconv.ParseUint(o, 10, 64)
|
||||
si, n2 := strconv.ParseUint(s, 10, 64)
|
||||
|
||||
// The case where both are strings compare the strings
|
||||
if n1 != nil && n2 != nil {
|
||||
if s > o {
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
} else if n1 != nil {
|
||||
// o is a string and s is a number
|
||||
return -1
|
||||
} else if n2 != nil {
|
||||
// s is a string and o is a number
|
||||
return 1
|
||||
}
|
||||
// Both are numbers
|
||||
if si > oi {
|
||||
return 1
|
||||
}
|
||||
return -1
|
||||
|
||||
}
|
||||
|
||||
// Like strings.ContainsAny but does an only instead of any.
|
||||
func containsOnly(s string, comp string) bool {
|
||||
return strings.IndexFunc(s, func(r rune) bool {
|
||||
return !strings.ContainsRune(comp, r)
|
||||
}) == -1
|
||||
}
|
||||
|
||||
// From the spec, "Identifiers MUST comprise only
|
||||
// ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty.
|
||||
// Numeric identifiers MUST NOT include leading zeroes.". These segments can
|
||||
// be dot separated.
|
||||
func validatePrerelease(p string) error {
|
||||
eparts := strings.Split(p, ".")
|
||||
for _, p := range eparts {
|
||||
if containsOnly(p, num) {
|
||||
if len(p) > 1 && p[0] == '0' {
|
||||
return ErrSegmentStartsZero
|
||||
}
|
||||
} else if !containsOnly(p, allowed) {
|
||||
return ErrInvalidPrerelease
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// From the spec, "Build metadata MAY be denoted by
|
||||
// appending a plus sign and a series of dot separated identifiers immediately
|
||||
// following the patch or pre-release version. Identifiers MUST comprise only
|
||||
// ASCII alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty."
|
||||
func validateMetadata(m string) error {
|
||||
eparts := strings.Split(m, ".")
|
||||
for _, p := range eparts {
|
||||
if !containsOnly(p, allowed) {
|
||||
return ErrInvalidMetadata
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
3
vendor/github.com/reflog/dateconstraints/.gitignore
сгенерированный
поставляемый
Обычный файл
3
vendor/github.com/reflog/dateconstraints/.gitignore
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,3 @@
|
||||
.vscode
|
||||
.idea
|
||||
*.iml
|
||||
21
vendor/github.com/reflog/dateconstraints/LICENSE
сгенерированный
поставляемый
Обычный файл
21
vendor/github.com/reflog/dateconstraints/LICENSE
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) [year] [fullname]
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
84
vendor/github.com/reflog/dateconstraints/README.md
сгенерированный
поставляемый
Обычный файл
84
vendor/github.com/reflog/dateconstraints/README.md
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,84 @@
|
||||
# Date Constraints
|
||||
|
||||
### Validate a date against constraints
|
||||
|
||||
## Overview [](https://godoc.org/github.com/reflog/dateconstraints)
|
||||
|
||||
This module is heavily based on https://github.com/Masterminds/semver so kudos to [Masterminds](https://github.com/Masterminds/semver).
|
||||
|
||||
> _For now only RFC3339 dates are supported_
|
||||
|
||||
## Basic Comparisons
|
||||
|
||||
There are two elements to the comparisons. First, a comparison string is a list
|
||||
of space or comma separated AND comparisons. These are then separated by || (OR)
|
||||
comparisons. For example, `">= 2020-03-01T00:00:00Z < 2020-04-01T00:00:00Z || >= 2020-05-01T00:00:00Z"` is will validate if a date is between 01/03/2020 till 01/04/2020 OR it's after 01/05/2020.
|
||||
|
||||
The basic comparisons are:
|
||||
|
||||
- `=`: equal
|
||||
- `!=`: not equal
|
||||
- `>`: greater than
|
||||
- `<`: less than
|
||||
- `>=`: greater than or equal to
|
||||
- `<=`: less than or equal to
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
|
||||
import "github.com/reflog/dateconstraints"
|
||||
import "time"
|
||||
|
||||
func main(){
|
||||
|
||||
date, _ := time.Parse(time.RFC3339, "2020-03-10T00:00:00Z")
|
||||
c, _ := date_constraints.NewConstraint("> 2020-03-01T00:00:00Z <= 2020-04-01T00:00:00Z")
|
||||
if c.Check(&date) {
|
||||
// date is in range!
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
In addition to testing a date against a constraint, it can be validated
|
||||
against a constraint. When validation fails a slice of errors containing why a
|
||||
date didn't meet the constraint is returned. For example,
|
||||
|
||||
```go
|
||||
c, err := date_constraints.NewConstraint("<= 2020-03-01T00:00:00Z, >= 2020-04-10T00:00:00Z")
|
||||
if err != nil {
|
||||
// Handle constraint not being parseable.
|
||||
}
|
||||
v, err := time.Parse(time.RFC3339, "2020-03-10T00:00:00Z")
|
||||
if err != nil {
|
||||
// Handle date not being parseable.
|
||||
}
|
||||
// Validate a date against a constraint.
|
||||
a, msgs := c.Validate(&v)
|
||||
// a is false
|
||||
for _, m := range msgs {
|
||||
fmt.Println(m)
|
||||
// Loops over the errors which would read
|
||||
// "2020-03-10T00:00:00Z is greater than 2020-03-01T00:00:00Z"
|
||||
// "2020-03-01T00:00:00Z is less than 2020-04-01T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
go get github.com/reflog/dateconstraints
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
|
||||
|
||||
Please make sure to update tests as appropriate.
|
||||
|
||||
## License
|
||||
|
||||
[MIT](https://choosealicense.com/licenses/mit/)
|
||||
277
vendor/github.com/reflog/dateconstraints/constraints.go
сгенерированный
поставляемый
Обычный файл
277
vendor/github.com/reflog/dateconstraints/constraints.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,277 @@
|
||||
package date_constraints
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Constraints is one or more constraint that a date can be
|
||||
// checked against.
|
||||
type Constraints struct {
|
||||
constraints [][]*constraint
|
||||
}
|
||||
|
||||
// NewConstraint returns a Constraints instance that a time.Time instance can
|
||||
// be checked against. If there is a parse error it will be returned.
|
||||
func NewConstraint(c string) (*Constraints, error) {
|
||||
|
||||
// Rewrite - ranges into a comparison operation.
|
||||
c = rewriteRange(c)
|
||||
|
||||
ors := strings.Split(c, "||")
|
||||
or := make([][]*constraint, len(ors))
|
||||
for k, v := range ors {
|
||||
|
||||
// TODO: Find a way to validate and fetch all the constraints in a simpler form
|
||||
|
||||
// Validate the segment
|
||||
if !validConstraintRegex.MatchString(v) {
|
||||
return nil, fmt.Errorf("improper constraint: %s", v)
|
||||
}
|
||||
|
||||
cs := findConstraintRegex.FindAllString(v, -1)
|
||||
if cs == nil {
|
||||
cs = append(cs, v)
|
||||
}
|
||||
result := make([]*constraint, len(cs))
|
||||
for i, s := range cs {
|
||||
pc, err := parseConstraint(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result[i] = pc
|
||||
}
|
||||
or[k] = result
|
||||
}
|
||||
|
||||
o := &Constraints{constraints: or}
|
||||
return o, nil
|
||||
}
|
||||
|
||||
// Check tests if a date satisfies the constraints.
|
||||
func (cs Constraints) Check(v *time.Time) bool {
|
||||
for _, o := range cs.constraints {
|
||||
joy := true
|
||||
for _, c := range o {
|
||||
if check, _ := c.check(v); !check {
|
||||
joy = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if joy {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// Validate checks if a date satisfies a constraint. If not a slice of
|
||||
// reasons for the failure are returned in addition to a bool.
|
||||
func (cs Constraints) Validate(v *time.Time) (bool, []error) {
|
||||
// loop over the ORs and check the inner ANDs
|
||||
var e []error
|
||||
|
||||
for _, o := range cs.constraints {
|
||||
joy := true
|
||||
for _, c := range o {
|
||||
if _, err := c.check(v); err != nil {
|
||||
e = append(e, err)
|
||||
joy = false
|
||||
}
|
||||
}
|
||||
|
||||
if joy {
|
||||
return true, []error{}
|
||||
}
|
||||
}
|
||||
|
||||
return false, e
|
||||
}
|
||||
|
||||
func (cs Constraints) String() string {
|
||||
buf := make([]string, len(cs.constraints))
|
||||
var tmp bytes.Buffer
|
||||
|
||||
for k, v := range cs.constraints {
|
||||
tmp.Reset()
|
||||
vlen := len(v)
|
||||
for kk, c := range v {
|
||||
tmp.WriteString(c.string())
|
||||
|
||||
// Space separate the AND conditions
|
||||
if vlen > 1 && kk < vlen-1 {
|
||||
tmp.WriteString(" ")
|
||||
}
|
||||
}
|
||||
buf[k] = tmp.String()
|
||||
}
|
||||
|
||||
return strings.Join(buf, " || ")
|
||||
}
|
||||
|
||||
var constraintOps map[string]cfunc
|
||||
var constraintRegex *regexp.Regexp
|
||||
var constraintRangeRegex *regexp.Regexp
|
||||
|
||||
// Used to find individual constraints within a multi-constraint string
|
||||
var findConstraintRegex *regexp.Regexp
|
||||
|
||||
// Used to validate an segment of ANDs is valid
|
||||
var validConstraintRegex *regexp.Regexp
|
||||
|
||||
const cvRegex string = `\d{4}(-\d\d(-\d\d(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?)?)?`
|
||||
|
||||
func init() {
|
||||
constraintOps = map[string]cfunc{
|
||||
"!=": constraintNotEqual,
|
||||
"=": constraintEqual,
|
||||
">": constraintGreaterThan,
|
||||
"<": constraintLessThan,
|
||||
">=": constraintGreaterThanEqual,
|
||||
"=>": constraintGreaterThanEqual,
|
||||
"<=": constraintLessThanEqual,
|
||||
"=<": constraintLessThanEqual,
|
||||
}
|
||||
|
||||
ops := make([]string, 0, len(constraintOps))
|
||||
for k := range constraintOps {
|
||||
ops = append(ops, regexp.QuoteMeta(k))
|
||||
}
|
||||
|
||||
constraintRegex = regexp.MustCompile(fmt.Sprintf(
|
||||
`^\s*(%s)\s*(%s)\s*$`,
|
||||
strings.Join(ops, "|"),
|
||||
cvRegex))
|
||||
|
||||
constraintRangeRegex = regexp.MustCompile(fmt.Sprintf(
|
||||
`\s*(%s)\s+-\s+(%s)\s*`,
|
||||
cvRegex, cvRegex))
|
||||
|
||||
findConstraintRegex = regexp.MustCompile(fmt.Sprintf(
|
||||
`(%s)\s*(%s)`,
|
||||
strings.Join(ops, "|"),
|
||||
cvRegex))
|
||||
|
||||
validConstraintRegex = regexp.MustCompile(fmt.Sprintf(
|
||||
`^(\s*(%s)\s*(%s)\s*\,?)+$`,
|
||||
strings.Join(ops, "|"),
|
||||
cvRegex))
|
||||
}
|
||||
|
||||
// An individual constraint
|
||||
type constraint struct {
|
||||
// The time used in the constraint check. For example, if a constraint
|
||||
// is '<= 2020-03-01T00:00:00Z' then con is an instance representing 2020-03-01T00:00:00Z.
|
||||
con *time.Time
|
||||
|
||||
// The original parsed date (e.g., 2020-03-01T00:00:00Z)
|
||||
orig string
|
||||
|
||||
// The original operator for the constraint (e.g. <=)
|
||||
origfunc string
|
||||
}
|
||||
|
||||
// Check if a date meets the constraint
|
||||
func (c *constraint) check(v *time.Time) (bool, error) {
|
||||
return constraintOps[c.origfunc](v, c)
|
||||
}
|
||||
|
||||
// String prints an individual constraint into a string
|
||||
func (c *constraint) string() string {
|
||||
return c.origfunc + c.orig
|
||||
}
|
||||
|
||||
type cfunc func(v *time.Time, c *constraint) (bool, error)
|
||||
|
||||
func parseConstraint(c string) (*constraint, error) {
|
||||
if len(c) > 0 {
|
||||
m := constraintRegex.FindStringSubmatch(c)
|
||||
if m == nil {
|
||||
return nil, fmt.Errorf("improper constraint: %s", c)
|
||||
}
|
||||
|
||||
cs := &constraint{
|
||||
orig: m[2],
|
||||
origfunc: m[1],
|
||||
}
|
||||
|
||||
con, err := time.Parse(time.RFC3339, m[2])
|
||||
if err != nil {
|
||||
|
||||
// The constraintRegex should catch any regex parsing errors. So,
|
||||
// we should never get here.
|
||||
return nil, errors.New("constraint Parser Error")
|
||||
}
|
||||
|
||||
cs.con = &con
|
||||
|
||||
return cs, nil
|
||||
}
|
||||
return nil, errors.New("constraint Parser Error")
|
||||
}
|
||||
|
||||
// Constraint functions
|
||||
func constraintNotEqual(v *time.Time, c *constraint) (bool, error) {
|
||||
if v.Equal(*c.con) {
|
||||
return false, fmt.Errorf("%s is equal to %s", v, c.orig)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func constraintEqual(v *time.Time, c *constraint) (bool, error) {
|
||||
if !v.Equal(*c.con) {
|
||||
return false, fmt.Errorf("%s is not equal to %s", v, c.orig)
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func constraintGreaterThan(v *time.Time, c *constraint) (bool, error) {
|
||||
if v.After(*c.con) {
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("%s is less than or equal to %s", v, c.orig)
|
||||
}
|
||||
|
||||
func constraintLessThan(v *time.Time, c *constraint) (bool, error) {
|
||||
if v.Before(*c.con) {
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("%s is greater than or equal to %s", v, c.orig)
|
||||
}
|
||||
|
||||
func constraintGreaterThanEqual(v *time.Time, c *constraint) (bool, error) {
|
||||
if v.After(*c.con) || v.Equal(*c.con) {
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("%s is less than %s", v, c.orig)
|
||||
}
|
||||
|
||||
func constraintLessThanEqual(v *time.Time, c *constraint) (bool, error) {
|
||||
if v.Before(*c.con) || v.Equal(*c.con) {
|
||||
return true, nil
|
||||
}
|
||||
return false, fmt.Errorf("%s is greater than %s", v, c.orig)
|
||||
}
|
||||
|
||||
func rewriteRange(i string) string {
|
||||
m := constraintRangeRegex.FindAllStringSubmatch(i, -1)
|
||||
if m == nil {
|
||||
return i
|
||||
}
|
||||
o := i
|
||||
for _, v := range m {
|
||||
t := fmt.Sprintf(">= %s, <= %s", v[1], v[11])
|
||||
o = strings.Replace(o, v[0], t, 1)
|
||||
}
|
||||
|
||||
return o
|
||||
}
|
||||
5
vendor/github.com/reflog/dateconstraints/go.mod
сгенерированный
поставляемый
Обычный файл
5
vendor/github.com/reflog/dateconstraints/go.mod
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,5 @@
|
||||
module github.com/reflog/dateconstraints
|
||||
|
||||
go 1.14
|
||||
|
||||
require github.com/stretchr/testify v1.6.1
|
||||
11
vendor/github.com/reflog/dateconstraints/go.sum
сгенерированный
поставляемый
Обычный файл
11
vendor/github.com/reflog/dateconstraints/go.sum
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,11 @@
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
6
vendor/modules.txt
поставляемый
6
vendor/modules.txt
поставляемый
@@ -1,3 +1,6 @@
|
||||
# github.com/Masterminds/semver/v3 v3.1.0
|
||||
## explicit
|
||||
github.com/Masterminds/semver/v3
|
||||
# github.com/Masterminds/squirrel v1.4.0
|
||||
## explicit
|
||||
github.com/Masterminds/squirrel
|
||||
@@ -400,6 +403,9 @@ github.com/prometheus/common/model
|
||||
github.com/prometheus/procfs
|
||||
github.com/prometheus/procfs/internal/fs
|
||||
github.com/prometheus/procfs/internal/util
|
||||
# github.com/reflog/dateconstraints v0.2.1
|
||||
## explicit
|
||||
github.com/reflog/dateconstraints
|
||||
# github.com/remyoudompheng/bigfft v0.0.0-20190728182440-6a916e37a237
|
||||
## explicit
|
||||
# github.com/rs/cors v1.7.0
|
||||
|
||||
@@ -72,7 +72,8 @@ func setupTestHelper(t testing.TB, store store.Store, includeCacheLayer bool) *T
|
||||
if err != nil {
|
||||
panic("failed to initialize memory store: " + err.Error())
|
||||
}
|
||||
|
||||
*memoryStore.Get().AnnouncementSettings.AdminNoticesEnabled = false
|
||||
*memoryStore.Get().AnnouncementSettings.UserNoticesEnabled = false
|
||||
var options []app.Option
|
||||
options = append(options, app.ConfigStore(memoryStore))
|
||||
options = append(options, app.StoreOverride(mainHelper.Store))
|
||||
|
||||
Ссылка в новой задаче
Block a user