MM-27918 In-Product notices support (#15316)

Этот коммит содержится в:
Eli Yukelzon
2020-09-21 10:28:46 +03:00
коммит произвёл GitHub
родитель 43ed6ad690
Коммит 4e9ddd4686
63 изменённых файлов: 4549 добавлений и 7 удалений

Просмотреть файл

@@ -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:

Просмотреть файл

@@ -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

Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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, &noticesCache, 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 Обычный файл
Просмотреть файл

@@ -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
Просмотреть файл

@@ -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
Просмотреть файл

@@ -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=

Просмотреть файл

@@ -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"
)

Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 its 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 Обычный файл
Просмотреть файл

@@ -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(&noticeStates, 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(&noticeStates[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(&noticeStates, sql, args...); err != nil {
return nil, errors.Wrapf(err, "failed to get ProductNoticeViewState with userId=%s", userId)
}
return noticeStates, nil
}

Просмотреть файл

@@ -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)

Просмотреть файл

@@ -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 Обычный файл
Просмотреть файл

@@ -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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1 @@
_fuzz/

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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -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
[![Stability:
Active](https://masterminds.github.io/stability/active.svg)](https://masterminds.github.io/stability/active.html)
[![](https://github.com/Masterminds/semver/workflows/Tests/badge.svg)](https://github.com/Masterminds/semver/actions)
[![GoDoc](https://img.shields.io/static/v1?label=godoc&message=reference&color=blue)](https://pkg.go.dev/github.com/Masterminds/semver/v3)
[![Go Report Card](https://goreportcard.com/badge/github.com/Masterminds/semver)](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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,3 @@
module github.com/Masterminds/semver/v3
go 1.12

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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,3 @@
.vscode
.idea
*.iml

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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,84 @@
# Date Constraints
### Validate a date against constraints
## Overview [![GoDoc](https://godoc.org/github.com/reflog/dateconstraints?status.svg)](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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -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 поставляемый
Просмотреть файл

@@ -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))