Merge remote-tracking branch 'upstream/master' into MM-47560

Этот коммит содержится в:
Emmy Ni
2022-10-21 13:08:59 -04:00
родитель 2bed35ec1c 6ac0faf99e
Коммит eb67874cd6
75 изменённых файлов: 2305 добавлений и 491 удалений

14
.github/codeql/codeql-config.yml поставляемый Обычный файл
Просмотреть файл

@@ -0,0 +1,14 @@
name: "CodeQL config"
query-filters:
- exclude:
problem.severity:
- warning
- recommendation
- exclude:
id: go/log-injection
paths-ignore:
- templates
- tests
- 'api4/*_local.go'

21
.github/workflows/codeql-analysis.yml поставляемый
Просмотреть файл

@@ -6,7 +6,7 @@ on:
branches: [ master ]
schedule:
- cron: '30 5,17 * * *'
permissions:
contents: read
@@ -20,17 +20,24 @@ jobs:
strategy:
fail-fast: false
matrix:
language: [ 'go', 'javascript' ]
language: [ 'go' ]
steps:
- name: Checkout repository
uses: actions/checkout@v2
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
debug: true
debug: false
config-file: ./.github/codeql/codeql-config.yml
# Autobuild attempts to build any compiled languages
- name: Autobuild
uses: github/codeql-action/autobuild@v1
uses: github/codeql-action/autobuild@v2
# Perform Analysis
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2

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

@@ -160,7 +160,7 @@ PLUGIN_PACKAGES += mattermost-plugin-jira-v2.4.0
PLUGIN_PACKAGES += mattermost-plugin-nps-v1.2.0
PLUGIN_PACKAGES += mattermost-plugin-welcomebot-v1.2.0
PLUGIN_PACKAGES += mattermost-plugin-zoom-v1.6.0
PLUGIN_PACKAGES += focalboard-v7.4.2
PLUGIN_PACKAGES += focalboard-v7.4.3
PLUGIN_PACKAGES += mattermost-plugin-apps-v1.1.0
# Prepares the enterprise build if exists. The IGNORE stuff is a hack to get the Makefile to execute the commands outside a target

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

@@ -1779,6 +1779,7 @@ func TestSearchGroupChannels(t *testing.T) {
}
func TestDeleteChannel(t *testing.T) {
t.Skip("MM-47465")
th := Setup(t).InitBasic()
defer th.TearDown()
c := th.Client

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

@@ -158,14 +158,14 @@ func requestCloudTrial(c *Context, w http.ResponseWriter, r *http.Request) {
// check if the email needs to be set
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
// this value will not be empty when both emails (user admin and CWS customer) are not business email and
// we need to request a new email from the user via the request business email modal
// a new business email was provided via the request business email modal
var startTrialRequest *model.StartCloudTrialRequest
if err = json.Unmarshal(bodyBytes, &startTrialRequest); err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
@@ -199,20 +199,20 @@ func validateBusinessEmail(c *Context, w http.ResponseWriter, r *http.Request) {
user, appErr := c.App.GetUser(c.AppContext.Session().UserId)
if appErr != nil {
c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.request_error", nil, "", http.StatusInternalServerError).Wrap(appErr)
c.Err = model.NewAppError("Api4.validateBusinessEmail", "api.cloud.request_error", nil, "", http.StatusForbidden).Wrap(appErr)
return
}
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
var emailToValidate *model.ValidateBusinessEmailRequest
err = json.Unmarshal(bodyBytes, &emailToValidate)
if err != nil {
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusInternalServerError).Wrap(err)
c.Err = model.NewAppError("Api4.requestCloudTrial", "api.cloud.app_error", nil, "", http.StatusBadRequest).Wrap(err)
return
}
@@ -244,14 +244,14 @@ func validateWorkspaceBusinessEmail(c *Context, w http.ResponseWriter, r *http.R
user, userErr := c.App.GetUser(c.AppContext.Session().UserId)
if userErr != nil {
c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.request_error", nil, userErr.Error(), http.StatusInternalServerError)
c.Err = userErr
return
}
// get the cloud customer email to validate if is a valid business email
cloudCustomer, err := c.App.Cloud().GetCloudCustomer(user.Id)
if err != nil {
c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.request_error", nil, err.Error(), http.StatusInternalServerError)
c.Err = model.NewAppError("Api4.validateWorkspaceBusinessEmail", "api.cloud.request_error", nil, err.Error(), http.StatusBadRequest)
return
}
emailErr := c.App.Cloud().ValidateBusinessEmail(user.Id, cloudCustomer.Email)

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

@@ -296,6 +296,20 @@ func Test_requestTrial(t *testing.T) {
require.Equal(t, subscriptionChanged, subscription)
require.Equal(t, http.StatusOK, r.StatusCode, "Status OK")
})
t.Run("Empty body returns bad request", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
r, err := th.SystemAdminClient.DoAPIPutBytes("/cloud/request-trial", nil)
require.Error(t, err)
closeBody(r)
require.Equal(t, http.StatusBadRequest, r.StatusCode, "Status Bad Request")
})
}
func Test_validateBusinessEmail(t *testing.T) {
@@ -373,6 +387,20 @@ func Test_validateBusinessEmail(t *testing.T) {
require.NoError(t, err)
require.Equal(t, http.StatusOK, res.StatusCode, "200")
})
t.Run("Empty body returns bad request", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
r, err := th.SystemAdminClient.DoAPIPostBytes("/cloud/validate-business-email", nil)
require.Error(t, err)
closeBody(r)
require.Equal(t, http.StatusBadRequest, r.StatusCode, "Status Bad Request")
})
}
func Test_validateWorkspaceBusinessEmail(t *testing.T) {
@@ -442,6 +470,39 @@ func Test_validateWorkspaceBusinessEmail(t *testing.T) {
_, err := th.SystemAdminClient.ValidateWorkspaceBusinessEmail()
require.NoError(t, err)
})
t.Run("Error while grabbing the cloud customer returns bad request", func(t *testing.T) {
th := Setup(t).InitBasic()
defer th.TearDown()
th.Client.Login(th.BasicUser.Email, th.BasicUser.Password)
th.App.Srv().SetLicense(model.NewTestLicense("cloud"))
cloud := mocks.CloudInterface{}
cloudCustomerInfo := model.CloudCustomerInfo{
Email: "badrequest@gmail.com",
}
// return an error while getting the cloud customer so we validate the forbidden error return
cloud.Mock.On("GetCloudCustomer", th.SystemAdminUser.Id).Return(nil, errors.New("error while gettings the cloud customer"))
// required cloud mocks so the request doesn't fail
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, cloudCustomerInfo.Email).Return(errors.New("invalid email"))
cloud.Mock.On("ValidateBusinessEmail", th.SystemAdminUser.Id, th.SystemAdminUser.Email).Return(nil)
cloudImpl := th.App.Srv().Cloud
defer func() {
th.App.Srv().Cloud = cloudImpl
}()
th.App.Srv().Cloud = &cloud
r, err := th.SystemAdminClient.DoAPIPostBytes("/cloud/validate-workspace-business-email", nil)
require.Error(t, err)
closeBody(r)
require.Equal(t, http.StatusBadRequest, r.StatusCode, "Status Bad Request")
})
}
func TestGetCloudProducts(t *testing.T) {

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

@@ -18,82 +18,88 @@ import (
func (api *API) InitGroup() {
// GET /api/v4/groups
api.BaseRoutes.Groups.Handle("", api.APISessionRequired(requireLicense(getGroups))).Methods("GET")
api.BaseRoutes.Groups.Handle("", api.APISessionRequired(getGroups)).Methods("GET")
// POST /api/v4/groups
api.BaseRoutes.Groups.Handle("", api.APISessionRequired(requireLicense(createGroup))).Methods("POST")
api.BaseRoutes.Groups.Handle("", api.APISessionRequired(createGroup)).Methods("POST")
// GET /api/v4/groups/:group_id
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}",
api.APISessionRequired(requireLicense(getGroup))).Methods("GET")
api.APISessionRequired(getGroup)).Methods("GET")
// PUT /api/v4/groups/:group_id/patch
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/patch",
api.APISessionRequired(requireLicense(patchGroup))).Methods("PUT")
api.APISessionRequired(patchGroup)).Methods("PUT")
// POST /api/v4/groups/:group_id/teams/:team_id/link
// POST /api/v4/groups/:group_id/channels/:channel_id/link
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/link",
api.APISessionRequired(requireLicense(linkGroupSyncable))).Methods("POST")
api.APISessionRequired(linkGroupSyncable)).Methods("POST")
// DELETE /api/v4/groups/:group_id/teams/:team_id/link
// DELETE /api/v4/groups/:group_id/channels/:channel_id/link
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/link",
api.APISessionRequired(requireLicense(unlinkGroupSyncable))).Methods("DELETE")
api.APISessionRequired(unlinkGroupSyncable)).Methods("DELETE")
// GET /api/v4/groups/:group_id/teams/:team_id
// GET /api/v4/groups/:group_id/channels/:channel_id
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}",
api.APISessionRequired(requireLicense(getGroupSyncable))).Methods("GET")
api.APISessionRequired(getGroupSyncable)).Methods("GET")
// GET /api/v4/groups/:group_id/teams
// GET /api/v4/groups/:group_id/channels
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}",
api.APISessionRequired(requireLicense(getGroupSyncables))).Methods("GET")
api.APISessionRequired(getGroupSyncables)).Methods("GET")
// PUT /api/v4/groups/:group_id/teams/:team_id/patch
// PUT /api/v4/groups/:group_id/channels/:channel_id/patch
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/{syncable_type:teams|channels}/{syncable_id:[A-Za-z0-9]+}/patch",
api.APISessionRequired(requireLicense(patchGroupSyncable))).Methods("PUT")
api.APISessionRequired(patchGroupSyncable)).Methods("PUT")
// GET /api/v4/groups/:group_id/stats
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/stats",
api.APISessionRequired(requireLicense(getGroupStats))).Methods("GET")
api.APISessionRequired(getGroupStats)).Methods("GET")
// GET /api/v4/groups/:group_id/members
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members",
api.APISessionRequired(requireLicense(getGroupMembers))).Methods("GET")
api.APISessionRequired(getGroupMembers)).Methods("GET")
// GET /api/v4/users/:user_id/groups
api.BaseRoutes.Users.Handle("/{user_id:[A-Za-z0-9]+}/groups",
api.APISessionRequired(requireLicense(getGroupsByUserId))).Methods("GET")
api.APISessionRequired(getGroupsByUserId)).Methods("GET")
// GET /api/v4/channels/:channel_id/groups
api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/groups",
api.APISessionRequired(requireLicense(getGroupsByChannel))).Methods("GET")
api.APISessionRequired(getGroupsByChannel)).Methods("GET")
// GET /api/v4/teams/:team_id/groups
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups",
api.APISessionRequired(requireLicense(getGroupsByTeam))).Methods("GET")
api.APISessionRequired(getGroupsByTeam)).Methods("GET")
// GET /api/v4/teams/:team_id/groups_by_channels
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups_by_channels",
api.APISessionRequired(requireLicense(getGroupsAssociatedToChannelsByTeam))).Methods("GET")
api.APISessionRequired(getGroupsAssociatedToChannelsByTeam)).Methods("GET")
// DELETE /api/v4/groups/:group_id
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}",
api.APISessionRequired(requireLicense(deleteGroup))).Methods("DELETE")
api.APISessionRequired(deleteGroup)).Methods("DELETE")
// POST /api/v4/groups/:group_id/members
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members",
api.APISessionRequired(requireLicense(addGroupMembers))).Methods("POST")
api.APISessionRequired(addGroupMembers)).Methods("POST")
// DELETE /api/v4/groups/:group_id/members
api.BaseRoutes.Groups.Handle("/{group_id:[A-Za-z0-9]+}/members",
api.APISessionRequired(requireLicense(deleteGroupMembers))).Methods("DELETE")
api.APISessionRequired(deleteGroupMembers)).Methods("DELETE")
}
func getGroup(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
@@ -130,6 +136,11 @@ func getGroup(c *Context, w http.ResponseWriter, r *http.Request) {
}
func createGroup(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
var group *model.GroupWithUserIds
if err := json.NewDecoder(r.Body).Decode(&group); err != nil {
c.SetInvalidParamWithErr("group", err)
@@ -185,6 +196,11 @@ func createGroup(c *Context, w http.ResponseWriter, r *http.Request) {
}
func patchGroup(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
@@ -277,6 +293,11 @@ func patchGroup(c *Context, w http.ResponseWriter, r *http.Request) {
}
func linkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
@@ -368,6 +389,11 @@ func linkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
@@ -411,6 +437,11 @@ func getGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getGroupSyncables(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
@@ -448,6 +479,11 @@ func getGroupSyncables(c *Context, w http.ResponseWriter, r *http.Request) {
}
func patchGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
@@ -529,6 +565,11 @@ func patchGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
}
func unlinkGroupSyncable(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
@@ -606,6 +647,11 @@ func verifyLinkUnlinkPermission(c *Context, syncableType model.GroupSyncableType
}
func getGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
@@ -651,6 +697,11 @@ func getGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getGroupStats(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
@@ -686,6 +737,11 @@ func getGroupStats(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getGroupsByUserId(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireUserId()
if c.Err != nil {
return
@@ -717,72 +773,46 @@ func getGroupsByUserId(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getGroupsByChannel(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireChannelId()
if c.Err != nil {
return
}
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getGroupsByChannel", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
return
}
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
b, appErr := getGroupsByChannelCommon(c, r)
if appErr != nil {
c.Err = appErr
return
}
var permission *model.Permission
if channel.Type == model.ChannelTypePrivate {
permission = model.PermissionReadPrivateChannelGroups
} else {
permission = model.PermissionReadPublicChannelGroups
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, permission) {
c.SetPermissionError(permission)
return
}
opts := model.GroupSearchOpts{
Q: c.Params.Q,
IncludeMemberCount: c.Params.IncludeMemberCount,
FilterAllowReference: c.Params.FilterAllowReference,
}
if c.Params.Paginate == nil || *c.Params.Paginate {
opts.PageOpts = &model.PageOpts{Page: c.Params.Page, PerPage: c.Params.PerPage}
}
groups, totalCount, appErr := c.App.GetGroupsByChannel(c.Params.ChannelId, opts)
if appErr != nil {
c.Err = appErr
return
}
b, err := json.Marshal(struct {
Groups []*model.GroupWithSchemeAdmin `json:"groups"`
Count int `json:"total_group_count"`
}{
Groups: groups,
Count: totalCount,
})
if err != nil {
c.Err = model.NewAppError("Api4.getGroupsByChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
}
w.Write(b)
}
func getGroupsByTeam(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
}
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
c.Err = model.NewAppError("Api4.getGroupsByTeam", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
b, appError := getGroupsByTeamCommon(c, r)
if appError != nil {
c.Err = appError
return
}
w.Write(b)
}
func getGroupsByTeamCommon(c *Context, r *http.Request) ([]byte, *model.AppError) {
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
return nil, model.NewAppError("Api4.getGroupsByTeam", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
}
opts := model.GroupSearchOpts{
Q: c.Params.Q,
@@ -795,8 +825,7 @@ func getGroupsByTeam(c *Context, w http.ResponseWriter, r *http.Request) {
groups, totalCount, appErr := c.App.GetGroupsByTeam(c.Params.TeamId, opts)
if appErr != nil {
c.Err = appErr
return
return nil, appErr
}
b, err := json.Marshal(struct {
@@ -808,14 +837,64 @@ func getGroupsByTeam(c *Context, w http.ResponseWriter, r *http.Request) {
})
if err != nil {
c.Err = model.NewAppError("Api4.getGroupsByTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
return
return nil, model.NewAppError("Api4.getGroupsByTeam", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
w.Write(b)
return b, nil
}
func getGroupsByChannelCommon(c *Context, r *http.Request) ([]byte, *model.AppError) {
if c.App.Channels().License() == nil || !*c.App.Channels().License().Features.LDAPGroups {
return nil, model.NewAppError("Api4.getGroupsByChannel", "api.ldap_groups.license_error", nil, "", http.StatusForbidden)
}
channel, appErr := c.App.GetChannel(c.AppContext, c.Params.ChannelId)
if appErr != nil {
return nil, appErr
}
var permission *model.Permission
if channel.Type == model.ChannelTypePrivate {
permission = model.PermissionReadPrivateChannelGroups
} else {
permission = model.PermissionReadPublicChannelGroups
}
if !c.App.SessionHasPermissionToChannel(c.AppContext, *c.AppContext.Session(), c.Params.ChannelId, permission) {
return nil, c.App.MakePermissionError(c.AppContext.Session(), []*model.Permission{permission})
}
opts := model.GroupSearchOpts{
Q: c.Params.Q,
IncludeMemberCount: c.Params.IncludeMemberCount,
FilterAllowReference: c.Params.FilterAllowReference,
}
if c.Params.Paginate == nil || *c.Params.Paginate {
opts.PageOpts = &model.PageOpts{Page: c.Params.Page, PerPage: c.Params.PerPage}
}
groups, totalCount, appErr := c.App.GetGroupsByChannel(c.Params.ChannelId, opts)
if appErr != nil {
return nil, appErr
}
b, err := json.Marshal(struct {
Groups []*model.GroupWithSchemeAdmin `json:"groups"`
Count int `json:"total_group_count"`
}{
Groups: groups,
Count: totalCount,
})
if err != nil {
return nil, model.NewAppError("Api4.getGroupsByChannel", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}
return b, nil
}
func getGroupsAssociatedToChannelsByTeam(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
@@ -855,6 +934,11 @@ func getGroupsAssociatedToChannelsByTeam(c *Context, w http.ResponseWriter, r *h
}
func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
var teamID, channelID string
source := c.Params.GroupSource
@@ -961,6 +1045,11 @@ func getGroups(c *Context, w http.ResponseWriter, r *http.Request) {
}
func deleteGroup(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
@@ -1004,6 +1093,11 @@ func deleteGroup(c *Context, w http.ResponseWriter, r *http.Request) {
}
func addGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return
@@ -1058,6 +1152,11 @@ func addGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
}
func deleteGroupMembers(c *Context, w http.ResponseWriter, r *http.Request) {
permissionErr := requireLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireGroupId()
if c.Err != nil {
return

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

@@ -3,7 +3,39 @@
package api4
import (
"net/http"
)
func (api *API) InitGroupLocal() {
api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/groups", api.APILocal(getGroupsByChannel)).Methods("GET")
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups", api.APILocal(getGroupsByTeam)).Methods("GET")
api.BaseRoutes.Channels.Handle("/{channel_id:[A-Za-z0-9]+}/groups", api.APILocal(getGroupsByChannelLocal)).Methods("GET")
api.BaseRoutes.Teams.Handle("/{team_id:[A-Za-z0-9]+}/groups", api.APILocal(getGroupsByTeamLocal)).Methods("GET")
}
func getGroupsByChannelLocal(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireChannelId()
if c.Err != nil {
return
}
b, appErr := getGroupsByChannelCommon(c, r)
if appErr != nil {
c.Err = appErr
return
}
w.Write(b)
}
func getGroupsByTeamLocal(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireTeamId()
if c.Err != nil {
return
}
b, appError := getGroupsByTeamCommon(c, r)
if appError != nil {
c.Err = appError
return
}
w.Write(b)
}

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

@@ -200,33 +200,27 @@ func (api *API) APILocal(h handlerFunc) http.Handler {
return handler
}
func requireLicense(f handlerFunc) handlerFunc {
return func(c *Context, w http.ResponseWriter, r *http.Request) {
if c.App.Channels().License() == nil {
c.Err = model.NewAppError("", "api.license_error", nil, "", http.StatusNotImplemented)
return
}
f(c, w, r)
func requireLicense(c *Context) *model.AppError {
if c.App.Channels().License() == nil {
err := model.NewAppError("", "api.license_error", nil, "", http.StatusNotImplemented)
return err
}
return nil
}
func minimumProfessionalLicense(f handlerFunc) handlerFunc {
return func(c *Context, w http.ResponseWriter, r *http.Request) {
lic := c.App.Srv().License()
if lic == nil || (lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise) {
c.Err = model.NewAppError("", model.NoTranslation, nil, "license is neither professional nor enterprise", http.StatusNotImplemented)
return
}
f(c, w, r)
func minimumProfessionalLicense(c *Context) *model.AppError {
lic := c.App.Srv().License()
if lic == nil || (lic.SkuShortName != model.LicenseShortSkuProfessional && lic.SkuShortName != model.LicenseShortSkuEnterprise) {
err := model.NewAppError("", model.NoTranslation, nil, "license is neither professional nor enterprise", http.StatusNotImplemented)
return err
}
return nil
}
func rejectGuests(f handlerFunc) handlerFunc {
return func(c *Context, w http.ResponseWriter, r *http.Request) {
if c.AppContext.Session().Props[model.SessionPropIsGuest] == "true" {
c.Err = model.NewAppError("", model.NoTranslation, nil, "insufficient permissions as a guest user", http.StatusNotImplemented)
return
}
f(c, w, r)
func rejectGuests(c *Context) *model.AppError {
if c.AppContext.Session().Props[model.SessionPropIsGuest] == "true" {
err := model.NewAppError("", model.NoTranslation, nil, "insufficient permissions as a guest user", http.StatusNotImplemented)
return err
}
return nil
}

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

@@ -13,31 +13,44 @@ import (
func (api *API) InitInsights() {
// Reactions
api.BaseRoutes.InsightsForTeam.Handle("/reactions", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopReactionsForTeamSince)))).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/reactions", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopReactionsForUserSince)))).Methods("GET")
api.BaseRoutes.InsightsForTeam.Handle("/reactions", api.APISessionRequired(getTopReactionsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/reactions", api.APISessionRequired(getTopReactionsForUserSince)).Methods("GET")
// Channels
api.BaseRoutes.InsightsForTeam.Handle("/channels", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopChannelsForTeamSince)))).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/channels", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopChannelsForUserSince)))).Methods("GET")
api.BaseRoutes.InsightsForTeam.Handle("/channels", api.APISessionRequired(getTopChannelsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/channels", api.APISessionRequired(getTopChannelsForUserSince)).Methods("GET")
// Threads
api.BaseRoutes.InsightsForTeam.Handle("/threads", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopThreadsForTeamSince)))).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/threads", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopThreadsForUserSince)))).Methods("GET")
api.BaseRoutes.InsightsForTeam.Handle("/threads", api.APISessionRequired(getTopThreadsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/threads", api.APISessionRequired(getTopThreadsForUserSince)).Methods("GET")
// user DMs
api.BaseRoutes.InsightsForUser.Handle("/dms", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopDMsForUserSince)))).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/dms", api.APISessionRequired(getTopDMsForUserSince)).Methods("GET")
// Inactive channels
api.BaseRoutes.InsightsForTeam.Handle("/inactive_channels", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopInactiveChannelsForTeamSince)))).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/inactive_channels", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getTopInactiveChannelsForUserSince)))).Methods("GET")
api.BaseRoutes.InsightsForTeam.Handle("/inactive_channels", api.APISessionRequired(getTopInactiveChannelsForTeamSince)).Methods("GET")
api.BaseRoutes.InsightsForUser.Handle("/inactive_channels", api.APISessionRequired(getTopInactiveChannelsForUserSince)).Methods("GET")
// New teammembers
api.BaseRoutes.InsightsForTeam.Handle("/team_members", api.APISessionRequired(minimumProfessionalLicense(rejectGuests(getNewTeamMembersSince)))).Methods("GET")
api.BaseRoutes.InsightsForTeam.Handle("/team_members", api.APISessionRequired(getNewTeamMembersSince)).Methods("GET")
}
// Top Reactions
func getTopReactionsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
@@ -60,7 +73,11 @@ func getTopReactionsForTeamSince(c *Context, w http.ResponseWriter, r *http.Requ
return
}
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topReactionList, appErr := c.App.GetTopReactionsForTeamSince(c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
@@ -82,6 +99,18 @@ func getTopReactionsForTeamSince(c *Context, w http.ResponseWriter, r *http.Requ
}
func getTopReactionsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// TeamId is an optional parameter
@@ -109,7 +138,11 @@ func getTopReactionsForUserSince(c *Context, w http.ResponseWriter, r *http.Requ
return
}
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topReactionList, appErr := c.App.GetTopReactionsForUserSince(c.AppContext.Session().UserId, c.Params.TeamId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
@@ -133,6 +166,18 @@ func getTopReactionsForUserSince(c *Context, w http.ResponseWriter, r *http.Requ
// Top Channels
func getTopChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
@@ -156,7 +201,11 @@ func getTopChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Reque
}
loc := user.GetTimezoneLocation()
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, loc)
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, appErr := c.App.GetTopChannelsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
@@ -184,6 +233,18 @@ func getTopChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Reque
}
func getTopChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// TeamId is an optional parameter
@@ -212,7 +273,11 @@ func getTopChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Reque
}
loc := user.GetTimezoneLocation()
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, loc)
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, appErr := c.App.GetTopChannelsForUserSince(c.AppContext, c.AppContext.Session().UserId, c.Params.TeamId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
@@ -241,6 +306,18 @@ func getTopChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Reque
// Top Threads
func getTopThreadsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
@@ -264,7 +341,11 @@ func getTopThreadsForTeamSince(c *Context, w http.ResponseWriter, r *http.Reques
return
}
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topThreads, appErr := c.App.GetTopThreadsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
@@ -286,6 +367,18 @@ func getTopThreadsForTeamSince(c *Context, w http.ResponseWriter, r *http.Reques
}
func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// restrict users with no access to team
@@ -313,7 +406,11 @@ func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Reques
}
}
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topThreads, appErr := c.App.GetTopThreadsForUserSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
@@ -336,13 +433,29 @@ func getTopThreadsForUserSince(c *Context, w http.ResponseWriter, r *http.Reques
// Top DMs
func getTopDMsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
user, err := c.App.GetUser(c.AppContext.Session().UserId)
if err != nil {
c.Err = err
return
}
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, user.GetTimezoneLocation())
if appErr != nil {
c.Err = appErr
return
}
topDMs, err := c.App.GetTopDMsForUserSince(user.Id, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
@@ -367,6 +480,18 @@ func getTopDMsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// Top Channels
func getTopInactiveChannelsForTeamSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
@@ -390,7 +515,11 @@ func getTopInactiveChannelsForTeamSince(c *Context, w http.ResponseWriter, r *ht
}
loc := user.GetTimezoneLocation()
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, loc)
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, err := c.App.GetTopInactiveChannelsForTeamSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
@@ -411,6 +540,18 @@ func getTopInactiveChannelsForTeamSince(c *Context, w http.ResponseWriter, r *ht
// top inactive channels
func getTopInactiveChannelsForUserSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.Params.TeamId = r.URL.Query().Get("team_id")
// TeamId is an optional parameter
@@ -439,7 +580,11 @@ func getTopInactiveChannelsForUserSince(c *Context, w http.ResponseWriter, r *ht
}
loc := user.GetTimezoneLocation()
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, loc)
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
topChannels, err := c.App.GetTopInactiveChannelsForUserSince(c.AppContext, c.Params.TeamId, c.AppContext.Session().UserId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),
@@ -479,6 +624,18 @@ func postCountByDurationViewModel(c *Context, topChannelList *model.TopChannelLi
}
func getNewTeamMembersSince(c *Context, w http.ResponseWriter, r *http.Request) {
// license and guest user check
permissionErr := minimumProfessionalLicense(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
permissionErr = rejectGuests(c)
if permissionErr != nil {
c.Err = permissionErr
return
}
c.RequireTeamId()
if c.Err != nil {
return
@@ -501,7 +658,11 @@ func getNewTeamMembersSince(c *Context, w http.ResponseWriter, r *http.Request)
return
}
loc := user.GetTimezoneLocation()
startTime := model.StartOfDayForTimeRange(c.Params.TimeRange, loc)
startTime, appErr := model.GetStartOfDayForTimeRange(c.Params.TimeRange, loc)
if appErr != nil {
c.Err = appErr
return
}
ntms, count, err := c.App.GetNewTeamMembersSince(c.AppContext, c.Params.TeamId, &model.InsightsOpts{
StartUnixMilli: startTime.UnixMilli(),

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

@@ -230,6 +230,12 @@ func TestGetTopReactionsForTeamSince(t *testing.T) {
CheckNotFoundStatus(t, resp)
})
t.Run("get-top-reactions-for-team-since invalid time range", func(t *testing.T) {
_, resp, err := client.GetTopReactionsForTeamSince(teamId, "7_days", 0, 5)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("get-top-reactions-for-team-since not a member of team", func(t *testing.T) {
th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam)
_, resp, err := client.GetTopReactionsForTeamSince(teamId, model.TimeRangeToday, 0, 5)
@@ -417,6 +423,12 @@ func TestGetTopReactionsForUserSince(t *testing.T) {
CheckNotFoundStatus(t, resp)
})
t.Run("get-top-reactions-for-user-since invalid time range", func(t *testing.T) {
_, resp, err := client.GetTopReactionsForUserSince(teamId, "7_days", 0, 5)
require.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("get-top-reactions-for-user-since not a member of team", func(t *testing.T) {
th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam)
_, resp, err := client.GetTopReactionsForUserSince(teamId, model.TimeRangeToday, 0, 5)
@@ -515,6 +527,12 @@ func TestGetTopChannelsForTeamSince(t *testing.T) {
CheckNotFoundStatus(t, resp)
})
t.Run("get-top-channels-for-team-since invalid time range", func(t *testing.T) {
_, resp, err := client.GetTopChannelsForTeamSince(teamId, "7_days", 0, 5)
assert.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("get-top-channels-for-team-since not a member of team", func(t *testing.T) {
th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam)
_, resp, err := client.GetTopChannelsForTeamSince(teamId, model.TimeRangeToday, 0, 5)
@@ -592,6 +610,12 @@ func TestGetTopChannelsForUserSince(t *testing.T) {
CheckNotFoundStatus(t, resp)
})
t.Run("get-top-channels-for-user-since invalid time range", func(t *testing.T) {
_, resp, err := client.GetTopChannelsForUserSince(teamId, "7_days", 0, 5)
assert.Error(t, err)
CheckBadRequestStatus(t, resp)
})
t.Run("get-top-channels-for-user-since not a member of team", func(t *testing.T) {
th.UnlinkUserFromTeam(th.BasicUser, th.BasicTeam)
_, resp, err := client.GetTopChannelsForUserSince(teamId, model.TimeRangeToday, 0, 5)

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

@@ -106,7 +106,13 @@ func addLicense(c *Context, w http.ResponseWriter, r *http.Request) {
// skip the restrictions if license is a sanctioned trial
if !license.IsSanctionedTrial() && license.IsTrialLicense() {
canStartTrialLicense, err := c.App.Srv().Platform().LicenseManager().CanStartTrial()
lm := c.App.Srv().Platform().LicenseManager()
if lm == nil {
c.Err = model.NewAppError("addLicense", "api.license.upgrade_needed.app_error", nil, "", http.StatusInternalServerError)
return
}
canStartTrialLicense, err := lm.CanStartTrial()
if err != nil {
c.Err = model.NewAppError("addLicense", "api.license.add_license.open.app_error", nil, "", http.StatusInternalServerError)
return

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

@@ -91,9 +91,6 @@ func TestUploadLicenseFile(t *testing.T) {
mockLicenseValidator := mocks2.LicenseValidatorIface{}
defer testutils.ResetLicenseValidator()
//startTimestamp, err := time.Parse("2 Jan 2006 3:04 pm", "1 Jan 2021 12:00 am")
//require.Nil(t, err)
userCount := 100
mills := model.GetMillis()
@@ -125,6 +122,37 @@ func TestUploadLicenseFile(t *testing.T) {
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
})
t.Run("try to get gone through trial, with TE build", func(t *testing.T) {
th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = false })
th.App.Srv().Platform().SetLicenseManager(nil)
mockLicenseValidator := mocks2.LicenseValidatorIface{}
defer testutils.ResetLicenseValidator()
license := model.License{
Id: model.NewId(),
Features: &model.Features{
Users: model.NewInt(100),
},
Customer: &model.Customer{
Name: "Test",
},
StartsAt: model.GetMillis() + 100,
ExpiresAt: model.GetMillis() + 100 + (30*(time.Hour*24) + (time.Hour * 8)).Milliseconds(),
}
mockLicenseValidator.On("LicenseFromBytes", mock.Anything).Return(&license, nil).Once()
licenseBytes, err := json.Marshal(license)
require.NoError(t, err)
mockLicenseValidator.On("ValidateLicense", mock.Anything).Return(true, string(licenseBytes))
utils.LicenseValidator = &mockLicenseValidator
resp, err := th.SystemAdminClient.UploadLicenseFile([]byte(""))
CheckErrorID(t, err, "api.license.upgrade_needed.app_error")
require.Equal(t, http.StatusInternalServerError, resp.StatusCode)
})
t.Run("allow uploading sanctioned trials even if server already gone through trial", func(t *testing.T) {
mockLicenseValidator := mocks2.LicenseValidatorIface{}
defer testutils.ResetLicenseValidator()

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

@@ -2498,7 +2498,7 @@ func TestGetTopChannelsForTeamSince(t *testing.T) {
{ID: channel5.Id, MessageCount: 2},
}
timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
t.Run("get-top-channels-for-team-since", func(t *testing.T) {
topChannels, err := th.App.GetTopChannelsForTeamSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5})
@@ -2576,7 +2576,7 @@ func TestGetTopChannelsForUserSince(t *testing.T) {
{ID: channel5.Id, MessageCount: 2},
}
timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
t.Run("get-top-channels-for-user-since", func(t *testing.T) {
topChannels, err := th.App.GetTopChannelsForUserSince(th.Context, th.BasicUser.Id, "", &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5})
@@ -2787,7 +2787,7 @@ func TestGetTopInactiveChannelsForTeamSince(t *testing.T) {
{ID: channel2.Id, MessageCount: 6},
}
timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
t.Run("get-top-channels-for-team-since", func(t *testing.T) {
topChannels, err := th.App.GetTopInactiveChannelsForTeamSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5})
@@ -2885,7 +2885,7 @@ func TestGetTopInactiveChannelsForUserSince(t *testing.T) {
{ID: channel2.Id, MessageCount: 6},
}
timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
t.Run("get-top-channels-for-user-since", func(t *testing.T) {
topChannels, err := th.App.GetTopInactiveChannelsForUserSince(th.Context, th.BasicChannel.TeamId, th.BasicUser.Id, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 4})

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

@@ -201,6 +201,18 @@ func truncateUserNames(name string, i int) string {
return name
}
type FieldRow struct {
Cells []*model.SlackAttachmentField
}
type EmailMessageAttachment struct {
model.SlackAttachment
Pretext template.HTML
Text template.HTML
FieldRows []FieldRow
}
type postData struct {
SenderName string
ChannelName string
@@ -211,6 +223,7 @@ type postData struct {
Time string
ShowChannelIcon bool
OtherChannelMembersCount int
MessageAttachments []*EmailMessageAttachment
}
/**
@@ -245,6 +258,7 @@ func (a *App) getNotificationEmailBody(c request.CTX, recipient *model.User, pos
}
pData.Message = template.HTML(normalizedPostMessage)
pData.Time = translateFunc("app.notification.body.dm.time", messageTime)
pData.MessageAttachments = a.processMessageAttachments(post)
}
data := a.Srv().EmailService.NewEmailTemplateData(recipient.Locale)
@@ -306,6 +320,81 @@ func (a *App) getNotificationEmailBody(c request.CTX, recipient *model.User, pos
return a.Srv().TemplatesContainer().RenderToString("messages_notification", data)
}
func (a *App) processMessageAttachments(post *model.Post) []*EmailMessageAttachment {
emailMessageAttachments := []*EmailMessageAttachment{}
for _, messageAttachment := range post.Attachments() {
emailMessageAttachment := &EmailMessageAttachment{
SlackAttachment: *messageAttachment,
Pretext: a.prepareTextForEmail(messageAttachment.Pretext),
Text: a.prepareTextForEmail(messageAttachment.Text),
}
stripedTitle, err := utils.StripMarkdown(emailMessageAttachment.Title)
if err != nil {
mlog.Warn("Failed parse to markdown from messageatatchment title", mlog.String("post_id", post.Id), mlog.Err(err))
stripedTitle = ""
}
emailMessageAttachment.Title = stripedTitle
shortFieldRow := FieldRow{}
for i := range messageAttachment.Fields {
// Create a new instance to avoid altering the original pointer reference
// We update field value to parse markdown.
// If we do that on the original pointer, the rendered text in mattermost
// becomes invalid as its no longer a markdown string, but rather an HTML string.
field := &model.SlackAttachmentField{
Title: messageAttachment.Fields[i].Title,
Value: messageAttachment.Fields[i].Value,
Short: messageAttachment.Fields[i].Short,
}
if stringValue, ok := field.Value.(string); ok {
field.Value = a.prepareTextForEmail(stringValue)
}
if !field.Short {
if len(shortFieldRow.Cells) > 0 {
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
shortFieldRow = FieldRow{}
}
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, FieldRow{[]*model.SlackAttachmentField{field}})
} else {
shortFieldRow.Cells = append(shortFieldRow.Cells, field)
if len(shortFieldRow.Cells) == 2 {
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
shortFieldRow = FieldRow{}
}
}
}
// collect any leftover short fields
if len(shortFieldRow.Cells) > 0 {
emailMessageAttachment.FieldRows = append(emailMessageAttachment.FieldRows, shortFieldRow)
shortFieldRow = FieldRow{}
}
emailMessageAttachments = append(emailMessageAttachments, emailMessageAttachment)
}
return emailMessageAttachments
}
func (a *App) prepareTextForEmail(text string) template.HTML {
escapedText := html.EscapeString(text)
markdownText, err := utils.MarkdownToHTML(escapedText)
if err != nil {
mlog.Warn("Encountered error while converting markdown to HTML", mlog.Err(err))
return template.HTML(text)
}
return template.HTML(markdownText)
}
type formattedPostTime struct {
Time time.Time
Year string

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

@@ -339,6 +339,103 @@ func TestGetNotificationEmailBodyFullNotificationLocaleTime24Hour(t *testing.T)
require.Contains(t, body, "14:30", fmt.Sprintf("Expected email text '14:30'. Got %s", body))
}
func TestGetNotificationEmailBodyFullNotificationWithSlackAttachments(t *testing.T) {
th := SetupWithStoreMock(t)
defer th.TearDown()
recipient := &model.User{}
post := &model.Post{
Message: "This is the message",
}
messageAttachments := []*model.SlackAttachment{
{
Color: "#FF0000",
Pretext: "message attachment 1 pretext",
AuthorName: "author name",
AuthorLink: "https://example.com/slack_attachment_1/author_link",
AuthorIcon: "https://example.com/slack_attachment_1/author_icon",
Title: "message attachment 1 title",
TitleLink: "https://example.com/slack_attachment_1/title_link",
Text: "message attachment 1 text",
ImageURL: "https://example.com/slack_attachment_1/image",
ThumbURL: "https://example.com/slack_attachment_1/thumb",
Fields: []*model.SlackAttachmentField{
{
Short: true,
Title: "message attachment 1 field 1 title",
Value: "message attachment 1 field 1 value",
},
{
Short: false,
Title: "message attachment 1 field 2 title",
Value: "message attachment 1 field 2 value",
},
{
Short: true,
Title: "message attachment 1 field 3 title",
Value: "message attachment 1 field 3 value",
},
{
Short: true,
Title: "message attachment 1 field 4 title",
Value: "message attachment 1 field 4 value",
},
},
},
{
Color: "#FF0000",
Pretext: "message attachment 2 pretext",
AuthorName: "author name 2",
Text: "message attachment 2 text",
},
}
model.ParseSlackAttachment(post, messageAttachments)
channel := &model.Channel{
DisplayName: "ChannelName",
Type: model.ChannelTypeOpen,
}
channelName := "ChannelName"
senderName := "sender"
teamName := "testteam"
teamURL := "http://localhost:8065/testteam"
emailNotificationContentsType := model.EmailNotificationContentsFull
translateFunc := i18n.GetUserTranslations("en")
storeMock := th.App.Srv().Store().(*mocks.Store)
teamStoreMock := mocks.TeamStore{}
teamStoreMock.On("GetByName", "testteam").Return(&model.Team{Name: "testteam"}, nil)
storeMock.On("Team").Return(&teamStoreMock)
body, err := th.App.getNotificationEmailBody(th.Context, recipient, post, channel, channelName, senderName, teamName, teamURL, emailNotificationContentsType, true, translateFunc, "user-avatar.png")
require.NoError(t, err)
require.Contains(t, body, "#FF0000")
require.Contains(t, body, "message attachment 1 pretext")
require.Contains(t, body, "author name")
require.Contains(t, body, "https://example.com/slack_attachment_1/author_link")
require.Contains(t, body, "https://example.com/slack_attachment_1/author_icon")
require.Contains(t, body, "message attachment 1 title")
require.Contains(t, body, "https://example.com/slack_attachment_1/title_link")
require.Contains(t, body, "message attachment 1 text")
require.Contains(t, body, "https://example.com/slack_attachment_1/image")
require.Contains(t, body, "https://example.com/slack_attachment_1/thumb")
require.Contains(t, body, "message attachment 1 field 1 title")
require.Contains(t, body, "message attachment 1 field 1 value")
require.Contains(t, body, "message attachment 1 field 2 title")
require.Contains(t, body, "message attachment 1 field 2 value")
require.Contains(t, body, "message attachment 1 field 3 title")
require.Contains(t, body, "message attachment 1 field 3 value")
require.Contains(t, body, "message attachment 1 field 4 title")
require.Contains(t, body, "message attachment 1 field 4 value")
require.Contains(t, body, "https://example.com/slack_attachment_1/thumb")
require.Contains(t, body, "message attachment 2 pretext")
require.Contains(t, body, "author name 2")
require.Contains(t, body, "message attachment 2 text")
}
// from here
func TestGetNotificationEmailBodyGenericNotificationPublicChannel(t *testing.T) {
th := SetupWithStoreMock(t)

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

@@ -1440,20 +1440,19 @@ func TestPushNotificationRace(t *testing.T) {
Return(&model.Preference{Value: "test"}, nil)
mockStore.On("Preference").Return(&mockPreferenceStore)
s := &Server{
products: make(map[string]Product),
Router: mux.NewRouter(),
filestore: &fmocks.FileBackend{},
products: make(map[string]Product),
Router: mux.NewRouter(),
}
var err error
s.platform, err = platform.New(platform.ServiceConfig{
ConfigStore: memoryStore,
})
}, platform.SetFileStore(&fmocks.FileBackend{}))
s.SetStore(mockStore)
require.NoError(t, err)
serviceMap := map[ServiceKey]any{
ConfigKey: s.platform,
LicenseKey: &licenseWrapper{s},
FilestoreKey: s.filestore,
FilestoreKey: s.FileBackend(),
}
ch, err := NewChannels(s, serviceMap)
require.NoError(t, err)

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

@@ -54,7 +54,7 @@ func ConfigStore(configStore *config.Store) Option {
func SetFileStore(filestore filestore.FileBackend) Option {
return func(s *Server) error {
s.filestore = filestore
s.platformOptions = append(s.platformOptions, platform.SetFileStore(filestore))
return nil
}
}

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

@@ -29,6 +29,10 @@ type permissionsServiceWrapper struct {
app AppIface
}
func (s *permissionsServiceWrapper) HasPermissionTo(userID string, permission *model.Permission) bool {
return s.app.HasPermissionTo(userID, permission)
}
func (s *permissionsServiceWrapper) HasPermissionToTeam(userID string, teamID string, permission *model.Permission) bool {
return s.app.HasPermissionToTeam(userID, teamID, permission)
}

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

@@ -991,6 +991,27 @@ func (a *App) getPlaybooksPermissionsAddManageRoles() (permissionsMap, error) {
return transformations, nil
}
func (a *App) getProductsBoardsPermissions() (permissionsMap, error) {
transformations := []permissionTransformation{}
permissionsProductsRead := []string{model.PermissionSysconsoleReadProductsBoards.Id}
permissionsProductsWrite := []string{model.PermissionSysconsoleWriteProductsBoards.Id}
// Give the new subsection READ permissions to any user with SYSTEM_MANAGER
transformations = append(transformations, permissionTransformation{
On: permissionOr(isRole(model.SystemManagerRoleId)),
Add: permissionsProductsRead,
})
// Give the new subsection WRITE permissions to any user with SYSTEM_ADMIN
transformations = append(transformations, permissionTransformation{
On: permissionOr(isRole(model.SystemAdminRoleId)),
Add: permissionsProductsWrite,
})
return transformations, nil
}
// DoPermissionsMigrations execute all the permissions migrations need by the current version.
func (a *App) DoPermissionsMigrations() error {
return a.Srv().doPermissionsMigrations()
@@ -1032,6 +1053,7 @@ func (s *Server) doPermissionsMigrations() error {
{Key: model.MigrationKeyAddPlaybooksPermissions, Migration: a.getAddPlaybooksPermissions},
{Key: model.MigrationKeyAddCustomUserGroupsPermissions, Migration: a.getAddCustomUserGroupsPermissions},
{Key: model.MigrationKeyAddPlayboosksManageRolesPermissions, Migration: a.getPlaybooksPermissionsAddManageRoles},
{Key: model.MigrationKeyAddProductsBoardsPermissions, Migration: a.getProductsBoardsPermissions},
}
roles, err := s.Store().Role().GetAll()

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

@@ -4,7 +4,7 @@
package platform
import (
"io/ioutil"
"os"
"path/filepath"
"sync"
"testing"
@@ -127,7 +127,7 @@ func SetupWithCluster(tb testing.TB, cluster einterfaces.ClusterInterface) *Test
}
func setupTestHelper(dbStore store.Store, enterprise bool, includeCacheLayer bool, tb testing.TB, options ...Option) *TestHelper {
tempWorkspace, err := ioutil.TempDir("", "apptest")
tempWorkspace, err := os.MkdirTemp("", "apptest")
if err != nil {
panic(err)
}

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

@@ -11,6 +11,7 @@ import (
"github.com/mattermost/mattermost-server/v6/config"
"github.com/mattermost/mattermost-server/v6/einterfaces"
"github.com/mattermost/mattermost-server/v6/model"
"github.com/mattermost/mattermost-server/v6/shared/filestore"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
"github.com/mattermost/mattermost-server/v6/store"
"github.com/mattermost/mattermost-server/v6/store/localcachelayer"
@@ -74,6 +75,13 @@ func Config(dsn string, readOnly bool, configDefaults *model.Config) Option {
}
}
func SetFileStore(filestore filestore.FileBackend) Option {
return func(ps *PlatformService) error {
ps.filestore = filestore
return nil
}
}
// ConfigStore applies the given config store, typically to replace the traditional sources with a memory store for testing.
func ConfigStore(configStore *config.Store) Option {
return func(ps *PlatformService) error {

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

@@ -20,6 +20,7 @@ import (
"github.com/mattermost/mattermost-server/v6/services/cache"
"github.com/mattermost/mattermost-server/v6/services/searchengine"
"github.com/mattermost/mattermost-server/v6/services/searchengine/bleveengine"
"github.com/mattermost/mattermost-server/v6/shared/filestore"
"github.com/mattermost/mattermost-server/v6/shared/mlog"
"github.com/mattermost/mattermost-server/v6/store"
"github.com/mattermost/mattermost-server/v6/store/localcachelayer"
@@ -41,6 +42,8 @@ type PlatformService struct {
configStore *config.Store
filestore filestore.FileBackend
cacheProvider cache.Provider
statusCache cache.Cache
sessionCache cache.Cache
@@ -213,6 +216,18 @@ func New(sc ServiceConfig, options ...Option) (*PlatformService, error) {
}
}
license := ps.License()
// Step 3: Initialize filestore
if ps.filestore == nil {
insecure := ps.Config().ServiceSettings.EnableInsecureOutgoingConnections
backend, err2 := filestore.NewFileBackend(ps.Config().FileSettings.ToFileBackendSettings(license != nil && *license.Features.Compliance, insecure != nil && *insecure))
if err2 != nil {
return nil, fmt.Errorf("failed to initialize filebackend: %w", err2)
}
ps.filestore = backend
}
var err error
ps.Store, err = ps.newStore()
if err != nil {
@@ -431,3 +446,7 @@ func (ps *PlatformService) GetPluginStatuses() (model.PluginStatuses, *model.App
return pluginStatuses, nil
}
func (ps *PlatformService) FileBackend() filestore.FileBackend {
return ps.filestore
}

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

@@ -196,6 +196,7 @@ func TestPluginAPIGetUserPreferences(t *testing.T) {
}
func TestPluginAPIDeleteUserPreferences(t *testing.T) {
t.Skip("MM-47612")
th := Setup(t)
defer th.TearDown()
api := th.SetupPluginAPI()

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

@@ -230,7 +230,7 @@ func TestGetTopReactionsForTeamSince(t *testing.T) {
expectedTopReactions[3] = &model.TopReaction{EmojiName: "sad", Count: int64(3)}
expectedTopReactions[4] = &model.TopReaction{EmojiName: "happy", Count: int64(2)}
timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
t.Run("get-top-reactions-for-team-since", func(t *testing.T) {
topReactions, err := th.App.GetTopReactionsForTeamSince(teamId, userId, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5})
@@ -401,7 +401,7 @@ func TestGetTopReactionsForUserSince(t *testing.T) {
expectedTopReactions[3] = &model.TopReaction{EmojiName: "heart", Count: int64(3)}
expectedTopReactions[4] = &model.TopReaction{EmojiName: "blush", Count: int64(2)}
timeRange := model.StartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
timeRange, _ := model.GetStartOfDayForTimeRange(model.TimeRangeToday, time.Now().Location())
t.Run("get-top-reactions-for-user-since", func(t *testing.T) {
topReactions, err := th.App.GetTopReactionsForUserSince(userId, teamId, &model.InsightsOpts{StartUnixMilli: timeRange.UnixMilli(), Page: 0, PerPage: 5})

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

@@ -137,7 +137,6 @@ type Server struct {
openGraphDataCache cache.Cache
clusterLeaderListenerId string
loggerLicenseListenerId string
filestore filestore.FileBackend
platform *platform.PlatformService
platformOptions []platform.Option
@@ -239,15 +238,6 @@ func NewServer(options ...Option) (*Server, error) {
s.LoadLicense()
}
license := s.License()
insecure := s.platform.Config().ServiceSettings.EnableInsecureOutgoingConnections
// Step 3: Initialize filestore
backend, err := filestore.NewFileBackend(s.platform.Config().FileSettings.ToFileBackendSettings(license != nil && *license.Features.Compliance, insecure != nil && *insecure))
if err != nil {
return nil, errors.Wrap(err, "failed to initialize filebackend")
}
s.filestore = backend
s.licenseWrapper = &licenseWrapper{
srv: s,
}
@@ -272,7 +262,7 @@ func NewServer(options ...Option) (*Server, error) {
ChannelKey: &channelsWrapper{srv: s},
ConfigKey: s.platform,
LicenseKey: s.licenseWrapper,
FilestoreKey: s.filestore,
FilestoreKey: s.platform.FileBackend(),
FileInfoStoreKey: &fileInfoWrapper{srv: s},
ClusterKey: s.platform,
UserKey: New(ServerConnector(s.Channels())),
@@ -439,6 +429,7 @@ func NewServer(options ...Option) (*Server, error) {
mlog.Info("Printing current working", mlog.String("directory", pwd))
mlog.Info("Loaded config", mlog.String("source", s.platform.DescribeConfig()))
license := s.License()
allowAdvancedLogging := license != nil && *license.Features.AdvancedLogging
if s.Audit == nil {
@@ -1425,7 +1416,7 @@ func (s *Server) SendRemoveExpiredLicenseEmail(email string, renewalLink, locale
}
func (s *Server) FileBackend() filestore.FileBackend {
return s.filestore
return s.platform.FileBackend()
}
func (s *Server) TotalWebsocketConnections() int {

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

@@ -90,28 +90,29 @@ func TestStartServerNoS3Bucket(t *testing.T) {
}
s3Endpoint := fmt.Sprintf("%s:%s", s3Host, s3Port)
configStore, _ := config.NewFileStore("config.json", true)
store, _ := config.NewStoreFromBacking(configStore, nil, false)
cfg := store.Get()
cfg.FileSettings = model.FileSettings{
DriverName: model.NewString(model.ImageDriverS3),
AmazonS3AccessKeyId: model.NewString(model.MinioAccessKey),
AmazonS3SecretAccessKey: model.NewString(model.MinioSecretKey),
AmazonS3Bucket: model.NewString("nosuchbucket"),
AmazonS3Endpoint: model.NewString(s3Endpoint),
AmazonS3Region: model.NewString(""),
AmazonS3PathPrefix: model.NewString(""),
AmazonS3SSL: model.NewBool(false),
}
*cfg.ServiceSettings.ListenAddress = ":0"
_, _, err := store.Set(cfg)
require.NoError(t, err)
s, err := NewServer(func(server *Server) error {
configStore, _ := config.NewFileStore("config.json", true)
store, _ := config.NewStoreFromBacking(configStore, nil, false)
var err error
server.platform, err = platform.New(platform.ServiceConfig{
ConfigStore: store,
})
require.NoError(t, err)
server.platform.UpdateConfig(func(cfg *model.Config) {
cfg.FileSettings = model.FileSettings{
DriverName: model.NewString(model.ImageDriverS3),
AmazonS3AccessKeyId: model.NewString(model.MinioAccessKey),
AmazonS3SecretAccessKey: model.NewString(model.MinioSecretKey),
AmazonS3Bucket: model.NewString("nosuchbucket"),
AmazonS3Endpoint: model.NewString(s3Endpoint),
AmazonS3Region: model.NewString(""),
AmazonS3PathPrefix: model.NewString(""),
AmazonS3SSL: model.NewBool(false),
}
*cfg.ServiceSettings.ListenAddress = ":0"
})
var err2 error
server.platform, err2 = platform.New(platform.ServiceConfig{}, platform.ConfigStore(store))
require.NoError(t, err2)
return nil
})
require.NoError(t, err)
@@ -120,6 +121,8 @@ func TestStartServerNoS3Bucket(t *testing.T) {
defer s.Shutdown()
// ensure that a new bucket was created
require.IsType(t, &filestore.S3FileBackend{}, s.FileBackend())
err = s.FileBackend().(*filestore.S3FileBackend).TestConnection()
require.NoError(t, err)
}

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

@@ -8,7 +8,7 @@ SHELL ["/bin/bash", "-o", "pipefail", "-c"]
ENV PATH="/mattermost/bin:${PATH}"
ARG PUID=2000
ARG PGID=2000
ARG MM_PACKAGE="https://releases.mattermost.com/7.3.0/mattermost-7.3.0-linux-amd64.tar.gz?src=docker"
ARG MM_PACKAGE="https://releases.mattermost.com/7.4.0/mattermost-7.4.0-linux-amd64.tar.gz?src=docker"
# # Install needed packages and indirect dependencies
RUN apt-get update \

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

@@ -4,6 +4,7 @@
package config
import (
"encoding/json"
"os"
"reflect"
"strconv"
@@ -73,6 +74,11 @@ func applyEnvKey(key, value string, rValueSubject reflect.Value) {
}
case reflect.SliceOf(reflect.TypeOf("")).Kind():
rFieldValue.Set(reflect.ValueOf(strings.Split(value, " ")))
case reflect.Map:
target := reflect.New(rFieldValue.Type()).Interface()
if err := json.Unmarshal([]byte(value), target); err == nil {
rFieldValue.Set(reflect.ValueOf(target).Elem())
}
}
}

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

@@ -31,15 +31,54 @@ func TestRemoveEnvOverrides(t *testing.T) {
expectedConfig *model.Config
}{
{
name: "basic override",
name: "config override",
inputConfig: modifiedDefault(func(in *model.Config) {
*in.ServiceSettings.TLSMinVer = "1.4"
in.PluginSettings.PluginStates = map[string]*model.PluginState{
"plugin1": {
Enable: false,
},
}
in.PluginSettings.Plugins = map[string]map[string]interface{}{
"com.mattermost.plugin-1": {
"key1": "value1",
},
"com_mattermost_plugin-2": {
"key2": "value2",
},
}
}),
env: map[string]string{
"MM_SERVICESETTINGS_TLSMINVER": "1.5",
"MM_PLUGINSETTINGS_PLUGINSTATES": `{
"plugin1": {
"Enable": true
}
}`,
"MM_PLUGINSETTINGS_PLUGINS": `{
"com.mattermost.plugin-1": {
"key1": "other-value"
},
"com_mattermost_plugin-2": {
"key2": "other-value"
}
}`,
},
expectedConfig: modifiedDefault(func(in *model.Config) {
*in.ServiceSettings.TLSMinVer = "1.5"
in.PluginSettings.PluginStates = map[string]*model.PluginState{
"plugin1": {
Enable: true,
},
}
in.PluginSettings.Plugins = map[string]map[string]interface{}{
"com.mattermost.plugin-1": {
"key1": "other-value",
},
"com_mattermost_plugin-2": {
"key2": "other-value",
},
}
}),
},
{
@@ -102,6 +141,41 @@ func TestRemoveEnvOverrides(t *testing.T) {
in.SqlSettings.DataSourceReplicas = []string{"otherthing", "alsothis"}
}),
},
{
name: "complex env settings",
inputConfig: modifiedDefault(func(in *model.Config) {
}),
env: map[string]string{
"MM_PLUGINSETTINGS_PLUGINSTATES": `{
"com.mattermost.plugin-1": {
"enable": true
}
}`,
"MM_PLUGINSETTINGS_PLUGINS": `{
"com.mattermost.plugin-1": {
"key": {
"key": "(?P<key>KEY)-(?P<id>\\d{1,6})(?P<comma>[,;]*)",
"value": "[$key-$id](https://example.com/?$project-$id)$comma"
}
}
}`,
},
expectedConfig: modifiedDefault(func(in *model.Config) {
in.PluginSettings.PluginStates = map[string]*model.PluginState{
"com.mattermost.plugin-1": {
Enable: true,
},
}
in.PluginSettings.Plugins = map[string]map[string]interface{}{
"com.mattermost.plugin-1": {
"key": map[string]interface{}{
"key": "(?P<key>KEY)-(?P<id>\\d{1,6})(?P<comma>[,;]*)",
"value": "[$key-$id](https://example.com/?$project-$id)$comma",
},
},
}
}),
},
{
name: "bad env",
inputConfig: modifiedDefault(func(in *model.Config) {

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

@@ -4267,14 +4267,6 @@
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
"translation": "Каналите в поканата трябва да са част от канещият екип."
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "Вашият лиценз не поддържа профили за гости"
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "Профилите за гости са забранени"
},
{
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "Грешка при анулиране на поканите по ел.поща."

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

@@ -1777,7 +1777,7 @@
},
{
"id": "api.templates.email_change_verify_body.title",
"translation": "Du hast deine E-Mail-Adresse geändert"
"translation": "Du hast deine E-Mail-Adresse erfolgreich geändert"
},
{
"id": "api.templates.email_change_verify_subject",
@@ -4801,11 +4801,11 @@
},
{
"id": "web.error.unsupported_browser.min_browser_version.edge",
"translation": "Version 44+"
"translation": "Version 95+"
},
{
"id": "web.error.unsupported_browser.min_browser_version.chrome",
"translation": "Version 100+"
"translation": "Version 106+"
},
{
"id": "web.error.unsupported_browser.learn_more",
@@ -5623,14 +5623,6 @@
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
"translation": "Die Kanäle der Einladung müssen Teil des Teams der Einladung sein."
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "Deine Lizenz unterstützt Gastkonten nicht"
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "Gastkonten sind deaktiviert"
},
{
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "Fehler beim Annulieren von E-Mail-Einladungen."
@@ -8380,7 +8372,7 @@
},
{
"id": "app.notification.body.dm.subTitle",
"translation": "Währen du weg warst, hat dir {{.SenderName}} eine neue Direktnachricht gesendet."
"translation": "Während du weg warst, hat dir {{.SenderName}} eine neue Direktnachricht gesendet."
},
{
"id": "app.license.generate_renewal_token.no_license",
@@ -9537,5 +9529,33 @@
{
"id": "app.user.get_badge_count.app_error",
"translation": "Wir konnten den Nachrichtenzähler für den Benutzer nicht abfragen."
},
{
"id": "app.job.error",
"translation": "Fehler bei der Auftragsausführung."
},
{
"id": "app.last_accessible_file.app_error",
"translation": "Fehler beim Abrufen der letzten zugänglichen Datei"
},
{
"id": "app.file.cloud.get.app_error",
"translation": "Die Datei kann nicht abgerufen werden, da sie das Limit des Cloud-Plans überschritten hat."
},
{
"id": "model.group.name.reserved_name.app_error",
"translation": "Gruppenname existiert bereits als reservierter Name"
},
{
"id": "app.plugin.product_mode.app_error",
"translation": "Plugin {{.Name}} kann im Produktmodus nicht aktiviert werden."
},
{
"id": "api.team.invite_guests_to_channels.license.error",
"translation": "Deine Lizenz unterstützt Gastkonten nicht"
},
{
"id": "api.team.invite_guests_to_channels.disabled.error",
"translation": "Gastkonten sind deaktiviert"
}
]

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

@@ -8739,6 +8739,10 @@
"id": "model.incoming_hook.username.app_error",
"translation": "Invalid username."
},
{
"id": "model.insights.get_start_of_day_for_time_range.time_range.app_error",
"translation": "Invalid time range."
},
{
"id": "model.job.is_valid.create_at.app_error",
"translation": "Create at must be a valid time."

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

@@ -5459,14 +5459,6 @@
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
"translation": "The channels of the invite must be part of the team of the invite."
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "Your licence does not support guest accounts"
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "Guest accounts are disabled"
},
{
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "Error invalidating email invites."
@@ -9537,5 +9529,21 @@
{
"id": "app.cloud.get_current_plan_name.app_error",
"translation": "Unable to get current plan name"
},
{
"id": "model.group.name.reserved_name.app_error",
"translation": "group name already exists as a reserved name"
},
{
"id": "app.last_accessible_file.app_error",
"translation": "Error fetching last accessible file"
},
{
"id": "app.job.error",
"translation": "Error during job execution."
},
{
"id": "app.file.cloud.get.app_error",
"translation": "Unable to retrieve file: cloud storage limit exceeded."
}
]

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

@@ -621,7 +621,7 @@
},
{
"id": "api.command_help.desc",
"translation": "Abrir la página de ayuda de Mattermost"
"translation": "Muestra el mensaje de ayuda de Mattermost"
},
{
"id": "api.command_help.name",
@@ -1781,7 +1781,7 @@
},
{
"id": "api.templates.email_change_verify_body.title",
"translation": "Haz actualizado tu correo electrónico"
"translation": "Haz actualizado satisfactoriamente tu correo electrónico"
},
{
"id": "api.templates.email_change_verify_subject",
@@ -1857,7 +1857,7 @@
},
{
"id": "api.templates.post_body.button",
"translation": "Ver mensaje"
"translation": "Responder en Mattermost"
},
{
"id": "api.templates.reset_body.button",
@@ -3305,7 +3305,7 @@
},
{
"id": "ent.migration.migratetoldap.duplicate_field",
"translation": "No se pueden migrar los usuarios de AD/LDAP con el campo especificado. Se ha detectado un entrada. Por favor, retire todos los duplicados e inténtalo de nuevo."
"translation": "No se pueden migrar los usuarios de AD/LDAP con el campo especificado. Se ha detectado un entrada duplicada. Por favor, elimina todos los duplicados e inténtalo de nuevo."
},
{
"id": "ent.migration.migratetoldap.user_not_found",
@@ -4573,7 +4573,7 @@
},
{
"id": "oauth.gitlab.tos.error",
"translation": "Los Términos de Servicio de GitLab se han actualizado. Por favor, vaya a gitlab.com a aceptar y, a continuación, intente iniciar sesión en Mattermost de nuevo."
"translation": "Los Términos de Servicio de GitLab se han actualizado. Por favor, visita {{.URL}} para aceptarlos y, a continuación, intenta iniciar sesión en Mattermost nuevamente."
},
{
"id": "plugin.api.update_user_status.bad_status",
@@ -4753,7 +4753,7 @@
},
{
"id": "web.error.unsupported_browser.min_os_version.windows",
"translation": "Windows 7+"
"translation": "Windows 8.1+"
},
{
"id": "web.error.unsupported_browser.min_os_version.mac",
@@ -4761,19 +4761,19 @@
},
{
"id": "web.error.unsupported_browser.min_browser_version.safari",
"translation": "Versión 12+"
"translation": "Versión 14.1+"
},
{
"id": "web.error.unsupported_browser.min_browser_version.firefox",
"translation": "Versión 78+"
"translation": "Versión 91+"
},
{
"id": "web.error.unsupported_browser.min_browser_version.edge",
"translation": "Versión 44+"
"translation": "Versión 95+"
},
{
"id": "web.error.unsupported_browser.min_browser_version.chrome",
"translation": "Versión 89+"
"translation": "Versión 106+"
},
{
"id": "web.error.unsupported_browser.learn_more",
@@ -5543,14 +5543,6 @@
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
"translation": "Los canales de la invitación deben ser parte del equipo al cual fue invitado."
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "La licencia actual no es compatible con cuentas de huéspedes"
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "Las cuentas de huéspedes están deshabilitadas"
},
{
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "Error al invalidar las invitaciones por correo electrónico."
@@ -5945,7 +5937,7 @@
},
{
"id": "api.push_notifications.session.expired",
"translation": "Sesión caducada: Inicie sesión para continuar recibiendo notificaciones. El administrador del sistema configura las sesiones para {{.siteName}} para que caduquen cada {{.daysCount}} día (s)."
"translation": "Sesión vencida: Inicie sesión para continuar recibiendo notificaciones. El administrador del sistema configura las sesiones para {{.siteName}} para que caduquen cada {{.hoursCount}} hora(s)."
},
{
"id": "api.post.error_get_post_id.pending",
@@ -7621,7 +7613,7 @@
},
{
"id": "api.user.get_authorization_code.endpoint.app_error",
"translation": "Error all obtener el endpoint para Documento de Descubrimiento."
"translation": "Error al obtener el endpoint desde Documento de Descubrimiento."
},
{
"id": "api.templates.payment_failed_no_card.title",
@@ -7645,15 +7637,15 @@
},
{
"id": "api.templates.payment_failed.title",
"translation": "Pago fallido"
"translation": "El pago ha fallado"
},
{
"id": "api.templates.payment_failed.subject",
"translation": "Se requiere una acción: El pago de Mattermost Cloud no se ha realizado"
"translation": "Se requiere una acción: El pago de {{.Plan}} Mattermost Cloud no se ha realizado"
},
{
"id": "api.templates.payment_failed.info3",
"translation": "Para asegurar una suscripción ininterrumpida a Mattermost Cloud, por favor, póngase en contacto con su institución financiera para arreglar el problema subyacente o actualizar su información de pago. Una vez actualizada la información de pago, Mattermost intentará liquidar cualquier saldo pendiente."
"translation": "Para asegurar un acceso ininterrumpido a {{.Plan}} Mattermost Cloud, por favor, póngase en contacto con su institución financiera para resolver el problema subyacente o actualizar su información de pago. Una vez actualizada la información de pago, Mattermost intentará liquidar cualquier saldo pendiente."
},
{
"id": "api.templates.payment_failed.info2",
@@ -7981,7 +7973,7 @@
},
{
"id": "api.templates.questions_footer.info",
"translation": "Envíenos un correo electrónico en cualquier momento a "
"translation": "¿Necesitas ayuda o tienes alguna pregunta? Envíanos un correo electrónico a "
},
{
"id": "api.templates.license_up_for_renewal_title",
@@ -8025,11 +8017,11 @@
},
{
"id": "api.templates.email_footer_v2",
"translation": "© 2021 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301"
"translation": "© 2022 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301"
},
{
"id": "api.templates.cloud_welcome_email.title",
"translation": El período de prueba de 14 días de tu espacio de trabajo {{.WorkSpace}} está listo!"
"translation": Tu espacio de trabajo ya está listo para usar!"
},
{
"id": "api.templates.cloud_welcome_email.subtitle_info",
@@ -8089,7 +8081,7 @@
},
{
"id": "api.templates.cloud_welcome_email.add_apps_sub_info",
"translation": "Agiliza tu trabajo con herramientas como Github, Jira y Zoom. Explora todas las integraciones que tenemos en nuestro"
"translation": "Agiliza tu trabajo con herramientas como GitHub, Jira y Zoom. Explora todas las integraciones que tenemos en nuestro"
},
{
"id": "api.command_share.unknown_action",
@@ -9153,7 +9145,7 @@
},
{
"id": "api.templates.invite_team_and_channel_subject",
"translation": " "
"translation": "[{{ .SiteName }}] {{ .SenderName }} te invitó a unirte al Canal {{ .ChannelName }} en el Equipo {{ .TeamDisplayName }}"
},
{
"id": "api.templates.invite_team_and_channels_body.title",
@@ -9237,7 +9229,7 @@
},
{
"id": "api.templates.invite_team_and_channel_body.title",
"translation": " "
"translation": "{{ .SenderName }} te invitó a unirte al Canal {{ .ChannelName }} en el Equipo {{ .TeamDisplayName}}"
},
{
"id": "api.templates.server_inactivity_info_bullet",
@@ -9270,5 +9262,301 @@
{
"id": "api.user.authorize_oauth_user.saml_response_too_long.app_error",
"translation": " "
},
{
"id": "app.cloud.get_subscription_delinquency_date.app_error",
"translation": "La suscripción no es morosa"
},
{
"id": "app.cloud.get_subscription.app_error",
"translation": "No se pudo recuperar la suscripción de la nube"
},
{
"id": "app.cloud.get_current_plan_name.app_error",
"translation": "No se puede obtener el nombre del plan actual"
},
{
"id": "app.cloud.get_cloud_products.app_error",
"translation": "No se pudieron obtener los productos de la nube"
},
{
"id": "api.templates.delinquency_90.title",
"translation": "Tu workspace Mattermost ha sido degradado"
},
{
"id": "api.templates.delinquency_90.subtitle3",
"translation": "Para desarchivar tus datos y mantener las características de pago, actualiza tu información de pago."
},
{
"id": "api.templates.delinquency_90.subtitle2",
"translation": "Además, tus datos pueden haber sido archivados debido a las limitaciones de Cloud Starter."
},
{
"id": "api.templates.delinquency_90.subtitle1",
"translation": "Si usas características Cloud Professional o Enterprise para operaciones importantes de negocio, ya no estarán activas y experimentarás un rendimiento degradado."
},
{
"id": "api.templates.delinquency_90.subject",
"translation": "Tu workspace Mattermost Cloud ha sido degradado"
},
{
"id": "api.templates.delinquency_90.secondary_action_button",
"translation": "Ver Planes y Precios"
},
{
"id": "api.templates.delinquency_90.button",
"translation": "Actualizar pago"
},
{
"id": "api.templates.delinquency_75.title",
"translation": "Tu workspace será degradado en 15 días"
},
{
"id": "api.templates.delinquency_75.subtitle3",
"translation": "Actualiza ahora tu información de pago, o degrada a Cloud Starter."
},
{
"id": "api.templates.delinquency_75.subtitle2",
"translation": "Tu workspace será degradado a Cloud Starter. Las características de tu {{.Plan}} serán bloqueadas y algunos de los datos de tu workspace podrían ser archivados hasta que liquides completamente tu saldo pendiente."
},
{
"id": "api.templates.delinquency_75.subtitle1",
"translation": "Este es un aviso final de que no hemos recibido el pago por tu workspace Mattermost Cloud desde {{.DelinquencyDate}}"
},
{
"id": "api.templates.delinquency_75.subject",
"translation": "Tu {{.Plan}} Mattermost será degradado en 15 días"
},
{
"id": "api.templates.delinquency_75.downgrade_to_starter",
"translation": "Degradar a Cloud Starter"
},
{
"id": "api.templates.delinquency_75.button",
"translation": "Actualizar pago"
},
{
"id": "api.templates.delinquency_7.title",
"translation": "Tu pago no se completó"
},
{
"id": "api.templates.delinquency_7.subtitle2",
"translation": "Para mantener tu plan {{.Plan}} activo, por favor contacta a tu institución financiera tan pronto como sea posible, Luego, actualiza tus detalles de pago según sea necesario."
},
{
"id": "api.templates.delinquency_7.subtitle1",
"translation": "No pudimos procesar tu pago más reciente"
},
{
"id": "api.templates.delinquency_7.button",
"translation": "Actualizar pago"
},
{
"id": "api.templates.delinquency_60.title",
"translation": "Tu workspace Mattermost será degradado en 30 días"
},
{
"id": "api.templates.delinquency_60.subtitle3",
"translation": "Actualiza tu información de pago ahora o degrada a Cloud Starter abajo."
},
{
"id": "api.templates.delinquency_60.subtitle2",
"translation": "Degradaremos tu workspace automáticamente en 30 días si no somos capaces de procesar tu pago."
},
{
"id": "api.templates.delinquency_60.subtitle1",
"translation": "Por favor actualiza pronto tu información de pago para procesar tus facturas pendientes."
},
{
"id": "api.templates.delinquency_60.subject",
"translation": "Acción requerida: El workspace será degradado en 30 días"
},
{
"id": "api.templates.delinquency_60.downgrade_to_starter",
"translation": "Degradar a Cloud Starter"
},
{
"id": "api.templates.delinquency_60.button",
"translation": "Actualizar pago"
},
{
"id": "api.templates.delinquency_45.title",
"translation": "Tu workspace será degradado pronto"
},
{
"id": "api.templates.delinquency_45.subtitle3",
"translation": "Actualiza la información de tu tarjeta de crédito ahora."
},
{
"id": "api.templates.delinquency_45.subtitle2",
"translation": "Un workspace degradado podría afectar negativamente los flujos de trabajo críticos, integraciones y otras actividades críticas de negocio realizadas en tu workspace."
},
{
"id": "api.templates.delinquency_45.subtitle1",
"translation": "No fuimos capaces de cobrar el pago para las facturas con fecha {{.DelinquencyDate}}. Tu workspace está en riesgo de ser degradado."
},
{
"id": "api.templates.delinquency_45.subject",
"translation": "Aviso: Tu {{.Plan}} Mattermost será degradado pronto"
},
{
"id": "api.templates.delinquency_45.button",
"translation": "Actualizar pago"
},
{
"id": "api.templates.delinquency_30.title",
"translation": "Tu workspace será degradado pronto"
},
{
"id": "api.templates.delinquency_30.subtitle2",
"translation": "si no se toma ninguna medida, tu workspace será degradado y los siguientes datos serán archivados:"
},
{
"id": "api.templates.delinquency_30.subtitle1",
"translation": "Tienes tiempo para mantener activo tu {{.Plan}} Mattermost pero necesitarás resolver los problemas con tu método de pago."
},
{
"id": "api.templates.delinquency_30.subject",
"translation": "Actúa para mantener las característica de tu {{.Plan}} Mattermost"
},
{
"id": "api.templates.delinquency_30.limits_documentation",
"translation": "Ver toda la documentación de límites."
},
{
"id": "api.templates.delinquency_30.button",
"translation": "Actualizar pago"
},
{
"id": "api.templates.delinquency_30.bullet.plugins",
"translation": "Plugins e integraciones activas"
},
{
"id": "api.templates.delinquency_30.bullet.message_history",
"translation": "Historial de mensajes"
},
{
"id": "api.templates.delinquency_30.bullet.files",
"translation": "Archivos"
},
{
"id": "api.templates.delinquency_30.bullet.cards",
"translation": "Tarjetas de tus Boards"
},
{
"id": "api.templates.delinquency_14.title",
"translation": "Pago no recibido"
},
{
"id": "api.templates.delinquency_14.subtitle2",
"translation": "Por favor contacta a tu institución financiera para resolver cualquier problema. Luego, actualiza los detalles de pago como sean necesarios."
},
{
"id": "api.templates.delinquency_14.subtitle1",
"translation": "No pudimos realizar el cargo a la tarjeta de crédito que tenemos registrada. Por lo cual tu workspace está en riesgo de ser degradado a Cloud Starter."
},
{
"id": "api.templates.delinquency_14.subject",
"translation": "El pago está vencido para tu {{.Plan}} Mattermost."
},
{
"id": "api.templates.delinquency_14.button",
"translation": "Actualizar pago"
},
{
"id": "api.team.invite_guests_to_channels.license.error",
"translation": "Tu licencia no admite cuentas de invitado"
},
{
"id": "api.team.invite_guests_to_channels.disabled.error",
"translation": "Las cuentas de invitado están deshabilitadas"
},
{
"id": "api.command_marketplace.unsupported.app_error",
"translation": "El comando marketplace no es soportado por tu dispositivo."
},
{
"id": "api.command_marketplace.name",
"translation": "marketplace"
},
{
"id": "api.command_marketplace.desc",
"translation": "Abre el Marketplace"
},
{
"id": "api.command_help.success",
"translation": "Mattermost es una plataforma de código abierto para comunicación segura, colaboración, y orquestación de trabajo entre herramientas y equipos.\nMattermost contiene tres herramientas clave:\n\n**Channels** - Mantente conectado con tu equipo vía 1:1 y en por mensajes grupales.\n**[Playbooks](/playbooks)** - Construye y configura procesos repetibles para lograr resultados específicos y predecibles.\n**[Tableros](/boards)** - Administra proyectos y tareas en una estructura de tablero Kanban para ayudar a que tú equipo logre los hitos clave.\n\n[Ver documentación y guías]({{.HelpLink}})"
},
{
"id": "app.cloud.trial_plan_bot_message",
"translation": "los miembros {{.UsersNum}} del espacio de trabajo {{.WorkspaceName}} han solicitado comenzar la prueba Enterprise para acceder a: "
},
{
"id": "api.cloud.delinquency_email.missing_email_to_trigger",
"translation": "Campos faltantes requeridos para enviar el correo electrónico de morosidad."
},
{
"id": "model.group.name.reserved_name.app_error",
"translation": "el nombre de grupo ya existe como un nombre reservado"
},
{
"id": "model.config.is_valid.amazons3_timeout.app_error",
"translation": "Valor de timeout inválido {{.Value}}. Debe ser un número positivo."
},
{
"id": "ent.saml.configure.certificate_parse_error.app_error",
"translation": "SAML no pudo cargar satisfactoriamente el Identity Provider Public Certificate, por favor contacta a tu administrador del sistema."
},
{
"id": "app.user.get_badge_count.app_error",
"translation": "No pudimos obtener el recuento de insignias para el usuario."
},
{
"id": "app.team.clear_cache.app_error",
"translation": "Error al limpiar el caché de los miembros del equipo"
},
{
"id": "app.post_reminder_dm",
"translation": "Hola, este es un recordatorio acerca de este mensaje de @{{.Username}}: {{.SiteURL}}/{{.TeamName}}/pl/{{.PostId}}"
},
{
"id": "app.post.get_top_dms_for_user_since.app_error",
"translation": "No es posible obtener los DMs top para el usuario."
},
{
"id": "app.plugin.product_mode.app_error",
"translation": "El plugin {{.Name}} no se pudo activar en el modo producto."
},
{
"id": "app.notify_admin.send_notification_post.app_error",
"translation": "No es posible enviar la publicación de notificación."
},
{
"id": "app.notify_admin.save.app_error",
"translation": "No es posible guardar los datos de notificación."
},
{
"id": "app.last_accessible_file.app_error",
"translation": "Error al obtener el último archivo accesible"
},
{
"id": "app.job.error",
"translation": "Error durante la ejecución de un job."
},
{
"id": "app.file.cloud.get.app_error",
"translation": "No se puede obtener el archivo porque superó los límites del plan en la nube."
},
{
"id": "app.cloud.upgrade_plan_bot_message_single",
"translation": "el miembro {{.UsersNum}} del workspace {{.WorkspaceName}} ha solicitado una actualización del workspace para: "
},
{
"id": "app.cloud.upgrade_plan_bot_message",
"translation": "los miembros {{.UsersNum}} del workspace {{.WorkspaceName}} han solicitado una actualización del workspace para: "
},
{
"id": "app.cloud.trial_plan_bot_message_single",
"translation": "el miembro {{.UsersNum}} del workspace {{.WorkspaceName}} ha solicitado comenzar la prueba Enterprise para acceder a: "
}
]

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

@@ -5159,14 +5159,6 @@
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
"translation": "کانال های دعوت باید بخشی از تیم دعوت باشند."
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "مجوز شما از حساب های مهمان پشتیبانی نمی کند"
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "حساب های مهمان غیرفعال شده است"
},
{
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "خطا در عدم اعتبار دعوت نامه های ایمیل."

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

@@ -5583,14 +5583,6 @@
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
"translation": "Les canaux présents dans l'invitation doivent faire partie de la même équipe pour laquelle vous êtes invité."
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "Votre licence ne prend pas en charge les comptes invités."
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "Les comptes invités sont désactivés"
},
{
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "Une erreur s'est produite lors de l'invalidation des e-mails d'invitation."

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

@@ -3125,7 +3125,7 @@
},
{
"id": "oauth.gitlab.tos.error",
"translation": "A GitLab Általános Szerződési Feltételei frissültek. Kérjük, látogassa meg a gitlab.com webhelyet, hogy elfogadja őket, majd próbáljon meg újra bejelentkezni a Mattermost-ba."
"translation": "A GitLab Általános Szerződési Feltételei frissültek. Kérjük, látogassa meg a {{.URL}} webhelyet, hogy elfogadja őket, majd próbáljon meg újra bejelentkezni a Mattermost-ba."
},
{
"id": "model.websocket_client.connect_fail.app_error",
@@ -5469,15 +5469,15 @@
},
{
"id": "api.templates.payment_failed.title",
"translation": "Sikertelen fizetés"
"translation": "A fizetési művelet nem sikerült"
},
{
"id": "api.templates.payment_failed.subject",
"translation": "Művelet szükséges: Sikertelen fizetés a Mattermost Cloudra"
"translation": "Művelet szükséges: Sikertelen fizetés a Mattermost {{.Plan}} csomagra"
},
{
"id": "api.templates.payment_failed.info3",
"translation": "A Mattermost Cloud zavartalan előfizetésének biztosítása érdekében kérjük, vegye fel a kapcsolatot a pénzintézetével a probléma megoldása érdekében, vagy frissítse fizetési adatait. A fizetési adatok frissítése után a Mattermost megpróbálja rendezni a fennmaradó egyenleget."
"translation": "A Mattermost {{.Plan}} zavartalan elérésének biztosítása érdekében kérjük, vegye fel a kapcsolatot a pénzintézetével a probléma megoldása érdekében, vagy frissítse fizetési adatait. A fizetési adatok frissítése után a Mattermost megpróbálja rendezni a fennmaradó egyenleget."
},
{
"id": "api.templates.payment_failed.info2",
@@ -5795,14 +5795,6 @@
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
"translation": "A meghívás csatornáinak a meghívó csapat részének kell lenniük."
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "Az Ön licence nem támogatja a vendég fiókokat"
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "A vendég fiókok le vannak tiltva"
},
{
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "Hiba történt az e-mail meghívók érvénytelenítésekor."
@@ -9354,5 +9346,45 @@
{
"id": "api.templates.delinquency_30.subject",
"translation": "Lépjen most, hogy megtarthassa a Mattermost {{.Plan}} funkcióit"
},
{
"id": "app.notify_admin.send_notification_post.app_error",
"translation": "Nem sikerült elküldeni az értesítési bejegyzést."
},
{
"id": "app.notify_admin.save.app_error",
"translation": "Nem sikerült elmenteni az értesítési beállításokat."
},
{
"id": "app.job.error",
"translation": "Hiba a művelet végrehajtása közben."
},
{
"id": "app.cloud.get_current_plan_name.app_error",
"translation": "Nem sikerült lekérni a jelenlegi előfizetési csomag nevét"
},
{
"id": "api.templates.delinquency_30.button",
"translation": "Fizetés frissítése"
},
{
"id": "api.templates.delinquency_30.bullet.plugins",
"translation": "Aktív bővítmények és integrációk"
},
{
"id": "api.cloud.delinquency_email.missing_email_to_trigger",
"translation": "Hiányzó kötelező mezők a késedelmes e-mail küldéséhez."
},
{
"id": "app.file.cloud.get.app_error",
"translation": "Nem sikerült lekérni a fájlt, mivel az meghaladta a felhőcsomag határértékét."
},
{
"id": "app.user.get_badge_count.app_error",
"translation": "Nem sikerült lekérni a felhasználó jelvény számlálóját."
},
{
"id": "model.group.name.reserved_name.app_error",
"translation": "csoport név létezik mint lefoglalt név"
}
]

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

@@ -5211,10 +5211,6 @@
"id": "api.team.add_members.user_denied",
"translation": "Questa squadra è gestita dai gruppi. Questo utente non appartiene a un gruppo sincronizzato con questa squadra."
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "Gli account ospite sono disattivati"
},
{
"id": "api.team.remove_member.group_constrained.app_error",
"translation": "Impossibile rimuovere un utente da una squadra con vincoli di gruppo."
@@ -5643,10 +5639,6 @@
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "Errore invalidando gli inviti email."
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "La tua licenza non supporta gli account ospite"
},
{
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
"translation": "I canali dell'invito devono far parte della squadra dell'invito."

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

@@ -5579,14 +5579,6 @@
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
"translation": "招待チャンネルは招待チーム内のチャンネルでなくてはなりません。"
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "現在のライセンスではゲストアカウントはサポートされていません"
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "ゲストアカウントは無効化されています"
},
{
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "電子メール招待の無効化でエラーが発生しました。"

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

@@ -619,7 +619,7 @@
},
{
"id": "api.command_help.desc",
"translation": "Mattermost 도움말 페이지 열기"
"translation": "Mattermost 도움말 기"
},
{
"id": "api.command_help.name",
@@ -5575,14 +5575,6 @@
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
"translation": "초대 채널은 초대한 팀의 채널이어야 합니다."
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "게스트 계정을 지원하지 않는 라이선스입니다"
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "게스트 계정이 비활성화되어 있습니다"
},
{
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "전자우편 초대에서 유효하지 않는 오류가 발생했습니다."
@@ -7934,5 +7926,41 @@
{
"id": "api.cloud.teams_limit_reached.create",
"translation": "팀 제한에 도달했기 때문에 팀을 만들 수 없습니다"
},
{
"id": "api.command_marketplace.desc",
"translation": "마켓플레이스 열기"
},
{
"id": "api.command_help.success",
"translation": "Mattermost는 도구와 팀을 넘나드는 안전한 소통, 협업, 작업 조율을 위한 오픈소스 플랫폼입니다.\nMattermost는 세가지 핵심 도구를 갖고 있습니다:\n\n**채널** - 1:1 및 그룹 채팅을 통해 팀과의 소통을 유지하세요.\n**[플레이북](/playbooks)** - 반복가능한 작업들을 구성하여 구체적이고 예측 가능한 결과를 만드세요.\n**[보드](/boards)** - 팀의 핵심 마일스톤에 도달하기 위해 칸반 보드에서 작업을 만들고 프로젝트를 관리하세요.\n\n[문서 및 가이드 살펴보기]({{.HelpLink}})"
},
{
"id": "api.cloud.delinquency_email.missing_email_to_trigger",
"translation": "연체 이메일을 보내기 위한 필수 항목이 누락되었습니다."
},
{
"id": "api.error_set_first_admin_complete_setup",
"translation": "스토어에서 첫 번째 관리자의 설정을 저장하는 동안 오류가 발생했습니다."
},
{
"id": "api.error_get_first_admin_visit_marketplace_status",
"translation": "스토어에서 첫 번째 관리자의 마켓플레이스 방문 상태를 검색하는 동안 오류가 발생했습니다."
},
{
"id": "api.error_get_first_admin_complete_setup",
"translation": "스토어에서 첫 번째 관리자 설정을 검색하는 동안 오류가 발생했습니다."
},
{
"id": "api.command_remote.invite_summary",
"translation": "AES-256으로 암호화된 초대를 비밀번호와 함께 외부 Mattermost 시스템 관리자에게 보냅니다. 초대를 받기 위해 `{{.Command}}` 슬래시 명령어를 사용합니다.\n\n```\n{{.Invitation}}\n```\n\n**보안 연결이 다음을 통해 Mattermost에 액세스할 수 있는지 확인하십시오** {{.SiteURL}}"
},
{
"id": "api.command_marketplace.unsupported.app_error",
"translation": "마켓플레이스 명령어는 현재 기기에서 지원하지 않습니다."
},
{
"id": "api.command_marketplace.name",
"translation": "마켓플레이스"
}
]

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

@@ -1777,7 +1777,7 @@
},
{
"id": "api.templates.email_change_verify_body.title",
"translation": "Je hebt je e-mailadres bijgewerkt"
"translation": "Je hebt je e-mailadres succesvol bijgewerkt"
},
{
"id": "api.templates.email_change_verify_subject",
@@ -4325,7 +4325,7 @@
},
{
"id": "model.post.is_valid.create_at.app_error",
"translation": "Aangemaakt op moet een geldige tijd bevatten"
"translation": "Aangemaakt op moet een geldige tijd bevatten."
},
{
"id": "model.post.is_valid.file_ids.app_error",
@@ -4781,11 +4781,11 @@
},
{
"id": "web.error.unsupported_browser.min_browser_version.edge",
"translation": "Versie 44+"
"translation": "Versie 95+"
},
{
"id": "web.error.unsupported_browser.min_browser_version.chrome",
"translation": "Versie 100+"
"translation": "Versie 106+"
},
{
"id": "web.error.unsupported_browser.learn_more",
@@ -5607,14 +5607,6 @@
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
"translation": "De kanalen van de uitgenodigde moet een kanaal dat deel uitmaakt van het team."
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "Je licentie ondersteunt geen gastgebruikers"
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "Gast accounts zijn uitgeschakeld"
},
{
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "Fout bij afkeuren e-mailuitnodigingen."
@@ -9537,5 +9529,33 @@
{
"id": "api.templates.delinquency_90.title",
"translation": "Jouw Mattermost-werkruimte werd gedowngraded"
},
{
"id": "app.job.error",
"translation": "Fout tijdens het uitvoeren van de opdracht."
},
{
"id": "model.group.name.reserved_name.app_error",
"translation": "groepsnaam bestaat al als een gereserveerde naam"
},
{
"id": "app.plugin.product_mode.app_error",
"translation": "Plugin {{.Name}} kan niet worden ingeschakeld in de productmodus."
},
{
"id": "app.last_accessible_file.app_error",
"translation": "Fout bij het ophalen van het laatst toegankelijke bestand"
},
{
"id": "app.file.cloud.get.app_error",
"translation": "Kan het bestand niet ophalen omdat dit bestand over de limiet van het cloud plan is."
},
{
"id": "api.team.invite_guests_to_channels.license.error",
"translation": "Jouw licentie ondersteunt geen gastaccounts"
},
{
"id": "api.team.invite_guests_to_channels.disabled.error",
"translation": "Gastaccounts zijn uitgeschakeld"
}
]

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

@@ -4761,11 +4761,11 @@
},
{
"id": "web.error.unsupported_browser.min_browser_version.edge",
"translation": "Wersja 44+"
"translation": "Wersja 95+"
},
{
"id": "web.error.unsupported_browser.min_browser_version.chrome",
"translation": "Wersja 100+"
"translation": "Wersja 106+"
},
{
"id": "web.error.unsupported_browser.learn_more",
@@ -5583,14 +5583,6 @@
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
"translation": "Kanały do których chcesz zaprosić, muszą być częścią zespołu do którego chcesz zaprosić."
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "Twoja licencja nie obsługuje kont gości"
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "Konta gości są wyłączone"
},
{
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "Błąd unieważniania zaproszenia E-Mail."
@@ -9542,5 +9534,29 @@
{
"id": "app.job.error",
"translation": "Błąd podczas wykonywania zadania."
},
{
"id": "app.last_accessible_file.app_error",
"translation": "Błąd pobierania ostatniego dostępnego pliku"
},
{
"id": "app.file.cloud.get.app_error",
"translation": "Nie można pobrać pliku, ponieważ przekroczony został limit planu chmury."
},
{
"id": "model.group.name.reserved_name.app_error",
"translation": "nazwa grupy już istnieje jako nazwa zastrzeżona"
},
{
"id": "app.plugin.product_mode.app_error",
"translation": "Wtyczka {{.Name}} nie może być włączona w trybie produktu."
},
{
"id": "api.team.invite_guests_to_channels.license.error",
"translation": "Twoja licencja nie wspiera kont gości"
},
{
"id": "api.team.invite_guests_to_channels.disabled.error",
"translation": "Konta gości są wyłączone"
}
]

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

@@ -621,7 +621,7 @@
},
{
"id": "api.command_help.desc",
"translation": "Abrir a página de ajuda do Mattermost"
"translation": "Exibir descrição de ajuda do Mattermost"
},
{
"id": "api.command_help.name",
@@ -1173,7 +1173,7 @@
},
{
"id": "api.marshal_error",
"translation": "marshal error"
"translation": "Falha para marshal."
},
{
"id": "api.oauth.allow_oauth.redirect_callback.app_error",
@@ -1789,7 +1789,7 @@
},
{
"id": "api.templates.email_footer",
"translation": "Para alterar suas preferencias de notificação, faça login no site da sua equipe e vá para Configurações de Conta > Notificações."
"translation": "Para alterar suas preferencias de notificação, faça login no site da sua equipe e vá para Configurações > Notificações."
},
{
"id": "api.templates.email_info1",
@@ -1813,7 +1813,7 @@
},
{
"id": "api.templates.invite_body.button",
"translation": "Entrar na Equipe"
"translation": "Entrar agora"
},
{
"id": "api.templates.invite_body.title",
@@ -1857,7 +1857,7 @@
},
{
"id": "api.templates.post_body.button",
"translation": "Ir Para a Publicação"
"translation": "Responder no Mattermost"
},
{
"id": "api.templates.reset_body.button",
@@ -1865,7 +1865,7 @@
},
{
"id": "api.templates.reset_body.title",
"translation": "Você solicitou uma redefinição de senha"
"translation": "Redefina sua senha"
},
{
"id": "api.templates.reset_subject",
@@ -4333,11 +4333,11 @@
},
{
"id": "model.post.is_valid.file_ids.app_error",
"translation": "Ids do arquivo inválido. Observe que os uploads estão limitados a cinco arquivos no máximo. Por favor, use publicações adicionais para mais arquivos."
"translation": "Ids do arquivo inválido. Observe que os uploads estão limitados a 10 arquivos no máximo. Por favor, use publicações adicionais para mais arquivos."
},
{
"id": "model.post.is_valid.filenames.app_error",
"translation": "Nomes de arquivo inválidos"
"translation": "Nomes de arquivo inválidos."
},
{
"id": "model.post.is_valid.hashtags.app_error",
@@ -4573,7 +4573,7 @@
},
{
"id": "oauth.gitlab.tos.error",
"translation": "Os Termos de Serviço do GitLab foram atualizados. Por favor vá até gitlab.com para aceitar os termos e depois tente se logar novamente no Mattermost."
"translation": "Os Termos de Serviço do GitLab foram atualizados. Por favor vá até {{.URL}} para aceitar os termos e depois tente se logar novamente no Mattermost."
},
{
"id": "plugin.api.update_user_status.bad_status",
@@ -4745,19 +4745,19 @@
},
{
"id": "web.error.unsupported_browser.min_os_version.windows",
"translation": "Windows 7+"
"translation": "Windows 8.1+"
},
{
"id": "web.error.unsupported_browser.min_os_version.mac",
"translation": "macOS 10.9+"
"translation": "macOS 10.14+"
},
{
"id": "web.error.unsupported_browser.min_browser_version.safari",
"translation": "Versão 12+"
"translation": "Versão 14.1+"
},
{
"id": "web.error.unsupported_browser.min_browser_version.firefox",
"translation": "Versão 60+"
"translation": "Versão 91+"
},
{
"id": "web.error.unsupported_browser.min_browser_version.edge",
@@ -4765,7 +4765,7 @@
},
{
"id": "web.error.unsupported_browser.min_browser_version.chrome",
"translation": "Versão 61+"
"translation": "Version 100+"
},
{
"id": "web.error.unsupported_browser.learn_more",
@@ -5587,14 +5587,6 @@
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
"translation": "Os canais do convite devem ser parte da equipe do convite."
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "Sua licença não suporta contas convidado"
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "Contas convidado estão desativadas"
},
{
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "Erro ao invalidar convites por email."
@@ -5945,7 +5937,7 @@
},
{
"id": "api.push_notifications.session.expired",
"translation": "Sessão expirada: faça login para continuar recebendo notificações. As sessões para {{.siteName}} estão configuradas pelo Administrador do Sistema para expirar a cada {{.daysCount}} dia(s)."
"translation": "Sessão expirada: faça login para continuar recebendo notificações. As sessões para {{.siteName}} estão configuradas pelo Administrador do Sistema para expirar a cada {{.hoursCount}} hour(s)."
},
{
"id": "api.post.error_get_post_id.pending",
@@ -6189,7 +6181,7 @@
},
{
"id": "app.preference.permanent_delete_by_user.app_error",
"translation": "Encontramos um erro enquanto excluía as preferências."
"translation": "Encontramos um erro enquanto excluiamos as preferências."
},
{
"id": "app.preference.get_category.app_error",
@@ -6897,7 +6889,7 @@
},
{
"id": "model.upload_session.is_valid.user_id.app_error",
"translation": "Valor inválido para user_id."
"translation": "Valor inválido para UserId"
},
{
"id": "model.upload_session.is_valid.type.app_error",
@@ -6905,15 +6897,15 @@
},
{
"id": "model.upload_session.is_valid.path.app_error",
"translation": "Valor inválido para path."
"translation": "Valor inválido para path"
},
{
"id": "model.upload_session.is_valid.id.app_error",
"translation": "Valor inválido para id."
"translation": "Valor inválido para id"
},
{
"id": "model.upload_session.is_valid.create_at.app_error",
"translation": "Valor inválido para create_at."
"translation": "Valor inválido para Create_At"
},
{
"id": "app.upload.upload_data.large_image.app_error",
@@ -7601,7 +7593,7 @@
},
{
"id": "api.templates.copyright",
"translation": "© 2020 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301"
"translation": "© 2021 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301"
},
{
"id": "api.roles.patch_roles.not_allowed_permission.error",
@@ -7645,15 +7637,15 @@
},
{
"id": "api.templates.payment_failed.title",
"translation": "Pagamento Falhou"
"translation": "O Pagamento não foi bem sucedido"
},
{
"id": "api.templates.payment_failed.subject",
"translation": "Ação necessária: Falha no pagamento para Mattermost Cloud"
"translation": "Ação necessária: Falha no pagamento para Mattermost {{.Plan}}"
},
{
"id": "api.templates.payment_failed.info3",
"translation": "Para garantir a assinatura ininterrupta do Mattermost Cloud, entre em contato com sua instituição financeira para corrigir o problema subjacente ou atualize suas informações de pagamento. Assim que as informações de pagamento forem atualizadas, a Mattermost tentará liquidar o saldo pendente."
"translation": "Para garantir a acesso ininterrupto do Mattermost {{.Plan}}, entre em contato com sua instituição financeira para corrigir o problema subjacente ou atualize suas informações de pagamento. Assim que as informações de pagamento forem atualizadas, a Mattermost tentará liquidar o saldo pendente."
},
{
"id": "api.templates.payment_failed.info2",
@@ -8069,7 +8061,7 @@
},
{
"id": "api.user.get_authorization_code.endpoint.app_error",
"translation": "Erro ao recuperar o endpoint da Descoberta de Documento."
"translation": "Erro ao recuperar o endpoint do Documento de Descoberta."
},
{
"id": "api.templates.welcome_body.subTitle2",
@@ -8117,11 +8109,11 @@
},
{
"id": "api.templates.questions_footer.info",
"translation": "Envie-nos um email a qualquer momento em "
"translation": "Precisa de ajuda ou tem dúvidas? Envie-nos um e-mail em "
},
{
"id": "api.templates.email_footer_v2",
"translation": "© 2020 Mattermost, Inc. 855 El Camino Real, 13A-168, Palo Alto, CA, 94301"
"translation": "© 2022 Mattermost, Inc. 530 Lytton Avenue, Second floor, Palo Alto, CA, 94301"
},
{
"id": "api.post.search_files.invalid_body.app_error",
@@ -8333,7 +8325,7 @@
},
{
"id": "api.context.remote_id_invalid.app_error",
"translation": "Não foi possível encontrar o id do cluster remoto {{.RemoteId}}."
"translation": "Não foi possível encontrar o ID de uma conexão segura {{.RemoteId}}."
},
{
"id": "api.context.json_encoding.app_error",
@@ -8357,15 +8349,15 @@
},
{
"id": "api.command_share.remote_uninvited",
"translation": "Remoto `{{.RemoteId}}` sem convite."
"translation": "Conexão segura `{{.RemoteId}}` sem convite."
},
{
"id": "api.command_share.remote_id_invalid.error",
"translation": "O id do cluster remoto é inválido: {{.Error}}"
"translation": "O ID de conexão segura é inválido: {{.Error}}"
},
{
"id": "api.command_share.remote_id.help",
"translation": "Id de uma instância remota existente. Veja o comando `remote` para adicionar uma instância remota."
"translation": "ID de uma conexão segura existente. Veja o comando `remote` para adicionar uma conexão segura."
},
{
"id": "api.command_share.permission_required",
@@ -8437,7 +8429,7 @@
},
{
"id": "api.templates.cloud_welcome_email.signin_sub_info2",
"translation": "para a melhor experiência em PC, Mac, iOS e Android."
"translation": "para a melhor experiência em Windows, Linux, Mac, iOS e Android."
},
{
"id": "api.templates.cloud_welcome_email.signin_sub_info",
@@ -8469,7 +8461,7 @@
},
{
"id": "api.templates.cloud_welcome_email.add_apps_sub_info",
"translation": "Otimize seu trabalho com ferramentas como Github, Google Agenda e Chrome. Explore todas as integrações que temos"
"translation": "Otimize seu trabalho com ferramentas como GitHub, Jira e Zoom. Explore todas as integrações que temos em nosso"
},
{
"id": "api.templates.cloud_welcome_email.add_apps_info",
@@ -8481,11 +8473,11 @@
},
{
"id": "api.remote_cluster.update_not_unique.app_error",
"translation": "Já existe um cluster remoto com a mesmo url."
"translation": "Já existe uma conexão segura com a mesmo url."
},
{
"id": "api.remote_cluster.update.app_error",
"translation": "Encontramos um erro ao atualizar o cluster remoto."
"translation": "Encontramos um erro ao atualizar a conexão segura."
},
{
"id": "api.remote_cluster.service_not_enabled.app_error",
@@ -8493,11 +8485,11 @@
},
{
"id": "api.remote_cluster.save_not_unique.app_error",
"translation": "O cluster remoto já foi adicionado."
"translation": "A conexão segura já foi adicionada."
},
{
"id": "api.remote_cluster.save.app_error",
"translation": "Encontramos um erro ao salvar o cluster remoto."
"translation": "Encontramos um erro ao salvar a conexão segura."
},
{
"id": "api.remote_cluster.invalid_topic.app_error",
@@ -8509,11 +8501,11 @@
},
{
"id": "api.remote_cluster.get.app_error",
"translation": "Encontramos um erro ao recuperar um cluster remoto."
"translation": "Encontramos um erro ao recuperar uma conexão segura."
},
{
"id": "api.remote_cluster.delete.app_error",
"translation": "Encontramos um erro ao excluir o cluster remoto."
"translation": "Encontramos um erro ao excluir uma conexão segura."
},
{
"id": "api.job.unable_to_create_job.incorrect_job_type",
@@ -8533,11 +8525,11 @@
},
{
"id": "api.context.remote_id_missing.app_error",
"translation": "Falta a id do cluster remoto."
"translation": "Falta a ID de conexão sergura."
},
{
"id": "api.context.remote_id_mismatch.app_error",
"translation": "Incompatibilidade de id do cluster remoto."
"translation": "Incompatibilidade de ID da conexão segura."
},
{
"id": "api.context.invitation_expired.error",
@@ -8549,11 +8541,11 @@
},
{
"id": "api.command_share.uninvite_remote_id.help",
"translation": "Id da instância remota para desconvidar."
"translation": "ID de uma conexão segura para desconvidar."
},
{
"id": "api.command_share.uninvite_remote.help",
"translation": "Cancela o convite de uma instância remota deste canal compartilhado"
"translation": "Cancela o convite de uma conexão segura deste canal compartilhado"
},
{
"id": "api.command_share.shared_channel_unavailable",
@@ -8569,11 +8561,11 @@
},
{
"id": "api.command_share.remote_not_valid",
"translation": "Deve especificar um cluster remoto válido para cancelar o convite"
"translation": "Deve especificar uma ID de conexão segura válida para cancelar o convite"
},
{
"id": "api.command_share.remote_already_invited",
"translation": "O cluster remoto já foi convidado."
"translation": "A conexão segura já foi convidada."
},
{
"id": "api.command_share.not_shared_channel_unshare",
@@ -8581,15 +8573,15 @@
},
{
"id": "api.command_share.no_remote_invited",
"translation": "Nenhum remoto foi convidado para este canal compartilhado."
"translation": "Nenhuma conexão remota foi convidada a este canal."
},
{
"id": "api.command_share.name",
"translation": "compartilhar"
"translation": "compartilhar-canal"
},
{
"id": "api.command_share.must_specify_valid_remote",
"translation": "Deve especificar um ID de conjunto remoto válido para convidar."
"translation": "Deve especificar um ID de conjunto válido seguro para convidar."
},
{
"id": "api.command_share.missing_action",
@@ -8650,5 +8642,13 @@
{
"id": "Boards",
"translation": "Quadros"
},
{
"id": "Playbooks",
"translation": "Playbooks"
},
{
"id": "api.cloud.delinquency_email.missing_email_to_trigger",
"translation": "Campos faltando para envio de email."
}
]

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

@@ -5587,14 +5587,6 @@
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
"translation": "Canalele invitației trebuie să facă parte din echipa invitației."
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "Licența dvs. nu acceptă conturile de invitați"
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "Conturile oaspeților sunt dezactivate"
},
{
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "Eroare la invalidarea invitațiilor de e-mail."

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

@@ -5235,10 +5235,6 @@
"id": "api.team.demote_user_to_guest.license.error",
"translation": "Ваша лицензия не поддерживает гостевые учётные записи"
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "Ваша лицензия не поддерживает гостевые учётные записи"
},
{
"id": "api.team.remove_member.group_constrained.app_error",
"translation": "Невозможно удалить пользователя из управляемой группами команды."
@@ -5675,10 +5671,6 @@
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "Ошибка аннулирования e-mail приглашений."
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "Гостевые учётные записи отключены"
},
{
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
"translation": "Каналы в приглашении должны быть частью команды в приглашении."

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

@@ -5275,14 +5275,6 @@
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
"translation": "De kanaler som ingår i inbjudan måste tillhöra de team som inbjudan gäller för."
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "Din licens tillåter inte gäståtkomst"
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "Gäståtkomst har inaktiverats"
},
{
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "Fel vid ogiltiggörande av e-postinbjudan."
@@ -9541,5 +9533,25 @@
{
"id": "app.job.error",
"translation": "Fel under utförandet av jobbet."
},
{
"id": "model.group.name.reserved_name.app_error",
"translation": "gruppnamnet finns redan som ett reserverat namn"
},
{
"id": "app.last_accessible_file.app_error",
"translation": "Fel vid hämtning av den senast tillgängliga filen"
},
{
"id": "app.file.cloud.get.app_error",
"translation": "Ditt abonnemang begränsar möjligheten att hämta filen."
},
{
"id": "api.team.invite_guests_to_channels.license.error",
"translation": "Din licens tillåter inte gäståtkomst"
},
{
"id": "api.team.invite_guests_to_channels.disabled.error",
"translation": "Gäståtkomst har inaktiverats"
}
]

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

@@ -2725,7 +2725,7 @@
},
{
"id": "app.import.validate_team_import_data.name_reserved.error",
"translation": "Takım adında ayrılmış bir sözcük var."
"translation": "Takım adında sistem kullanımına ayrılmış bir sözcük var."
},
{
"id": "app.import.validate_team_import_data.scheme_invalid.error",
@@ -5577,7 +5577,7 @@
},
{
"id": "api.ldap_groups.existing_reserved_name_error",
"translation": "aynı adlı bir grup zaten ayrılmış bir ad olarak var"
"translation": "aynı adlı bir grup zaten sistem kullanımına ayrılmış bir ad olarak var"
},
{
"id": "api.ldap_groups.existing_user_name_error",
@@ -5655,14 +5655,6 @@
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "E-posta çağrıları geçersiz kılınırken sorun çıktı."
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "Konuk hesapları kullanılmıyor"
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "Lisansınızda konuk hesaplarını kullanma özelliği yok"
},
{
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
"translation": "Çağrılan kanallar çağrı yapılan takımın bir parçası olmalıdır."
@@ -5849,11 +5841,11 @@
},
{
"id": "web.error.unsupported_browser.min_browser_version.chrome",
"translation": "Sürüm 100+"
"translation": "Sürüm 106+"
},
{
"id": "web.error.unsupported_browser.min_browser_version.edge",
"translation": "Sürüm 44+"
"translation": "Sürüm 95+"
},
{
"id": "web.error.unsupported_browser.min_browser_version.firefox",
@@ -9537,5 +9529,33 @@
{
"id": "app.user.get_badge_count.app_error",
"translation": "Kullanıcının nişan sayısını alamadık."
},
{
"id": "app.job.error",
"translation": "Görev yürütülürken sorun çıktı."
},
{
"id": "app.last_accessible_file.app_error",
"translation": "Erişilebilen son dosya alınırken sorun çıktı"
},
{
"id": "app.file.cloud.get.app_error",
"translation": "Cloud tarifesinin sınırlarını aştığından dosya alınamadı."
},
{
"id": "model.group.name.reserved_name.app_error",
"translation": "aynı adlı bir grup zaten sistem kullanımına ayrılmış bir ad olarak var"
},
{
"id": "app.plugin.product_mode.app_error",
"translation": "{{.Name}} uygulama eki ürün kipinde etkinleştirilemez."
},
{
"id": "api.team.invite_guests_to_channels.license.error",
"translation": "Lisansınız konuk hesaplarının kullanılmasını desteklemiyor"
},
{
"id": "api.team.invite_guests_to_channels.disabled.error",
"translation": "Konuk hesapları devre dışı bırakılmış"
}
]

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

@@ -5583,14 +5583,6 @@
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
"translation": "The channels of the invite must be part of the team of the invite."
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "Редакція не підтримує Elasticsearch. "
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "Guest accounts are disabled"
},
{
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "Error invalidating email invites."
@@ -6858,5 +6850,17 @@
{
"id": "api.admin.add_certificate.parseform.app_error",
"translation": "Помилка синтаксісу багато-форматного запиту"
},
{
"id": "api.channel.patch_channel_moderations.cache_invalidation.error",
"translation": "Помилка валідації кешу"
},
{
"id": "api.back_to_app",
"translation": "Повернутися до {{.SiteName}}"
},
{
"id": "Channels",
"translation": "Канали"
}
]

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

@@ -5611,10 +5611,6 @@
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "作废邮件邀请错误。"
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "您的许可证不支持访客帐号"
},
{
"id": "api.team.remove_member.group_constrained.app_error",
"translation": "无法从组受限团队移除用户。"
@@ -5627,10 +5623,6 @@
"id": "api.team.search_teams.pagination_not_implemented.public_team_search",
"translation": "仅搜索公共团队不支持分页。"
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "访客帐号已停用"
},
{
"id": "api.templates.remove_expired_license.subject",
"translation": "Mattermost 企业许可证已停用。"

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

@@ -421,7 +421,7 @@
},
{
"id": "api.command_channel_header.update_channel.app_error",
"translation": "更新當前頻道時錯誤。"
"translation": "更新當前頻道名稱時錯誤。"
},
{
"id": "api.command_channel_purpose.channel.app_error",
@@ -453,7 +453,7 @@
},
{
"id": "api.command_channel_purpose.update_channel.app_error",
"translation": "更新當前頻道時錯誤。"
"translation": "更新當前頻道用途時錯誤。"
},
{
"id": "api.command_channel_remove.channel.app_error",
@@ -619,7 +619,7 @@
},
{
"id": "api.command_help.desc",
"translation": "開啟 Mattermost 說明頁面"
"translation": "顯示 Mattermost 說明頁面"
},
{
"id": "api.command_help.name",
@@ -5579,14 +5579,6 @@
"id": "api.team.invite_guests.channel_in_invalid_team.app_error",
"translation": "邀請的頻道必須屬於邀請的團隊"
},
{
"id": "api.team.invate_guests_to_channels.license.error",
"translation": "授權不支援訪客帳號"
},
{
"id": "api.team.invate_guests_to_channels.disabled.error",
"translation": "訪客帳號已停用"
},
{
"id": "api.team.invalidate_all_email_invites.app_error",
"translation": "使電子郵件邀請無效時失敗。"
@@ -7302,5 +7294,57 @@
{
"id": "api.cloud.teams_limit_reached.create",
"translation": "無法建立團隊,已達到團隊數量上限"
},
{
"id": "api.command_custom_status.success",
"translation": "你的狀態已設為 “{{.EmojiName}} {{.StatusMessage}}”。您可以從頻道側邊欄中的狀態彈出視窗更改您的狀態。"
},
{
"id": "api.command_custom_status.name",
"translation": "狀態"
},
{
"id": "api.command_custom_status.hint",
"translation": "[:emoji_name:] [status_message] 或清除"
},
{
"id": "api.command_custom_status.desc",
"translation": "設定或清除您的狀態"
},
{
"id": "api.command_custom_status.clear.success",
"translation": "狀態已清除。"
},
{
"id": "api.command_custom_status.clear.app_error",
"translation": "清除狀態錯誤。"
},
{
"id": "api.command_custom_status.app_error",
"translation": "設定狀態錯誤。"
},
{
"id": "api.command_channel_purpose.update_channel.max_length",
"translation": "輸入的文本超出字數限制。頻道用途不得超過 {{.MaxLength}} 個字數。"
},
{
"id": "api.command_channel_header.update_channel.max_length",
"translation": "輸入的文本超出字數限制。頻道標題不得超過 {{.MaxLength}} 個字數。"
},
{
"id": "api.cloud.delinquency_email.missing_email_to_trigger",
"translation": "缺少發送拖欠電子郵件的必要欄位。"
},
{
"id": "api.cloud.cws_webhook_event_missing_error",
"translation": "Webhook 事件未處理。事件可能遺失或無效。"
},
{
"id": "api.channel.create_channel.direct_channel.team_restricted_error",
"translation": "無法在這些使用者間創建直連頻道因為他們不屬於同一個團隊。"
},
{
"id": "api.admin.saml.failure_reset_authdata_to_email.app_error",
"translation": "無法寄出重設AuthData欄位失敗的電子郵件。"
}
]

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

@@ -8004,8 +8004,8 @@ func (c *Client4) ConfirmCustomerPayment(confirmRequest *ConfirmPaymentMethodReq
return BuildResponse(r), nil
}
func (c *Client4) RequestCloudTrial(email *StartCloudTrialRequest) (*Subscription, *Response, error) {
payload, err := json.Marshal(email)
func (c *Client4) RequestCloudTrial(cloudTrialRequest *StartCloudTrialRequest) (*Subscription, *Response, error) {
payload, err := json.Marshal(cloudTrialRequest)
if err != nil {
return nil, nil, NewAppError("RequestCloudTrial", "api.marshal_error", nil, "", http.StatusInternalServerError).Wrap(err)
}

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

@@ -110,11 +110,12 @@ type ValidateBusinessEmailResponse struct {
// CloudCustomerInfo represents editable info of a customer.
type CloudCustomerInfo struct {
Name string `json:"name"`
Email string `json:"email,omitempty"`
ContactFirstName string `json:"contact_first_name,omitempty"`
ContactLastName string `json:"contact_last_name,omitempty"`
NumEmployees int `json:"num_employees"`
Name string `json:"name"`
Email string `json:"email,omitempty"`
ContactFirstName string `json:"contact_first_name,omitempty"`
ContactLastName string `json:"contact_last_name,omitempty"`
NumEmployees int `json:"num_employees"`
MonthlySubscriptionIntentWireTransfer string `json:"monthly_subscription_intent_wire_transfer"`
}
// Address model represents a customer's address.

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

@@ -2747,6 +2747,16 @@ func (s *CloudSettings) SetDefaults() {
}
}
type ProductSettings struct {
EnablePublicSharedBoards *bool
}
func (s *ProductSettings) SetDefaults() {
if s.EnablePublicSharedBoards == nil {
s.EnablePublicSharedBoards = NewBool(false)
}
}
type PluginState struct {
Enable bool
}
@@ -3136,6 +3146,7 @@ type Config struct {
DataRetentionSettings DataRetentionSettings
MessageExportSettings MessageExportSettings
JobSettings JobSettings
ProductSettings ProductSettings
PluginSettings PluginSettings
DisplaySettings DisplaySettings
GuestAccountsSettings GuestAccountsSettings
@@ -3235,6 +3246,7 @@ func (o *Config) SetDefaults() {
o.ThemeSettings.SetDefaults()
o.ClusterSettings.SetDefaults()
o.PluginSettings.SetDefaults(o.LogSettings)
o.ProductSettings.SetDefaults()
o.AnalyticsSettings.SetDefaults()
o.ComplianceSettings.SetDefaults()
o.LocalizationSettings.SetDefaults()

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

@@ -76,6 +76,8 @@ type FeatureFlags struct {
SendWelcomePost bool
PostPriority bool
PeopleProduct bool
}
func (f *FeatureFlags) SetDefaults() {
@@ -104,6 +106,7 @@ func (f *FeatureFlags) SetDefaults() {
f.PlanUpgradeButtonText = "upgrade"
f.SendWelcomePost = true
f.PostPriority = false
f.PeopleProduct = false
}
func (f *FeatureFlags) Plugins() map[string]string {

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

@@ -4,6 +4,7 @@
package model
import (
"net/http"
"time"
)
@@ -260,6 +261,23 @@ func StartOfDayForTimeRange(timeRange string, location *time.Location) *time.Tim
return &resultTime
}
// GetStartOfDayForTimeRange gets the unix start time in milliseconds from the given time range.
// Time range can be one of: "today", "7_day", or "28_day".
func GetStartOfDayForTimeRange(timeRange string, location *time.Location) (*time.Time, *AppError) {
now := time.Now().In(location)
resultTime := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, location)
switch timeRange {
case TimeRangeToday:
case TimeRange7Day:
resultTime = resultTime.Add(time.Hour * time.Duration(-144))
case TimeRange28Day:
resultTime = resultTime.Add(time.Hour * time.Duration(-648))
default:
return nil, NewAppError("GetStartOfDayForTimeRange", "model.insights.get_start_of_day_for_time_range.time_range.app_error", nil, "", http.StatusBadRequest)
}
return &resultTime, nil
}
// GetTopReactionListWithPagination adds a rank to each item in the given list of TopReaction and checks if there is
// another page that can be fetched based on the given limit and offset. The given list of TopReaction is assumed to be
// sorted by Count. Returns a TopReactionList.

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

@@ -38,4 +38,5 @@ const (
MigrationKeyAddPlaybooksPermissions = "playbooks_permissions"
MigrationKeyAddCustomUserGroupsPermissions = "custom_groups_permissions"
MigrationKeyAddPlayboosksManageRolesPermissions = "playbooks_manage_roles"
MigrationKeyAddProductsBoardsPermissions = "products_boards"
)

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

@@ -354,6 +354,9 @@ var PermissionRunManageProperties *Permission
var PermissionRunManageMembers *Permission
var PermissionRunView *Permission
var PermissionSysconsoleReadProductsBoards *Permission
var PermissionSysconsoleWriteProductsBoards *Permission
// General permission that encompasses all system admin functions
// in the future this could be broken up to allow access to some
// admin functions but not others
@@ -2070,6 +2073,19 @@ func initializePermissions() {
PermissionScopeRun,
}
PermissionSysconsoleReadProductsBoards = &Permission{
"sysconsole_read_products_boards",
"",
"",
PermissionScopeSystem,
}
PermissionSysconsoleWriteProductsBoards = &Permission{
"sysconsole_write_products_boards",
"",
"",
PermissionScopeSystem,
}
SysconsoleReadPermissions = []*Permission{
PermissionSysconsoleReadAboutEditionAndLicense,
PermissionSysconsoleReadBilling,
@@ -2125,6 +2141,7 @@ func initializePermissions() {
PermissionSysconsoleReadExperimentalFeatures,
PermissionSysconsoleReadExperimentalFeatureFlags,
PermissionSysconsoleReadExperimentalBleve,
PermissionSysconsoleReadProductsBoards,
}
SysconsoleWritePermissions = []*Permission{
@@ -2182,6 +2199,7 @@ func initializePermissions() {
PermissionSysconsoleWriteExperimentalFeatures,
PermissionSysconsoleWriteExperimentalFeatureFlags,
PermissionSysconsoleWriteExperimentalBleve,
PermissionSysconsoleWriteProductsBoards,
}
SystemScopedPermissionsMinusSysconsole := []*Permission{

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

@@ -261,6 +261,7 @@ func init() {
PermissionSysconsoleReadExperimentalFeatures.Id,
PermissionSysconsoleReadExperimentalFeatureFlags.Id,
PermissionSysconsoleReadExperimentalBleve.Id,
PermissionSysconsoleReadProductsBoards.Id,
}
SystemManagerDefaultPermissions = []string{
@@ -339,6 +340,8 @@ func init() {
PermissionSysconsoleWriteIntegrationsBotAccounts.Id,
PermissionSysconsoleWriteIntegrationsGif.Id,
PermissionSysconsoleWriteIntegrationsCors.Id,
PermissionSysconsoleReadProductsBoards.Id,
PermissionSysconsoleWriteProductsBoards.Id,
}
SystemCustomGroupAdminDefaultPermissions = []string{

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

@@ -39,6 +39,7 @@ type PostService interface {
//
// The service shall be registered via app.PermissionKey service key.
type PermissionService interface {
HasPermissionTo(userID string, permission *model.Permission) bool
HasPermissionToTeam(userID, teamID string, permission *model.Permission) bool
HasPermissionToChannel(askingUserID string, channelID string, permission *model.Permission) bool
}

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

@@ -67,6 +67,7 @@ const (
TrackConfigBleve = "config_bleve"
TrackConfigExport = "config_export"
TrackFeatureFlags = "config_feature_flags"
TrackConfigProducts = "products"
TrackPermissionsGeneral = "permissions_general"
TrackPermissionsSystemScheme = "permissions_system_scheme"
TrackPermissionsTeamSchemes = "permissions_team_schemes"
@@ -827,6 +828,10 @@ func (ts *TelemetryService) trackConfig() {
"retention_days": *cfg.ExportSettings.RetentionDays,
})
ts.SendTelemetry(TrackConfigProducts, map[string]any{
"enable_public_shared_boards": *cfg.ProductSettings.EnablePublicSharedBoards,
})
// Convert feature flags to map[string]any for sending
flags := cfg.FeatureFlags.ToMap()
interfaceFlags := make(map[string]any)

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

@@ -37,7 +37,7 @@ func (s *SearchUserStore) deleteUserIndex(user *model.User) {
func (s *SearchUserStore) Search(teamId, term string, options *model.UserSearchOptions) ([]*model.User, error) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsSearchEnabled() {
listOfAllowedChannels, nErr := s.getListOfAllowedChannelsForTeam(teamId, options.ViewRestrictions)
listOfAllowedChannels, nErr := s.getListOfAllowedChannels(teamId, "", options.ViewRestrictions)
if nErr != nil {
mlog.Warn("Encountered error on Search.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
continue
@@ -148,7 +148,7 @@ func (s *SearchUserStore) autocompleteUsersInChannelByEngine(engine searchengine
return autocomplete, nil
}
// getListOfAllowedChannelsForTeam return the list of allowed channels to search user based on the
// getListOfAllowedChannels return the list of allowed channels to search user based on the
// next scenarios:
// - If there isn't view restrictions (team or channel) and no team id to filter them, then all
// channels are allowed (nil return)
@@ -159,7 +159,7 @@ func (s *SearchUserStore) autocompleteUsersInChannelByEngine(engine searchengine
// - If we receive channels restrictions we get:
// - If we don't have team id, we get those restricted channels (guest accounts and quick search)
// - If we have a team id then we only return those restricted channels that belongs to that team
func (s *SearchUserStore) getListOfAllowedChannelsForTeam(teamId string, viewRestrictions *model.ViewUsersRestrictions) ([]string, error) {
func (s *SearchUserStore) getListOfAllowedChannels(teamId, channelId string, viewRestrictions *model.ViewUsersRestrictions) ([]string, error) {
var listOfAllowedChannels []string
if viewRestrictions == nil && teamId == "" {
// nil return without error means all channels are allowed
@@ -174,6 +174,20 @@ func (s *SearchUserStore) getListOfAllowedChannelsForTeam(teamId string, viewRes
for _, channel := range channels {
listOfAllowedChannels = append(listOfAllowedChannels, channel.Id)
}
if channelId != "" {
ch, err := s.rootStore.Channel().Get(channelId, true)
if err != nil {
return nil, errors.Wrapf(err, "failed to get channel with id: %s", channelId)
}
// Check if DM/GM channel, and add to the list.
// This is because GetTeamChannels does not return DM/GM channels.
// And since the channelId is passed from the API layer, it is already
// auth checked to confirm that the user has permission.
if ch.IsGroupOrDirect() {
listOfAllowedChannels = append(listOfAllowedChannels, channelId)
}
}
return listOfAllowedChannels, nil
}
@@ -196,7 +210,7 @@ func (s *SearchUserStore) getListOfAllowedChannelsForTeam(teamId string, viewRes
func (s *SearchUserStore) AutocompleteUsersInChannel(teamId, channelId, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, error) {
for _, engine := range s.rootStore.searchEngine.GetActiveEngines() {
if engine.IsAutocompletionEnabled() {
listOfAllowedChannels, nErr := s.getListOfAllowedChannelsForTeam(teamId, options.ViewRestrictions)
listOfAllowedChannels, nErr := s.getListOfAllowedChannels(teamId, channelId, options.ViewRestrictions)
if nErr != nil {
mlog.Warn("Encountered error on AutocompleteUsersInChannel.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))
continue
@@ -205,6 +219,7 @@ func (s *SearchUserStore) AutocompleteUsersInChannel(teamId, channelId, term str
return &model.UserAutocompleteInChannel{}, nil
}
options.ListOfAllowedChannels = listOfAllowedChannels
autocomplete, nErr := s.autocompleteUsersInChannelByEngine(engine, teamId, channelId, term, options)
if nErr != nil {
mlog.Warn("Encountered error on AutocompleteUsersInChannel.", mlog.String("search_engine", engine.GetName()), mlog.Err(nErr))

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

@@ -742,6 +742,8 @@ func (us SqlUserStore) GetProfilesInChannel(options *model.UserGetOptions) ([]*m
query = query.Where("u.DeleteAt = 0")
}
query = applyMultiRoleFilters(query, options.Roles, options.TeamRoles, options.ChannelRoles, us.DriverName() == model.DatabaseDriverPostgres)
queryString, args, err := query.ToSql()
if err != nil {
return nil, errors.Wrap(err, "get_profiles_in_channel_tosql")
@@ -1803,7 +1805,16 @@ func (us SqlUserStore) GetUsersBatchForIndexing(startTime int64, startFileID str
`).
From("ChannelMembers cm").
Join("Channels c ON cm.ChannelId = c.Id").
Where(sq.Eq{"c.Type": model.ChannelTypeOpen, "cm.UserId": userIds}).
Where(sq.And{
sq.Eq{
"cm.UserId": userIds,
},
sq.Or{
sq.Eq{"c.Type": model.ChannelTypeOpen},
sq.Eq{"c.Type": model.ChannelTypeDirect},
sq.Eq{"c.Type": model.ChannelTypeGroup},
},
}).
ToSql()
if err != nil {
return nil, errors.Wrap(err, "GetUsersBatchForIndexing_ToSql2")

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

@@ -1003,6 +1003,36 @@ func testUserStoreGetProfilesInChannel(t *testing.T, ss store.Store) {
require.NoError(t, err)
assert.Equal(t, []*model.User{sanitized(u1)}, users)
})
t.Run("Filter by channel members and channel admins", func(t *testing.T) {
// save admin for c1
user2Admin, err := ss.User().Save(&model.User{
Email: MakeEmail(),
Username: "bbb" + model.NewId(),
})
require.NoError(t, err)
defer func() { require.NoError(t, ss.User().PermanentDelete(user2Admin.Id)) }()
_, nErr = ss.Team().SaveMember(&model.TeamMember{TeamId: teamId, UserId: user2Admin.Id}, -1)
require.NoError(t, nErr)
_, nErr = ss.Channel().SaveMember(&model.ChannelMember{
ChannelId: c1.Id,
UserId: user2Admin.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
ExplicitRoles: "channel_admin",
})
require.NoError(t, nErr)
ss.Channel().UpdateMembersRole(c1.Id, []string{user2Admin.Id})
users, err := ss.User().GetProfilesInChannel(&model.UserGetOptions{
InChannelId: c1.Id,
ChannelRoles: []string{model.ChannelAdminRoleId},
Page: 0,
PerPage: 5,
})
require.NoError(t, err)
assert.Equal(t, user2Admin.Id, users[0].Id)
})
}
func testUserStoreGetProfilesInChannelByAdmin(t *testing.T, ss store.Store, s SqlStore) {
@@ -4882,10 +4912,35 @@ func testUserStoreGetUsersBatchForIndexing(t *testing.T, ss store.Store) {
})
require.NoError(t, err)
cDM := &model.Channel{
Name: model.NewId() + "__" + model.NewId(),
Type: model.ChannelTypeDirect,
}
cm1 := &model.ChannelMember{
UserId: u3.Id,
ChannelId: cDM.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
cm2 := &model.ChannelMember{
UserId: u2.Id,
ChannelId: cDM.Id,
NotifyProps: model.GetDefaultChannelNotifyProps(),
}
cDM, nErr = ss.Channel().SaveDirectChannel(cDM, cm1, cm2)
require.NoError(t, nErr)
// Getting all users
res1List, err := ss.User().GetUsersBatchForIndexing(u1.CreateAt-1, "", 100)
require.NoError(t, err)
assert.Len(t, res1List, 3)
for _, user := range res1List {
switch user.Id {
case u2.Id:
assert.ElementsMatch(t, user.ChannelsIds, []string{cPub1.Id, cPub2.Id, cDM.Id})
case u3.Id:
assert.ElementsMatch(t, user.ChannelsIds, []string{cPub2.Id, cDM.Id})
}
}
// Testing pagination
res2List, err := ss.User().GetUsersBatchForIndexing(u1.CreateAt-1, "", 1)

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

@@ -8,7 +8,13 @@
<mj-include path="./partials/header.mjml" />
<mj-raw>{{if .Props.Message}}</mj-raw>
<mj-raw>{{range .Props.Posts}}<div class="postCard"></mj-raw>
<mj-include path="./partials/card.mjml" />
<mj-section css-class="messageCard" padding="0px">
<mj-group>
<mj-include path="./partials/message_avatar_col.mjml" />
<mj-include path="./partials/sender_info_col.mjml" />
<mj-include path="./partials/message_button.mjml" />
</mj-group>
</mj-section>
<mj-raw></div>{{end}}</mj-raw>
<mj-raw>{{else}}</mj-raw>
<mj-section padding="0px 0px 40px 0px">

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

@@ -356,7 +356,160 @@
max-width: 100% !important;
}
}
.messageAttachments * {
font-family: Open Sans, sans-serif !important;
}
.messageAttachmentContent {
padding: 0px;
border: 1px solid rgba(63, 67, 80, 0.16);
margin-bottom: 20px;
border-radius: 0 4px 4px 0;
}
.messageAttachmentContent>table,
.attachment__body {
font-family: Open Sans, sans-serif;
text-align: left;
font-size: 14px;
line-height: 20px;
color: #3F4350;
}
.messageAttachmentContent .attachment__author-icon {
width: 14px;
height: 14px;
margin-right: 5px;
border-radius: 50px;
vertical-align: middle;
}
.attachment__author-name {
opacity: 0.6;
}
.messageAttachmentContent p {
margin: 0;
}
.attachment__title {
padding: 0;
margin: 5px 0;
font-size: 14px;
font-weight: 600;
line-height: 18px;
}
.attachment__image {
max-height: 300px;
border: 1px solid transparent;
margin-bottom: 1em;
}
.attachment__thumb-image {
max-width: 100%;
max-height: 75px;
}
.attachment__thumb-container {
max-width: 80px;
width: max-content;
}
.attachment__wrapper {
width: 100%;
display: flex;
flex-direction: row;
gap: 12px;
}
.attachment__body {
flex: 1;
}
.messageAttachmentContent>table.attachment__footer-container {
color: #a3a3a3;
font-size: 12px;
}
.attachment__footer-icon {
width: 16px;
height: 16px;
}
.attachment__footer-container {
color: #a3a3a3;
font-size: 12px;
}
.messageAttachment_title {
padding-top: 1em;
font-weight: 600;
margin-bottom: 4px;
}
.pretext h1 {
font-size: 28px;
line-height: 32px;
}
.pretext h2 {
font-size: 25px;
line-height: 30px;
}
.pretext h3 {
font-size: 22px;
line-height: 25px;
}
.pretext h4 {
font-size: 19px;
line-height: 24px;
}
.pretext h5 {
font-size: 15px;
line-height: 20px;
}
.pretext h6 {
font-size: 1em;
line-height: 1.4em;
}
.pretext>* {
margin-bottom: 16px;
}
.messageAttachments {
margin-top: 22px;
}
.messageAttachments h1,
h2,
h3,
h4,
h5,
h6 {
font-weight: 500;
}
code {
padding: 2px 4px;
font-size: 90%;
background-color: rgba(63, 67, 80, 0.1);
border-radius: 4px;
}
.messageAttachments a {
background-color: unset !important;
border: none !important;
padding: unset !important;
}
</style>
<!-- this empty mj attribute tag is required by MJML to compile successfully -->
</head>
<body style="word-spacing:normal;">
@@ -507,7 +660,96 @@
</tbody>
</table>
</div>
<!--[if mso | IE]></td><td style="vertical-align:top;width:552px;" ><![endif]-->
<!--[if mso | IE]></td><![endif]-->
{{if .MessageAttachments}}
<div class="messageAttachments" style="margin-top: 22px;">
{{range .MessageAttachments}}
<div class="messageAttachment" style="vertical-align: top; font-family: Open Sans, sans-serif;" width="100%">
<div class="pretext" style="text-align: left; font-size: 14px; line-height: 20px; color: #3F4350; padding: 0px; font-family: Open Sans, sans-serif;">
{{.Pretext}}
</div>
<div class="messageAttachmentContent" style="border: 1px solid rgba(63, 67, 80, 0.16); margin-bottom: 20px; border-radius: 0 4px 4px 0; border-left: 4px solid {{.Color}}; padding: 12px; font-family: Open Sans, sans-serif;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="text-align: left; line-height: 20px; color: #3F4350; vertical-align: top; font-size: 14px; font-family: Open Sans, sans-serif;" width="100%" valign="top" align="left">
<tbody style="font-family: Open Sans, sans-serif;">
{{if or .AuthorIcon .AuthorName}}
<tr style="font-family: Open Sans, sans-serif;">
<td style="font-family: Open Sans, sans-serif;">
{{if .AuthorLink}}<a href="{{.AuthorLink}}" style="color: #1C58D9; font-family: Open Sans, sans-serif; text-decoration: none; background-color: unset; border: none; padding: unset;">{{end}}
{{if .AuthorIcon}}<img class="attachment__author-icon" alt="attachment author icon" src="{{.AuthorIcon}}" style="width: 14px; height: 14px; margin-right: 5px; border-radius: 50px; vertical-align: middle; font-family: Open Sans, sans-serif;" width="14" height="14">{{end}}
{{if .AuthorName}}<span class="attachment__author-name" style="opacity: 0.6; font-family: Open Sans, sans-serif;">{{.AuthorName}}</span>{{end}}
{{if .AuthorLink}}</a>{{end}}
</td>
</tr>
{{end}}
{{if .Title}}
<tr style="font-family: Open Sans, sans-serif;">
<td style="font-family: Open Sans, sans-serif;">
<h1 class="attachment__title" style="padding: 0; margin: 5px 0; font-size: 14px; line-height: 18px; font-weight: 500; font-family: Open Sans, sans-serif;">
{{if .TitleLink}}<a href="{{.TitleLink}}" style="color: #1C58D9; font-family: Open Sans, sans-serif; text-decoration: none; background-color: unset; border: none; padding: unset;">{{end}}
{{.Title}}
{{if .Title}}</a>{{end}}
</h1>
</td>
</tr>
{{end}}
</tbody>
</table>
<div class="attachment__wrapper" style="width: 100%; display: flex; flex-direction: row; gap: 12px; font-family: Open Sans, sans-serif;">
<div class="attachment__body" style="text-align: left; font-size: 14px; line-height: 20px; color: #3F4350; flex: 1; font-family: Open Sans, sans-serif;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align: top; font-size: 14px; color: #3F4350; font-family: Open Sans, sans-serif;" width="100%" valign="top">
<tbody style="font-family: Open Sans, sans-serif;">
<tr style="font-family: Open Sans, sans-serif;">
<td vertical-align="middle" class="messageButton" style="font-family: Open Sans, sans-serif;">
<div style="font-family: Open Sans, sans-serif;">{{.Text}}</div>
</td>
</tr>
{{if .ImageURL}}
<tr style="font-family: Open Sans, sans-serif;">
<td style="font-family: Open Sans, sans-serif;">
<img class="attachment__image" src="{{.ImageURL}}" style="max-height: 300px; border: 1px solid transparent; margin-bottom: 1em; font-family: Open Sans, sans-serif;">
</td>
</tr>
{{end}}
</tbody>
</table>
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align: top; font-size: 14px; table-layout: fixed; margin-bottom: 0.7em; font-family: Open Sans, sans-serif;" width="100%" valign="top">
{{range .FieldRows}}
<tr class="messageAttachment_row" style="font-family: Open Sans, sans-serif;">
{{ $length := len .Cells }}
{{range .Cells}}
<td class="messageAttachment_field" {{ if eq $length 1 }}colspan="2" {{end}} style="font-family: Open Sans, sans-serif;">
<div class="messageAttachment_title" style="padding-top: 1em; font-weight: 600; margin-bottom: 4px; font-family: Open Sans, sans-serif;">{{.Title}}</div>
<div style="font-family: Open Sans, sans-serif;">{{.Value}}</div>
</td>
{{end}}
</tr>
{{end}}
</table>
</div>
{{if .ThumbURL}}
<div class="attachment__thumb-container" style="max-width: 80px; width: max-content; font-family: Open Sans, sans-serif;">
<img class="attachment__thumb-image" src="{{.ThumbURL}}" style="max-width: 100%; max-height: 75px; font-family: Open Sans, sans-serif;">
</div>
{{end}}
</div>
{{if .Footer}}
<table class="attachment__footer-container" border="0" cellpadding="0" cellspacing="0" role="presentation" style="text-align: left; line-height: 20px; color: #a3a3a3; vertical-align: top; font-size: 14px; font-family: Open Sans, sans-serif;" width="100%" valign="top" align="left">
<tbody style="font-family: Open Sans, sans-serif;">
<tr style="font-family: Open Sans, sans-serif;">
<td style="font-family: Open Sans, sans-serif;">
{{if .FooterIcon}}<img class="attachment__footer-icon" src="{{.FooterIcon}}" style="width: 16px; height: 16px; vertical-align: middle; font-family: Open Sans, sans-serif;" width="16" height="16">{{end}}
<span style="font-size: 12px; font-family: Open Sans, sans-serif;">{{.Footer}}</span>
</td>
</tr>
</tbody>
</table>
{{end}}
</div>
</div>
{{end}}
</div>
{{end}}
<!--[if mso | IE]><td style="vertical-align:top;width:552px;" ><![endif]-->
<div class="mj-column-per-100 mj-outlook-group-fix" style="font-size:0px;text-align:left;direction:ltr;display:inline-block;vertical-align:top;width:100%;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top;" width="100%">
<tbody>

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

@@ -1,13 +1,21 @@
<mjml>
<mj-head>
<mj-include path="./partials/style.mjml" />
<mj-include path="partials/message_attachment_styles.mjml" />
</mj-head>
<mj-body css-class="emailBody">
<mj-wrapper mj-class="email">
<mj-include path="./partials/logo.mjml" />
<mj-include path="./partials/header.mjml" />
<mj-raw>{{range .Props.Posts}}<div class="postCard"></mj-raw>
<mj-include path="./partials/card.mjml" />
<mj-section css-class="messageCard" padding="0px">
<mj-group>
<mj-include path="./partials/message_avatar_col.mjml" />
<mj-include path="./partials/sender_info_col.mjml" />
<mj-include path="partials/message_attachment.html" type="html" />
<mj-include path="./partials/message_button.mjml" />
</mj-group>
</mj-section>
<mj-raw></div>{{end}}</mj-raw>
<mj-section padding="16px 0px 40px 0px">
<mj-column>

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

@@ -1,42 +0,0 @@
<mj-section css-class="messageCard" padding="0px">
<mj-group>
<mj-column css-class="messageAvatarCol">
<mj-image css-class="messageAvatar" src="cid:{{.SenderPhoto}}" padding="0px" />
</mj-column>
<mj-column css-class="senderInfoCol" width=90% >
<mj-raw>
<tr>
<td>
<div class="postNameAndTime">
<div class="senderName">{{.SenderName}}</div>
{{if .Time}}
<div class="time">{{.Time}}</div>
{{end}}
{{if .ChannelName}}
<div class="channelBg">
{{if .ShowChannelIcon}}
<div class="channelLogo"><img src="{{$.Props.SiteURL}}/static/images/channel_icon.png" width=10px height=10px></img></div>
{{end}}
<div class="channelName">
{{if .OtherChannelMembersCount}}
<span class="gmChannelCount">{{.OtherChannelMembersCount}}</span>
{{end}}
{{.ChannelName}}
</div>
</div>
{{end}}
</div>
</td>
</tr>
</mj-raw>
<mj-text css-class="senderMessage" padding="0px">
{{.Message}}
</mj-text>
</mj-column>
<mj-column width=100%>
<mj-raw>{{if .MessageURL}}</mj-raw>
<mj-button href="{{.MessageURL}}" padding="16px 0px 0px 0px" css-class="messageButton">{{$.Props.MessageButton}}</mj-button>
<mj-raw>{{end}}</mj-raw>
</mj-column>
</mj-group>
</mj-section>

146
templates/partials/message_attachment.css Обычный файл
Просмотреть файл

@@ -0,0 +1,146 @@
.messageAttachments * {
font-family: Open Sans, sans-serif !important;
}
.messageAttachmentContent {
padding: 0px;
border: 1px solid rgba(63, 67, 80, 0.16);
margin-bottom: 20px;
border-radius: 0 4px 4px 0;
}
.messageAttachmentContent > table, .attachment__body {
font-family: Open Sans, sans-serif;
text-align: left;
font-size: 14px;
line-height: 20px;
color: #3F4350;
}
.messageAttachmentContent .attachment__author-icon {
width: 14px;
height: 14px;
margin-right: 5px;
border-radius: 50px;
vertical-align: middle;
}
.attachment__author-name {
opacity: 0.6;
}
.messageAttachmentContent p {
margin: 0;
}
.attachment__title {
padding: 0;
margin: 5px 0;
font-size: 14px;
font-weight: 600;
line-height: 18px;
}
.attachment__image {
max-height: 300px;
border: 1px solid transparent;
margin-bottom: 1em;
}
.attachment__thumb-image {
max-width: 100%;
max-height: 75px;
}
.attachment__thumb-container {
max-width: 80px;
width: max-content;
}
.attachment__wrapper {
width: 100%;
display: flex;
flex-direction: row;
gap: 12px;
}
.attachment__body {
flex: 1;
}
.messageAttachmentContent > table.attachment__footer-container {
color: #a3a3a3;
font-size: 12px;
}
.attachment__footer-icon {
width: 16px;
height: 16px;
}
.attachment__footer-container {
color: #a3a3a3;
font-size: 12px;
}
.messageAttachment_title {
padding-top: 1em;
font-weight: 600;
margin-bottom: 4px;
}
.pretext h1 {
font-size: 28px;
line-height: 32px;
}
.pretext h2 {
font-size: 25px;
line-height: 30px;
}
.pretext h3 {
font-size: 22px;
line-height: 25px;
}
.pretext h4 {
font-size: 19px;
line-height: 24px;
}
.pretext h5 {
font-size: 15px;
line-height: 20px;
}
.pretext h6 {
font-size: 1em;
line-height: 1.4em;
}
.pretext > * {
margin-bottom: 16px;
}
.messageAttachments {
margin-top: 22px;
}
.messageAttachments h1, h2, h3, h4, h5, h6 {
font-weight: 500;
}
code {
padding: 2px 4px;
font-size: 90%;
background-color: rgba(63, 67, 80, 0.1);
border-radius: 4px;
}
.messageAttachments a {
background-color: unset !important;
border: none !important;
padding: unset !important;
}

95
templates/partials/message_attachment.html Обычный файл
Просмотреть файл

@@ -0,0 +1,95 @@
{{if .MessageAttachments}}
<div class="messageAttachments">
{{range .MessageAttachments}}
<div class="messageAttachment" style="vertical-align:top;" width="100%">
<div class="pretext" style="font-family: Open Sans, sans-serif; text-align: left; font-size: 14px; line-height: 20px; color: #3F4350; padding: 0px;">
{{.Pretext}}
</div>
<div class="messageAttachmentContent" style="border-left: 4px solid {{.Color}}; padding: 12px;">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top; font-size: 14px" width="100%">
<tbody>
{{if or .AuthorIcon .AuthorName}}
<tr>
<td>
{{if .AuthorLink}}<a href="{{.AuthorLink}}">{{end}}
{{if .AuthorIcon}}<img class="attachment__author-icon" alt="attachment author icon" src="{{.AuthorIcon}}"/>{{end}}
{{if .AuthorName}}<span class="attachment__author-name">{{.AuthorName}}</span>{{end}}
{{if .AuthorLink}}</a>{{end}}
</td>
</tr>
{{end}}
{{if .Title}}
<tr>
<td>
<h1 class="attachment__title">
{{if .TitleLink}}<a href="{{.TitleLink}}">{{end}}
{{.Title}}
{{if .Title}}</a>{{end}}
</h1>
</td>
</tr>
{{end}}
</tbody>
</table>
<div class="attachment__wrapper">
<div class="attachment__body">
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top; font-size: 14px; color: #3F4350;" width="100%">
<tbody>
<tr>
<td vertical-align="middle" class="messageButton">
<div>{{.Text}}</div>
</td>
</tr>
{{if .ImageURL}}
<tr>
<td>
<img class="attachment__image" src="{{.ImageURL}}"/>
</td>
</tr>
{{end}}
</table>
<table border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top; font-size: 14px; table-layout: fixed; margin-bottom: 0.7em;" width="100%">
{{range .FieldRows}}
<tr class="messageAttachment_row">
{{ $length := len .Cells }}
{{range .Cells}}
<td
class="messageAttachment_field"
{{ if eq $length 1 }}colspan="2"{{end}}
>
<div class="messageAttachment_title">{{.Title}}</div>
<div>{{.Value}}</div>
</td>
{{end}}
</tr>
{{end}}
</table>
</div>
{{if .ThumbURL}}
<div class="attachment__thumb-container">
<img class="attachment__thumb-image" src="{{.ThumbURL}}"/>
</div>
{{end}}
</div>
{{if .Footer}}
<table class="attachment__footer-container" border="0" cellpadding="0" cellspacing="0" role="presentation" style="vertical-align:top; font-size: 14px" width="100%">
<tbody>
<tr>
<td>
{{if .FooterIcon}}<img class="attachment__footer-icon" src="{{.FooterIcon}}" style="vertical-align: middle;"/>{{end}}
<span style="font-size: 12px;">{{.Footer}}</span>
</td>
</tr>
</tbody>
</table>
{{end}}
</div>
</div>
{{end}}
</div>
{{end}}

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

@@ -0,0 +1,5 @@
<!-- this empty mj attribute tag is required by MJML to compile successfully -->
<mj-attributes/>
<mj-include type="css" path="message_attachment.css"/>
<mj-include type="css" css-inline="inline" path="message_attachment.css"/>

3
templates/partials/message_avatar_col.mjml Обычный файл
Просмотреть файл

@@ -0,0 +1,3 @@
<mj-column css-class="messageAvatarCol">
<mj-image css-class="messageAvatar" src="cid:{{.SenderPhoto}}" padding="0px" />
</mj-column>

5
templates/partials/message_button.mjml Обычный файл
Просмотреть файл

@@ -0,0 +1,5 @@
<mj-column width=100%>
<mj-raw>{{if .MessageURL}}</mj-raw>
<mj-button href="{{.MessageURL}}" padding="16px 0px 0px 0px" css-class="messageButton">{{$.Props.MessageButton}}</mj-button>
<mj-raw>{{end}}</mj-raw>
</mj-column>

30
templates/partials/sender_info_col.mjml Обычный файл
Просмотреть файл

@@ -0,0 +1,30 @@
<mj-column css-class="senderInfoCol" width=90% >
<mj-raw>
<tr>
<td>
<div class="postNameAndTime">
<div class="senderName">{{.SenderName}}</div>
{{if .Time}}
<div class="time">{{.Time}}</div>
{{end}}
{{if .ChannelName}}
<div class="channelBg">
{{if .ShowChannelIcon}}
<div class="channelLogo"><img src="{{$.Props.SiteURL}}/static/images/channel_icon.png" width=10px height=10px></img></div>
{{end}}
<div class="channelName">
{{if .OtherChannelMembersCount}}
<span class="gmChannelCount">{{.OtherChannelMembersCount}}</span>
{{end}}
{{.ChannelName}}
</div>
</div>
{{end}}
</div>
</td>
</tr>
</mj-raw>
<mj-text css-class="senderMessage" padding="0px">
{{.Message}}
</mj-text>
</mj-column>

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

@@ -69,6 +69,7 @@ func GetMockStoreForSetupFunctions() *mocks.Store {
systemStore.On("GetByName", model.MigrationKeyAddCustomUserGroupsPermissions).Return(&model.System{Name: model.MigrationKeyAddCustomUserGroupsPermissions, Value: "true"}, nil)
systemStore.On("GetByName", model.MigrationKeyAddPlayboosksManageRolesPermissions).Return(&model.System{Name: model.MigrationKeyAddPlayboosksManageRolesPermissions, Value: "true"}, nil)
systemStore.On("GetByName", "CustomGroupAdminRoleCreationMigrationComplete").Return(&model.System{Name: model.MigrationKeyAddPlayboosksManageRolesPermissions, Value: "true"}, nil)
systemStore.On("GetByName", "products_boards").Return(&model.System{Name: "products_boards", Value: "true"}, nil)
systemStore.On("InsertIfExists", mock.AnythingOfType("*model.System")).Return(&model.System{}, nil).Once()
systemStore.On("Save", mock.AnythingOfType("*model.System")).Return(nil)