summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Makefile33
-rw-r--r--api/admin.go575
-rw-r--r--api/admin_test.go25
-rw-r--r--api/api.go19
-rw-r--r--api/apitestlib.go74
-rw-r--r--api/authentication.go5
-rw-r--r--api/authorization.go187
-rw-r--r--api/auto_users.go9
-rw-r--r--api/channel.go1140
-rw-r--r--api/channel_test.go94
-rw-r--r--api/cli_test.go7
-rw-r--r--api/command.go55
-rw-r--r--api/command_away.go3
-rw-r--r--api/command_echo.go3
-rw-r--r--api/command_expand_collapse.go5
-rw-r--r--api/command_invite_people.go21
-rw-r--r--api/command_join.go5
-rw-r--r--api/command_loadtest.go13
-rw-r--r--api/command_logout.go4
-rw-r--r--api/command_msg.go11
-rw-r--r--api/command_offline.go3
-rw-r--r--api/command_online.go3
-rw-r--r--api/context.go164
-rw-r--r--api/context_test.go23
-rw-r--r--api/deprecated.go26
-rw-r--r--api/emoji.go27
-rw-r--r--api/emoji_test.go13
-rw-r--r--api/file.go544
-rw-r--r--api/file_test.go76
-rw-r--r--api/general.go3
-rw-r--r--api/license.go16
-rw-r--r--api/oauth.go180
-rw-r--r--api/post.go1473
-rw-r--r--api/post_test.go612
-rw-r--r--api/preference.go11
-rw-r--r--api/reaction.go28
-rw-r--r--api/status.go257
-rw-r--r--api/status_test.go11
-rw-r--r--api/team.go699
-rw-r--r--api/team_test.go75
-rw-r--r--api/user.go1712
-rw-r--r--api/user_test.go300
-rw-r--r--api/webhook.go71
-rw-r--r--api/webrtc.go41
-rw-r--r--api/webrtc_test.go3
-rw-r--r--api/websocket.go22
-rw-r--r--api/websocket_handler.go20
-rw-r--r--api/websocket_test.go5
-rw-r--r--app/admin.go191
-rw-r--r--app/analytics.go239
-rw-r--r--app/app.go16
-rw-r--r--app/apptestlib.go192
-rw-r--r--app/audit.go16
-rw-r--r--app/authorization.go197
-rw-r--r--app/authorization_test.go (renamed from api/authorization_test.go)2
-rw-r--r--app/brand.go49
-rw-r--r--app/channel.go766
-rw-r--r--app/command.go31
-rw-r--r--app/compliance.go62
-rw-r--r--app/email.go236
-rw-r--r--app/email_batching.go (renamed from api/email_batching.go)4
-rw-r--r--app/email_batching_test.go (renamed from api/email_batching_test.go)2
-rw-r--r--app/email_test.go419
-rw-r--r--app/file.go529
-rw-r--r--app/file_test.go33
-rw-r--r--app/import.go (renamed from api/import.go)4
-rw-r--r--app/ldap.go40
-rw-r--r--app/notification.go721
-rw-r--r--app/notification_test.go313
-rw-r--r--app/oauth.go34
-rw-r--r--app/post.go501
-rw-r--r--app/preference.go16
-rw-r--r--app/saml.go67
-rw-r--r--app/server.go (renamed from api/server.go)52
-rw-r--r--app/session.go180
-rw-r--r--app/session_test.go31
-rw-r--r--app/slackimport.go (renamed from api/slackimport.go)72
-rw-r--r--app/slackimport_test.go (renamed from api/slackimport_test.go)2
-rw-r--r--app/status.go255
-rw-r--r--app/team.go563
-rw-r--r--app/user.go982
-rw-r--r--app/user_test.go53
-rw-r--r--app/web_conn.go (renamed from api/web_conn.go)42
-rw-r--r--app/web_hub.go (renamed from api/web_hub.go)4
-rw-r--r--app/webhook.go189
-rw-r--r--app/websocket_router.go (renamed from api/websocket_router.go)30
-rw-r--r--app/webtrc.go33
-rw-r--r--cmd/platform/channel.go18
-rw-r--r--cmd/platform/channelargs.go6
-rw-r--r--cmd/platform/import.go4
-rw-r--r--cmd/platform/init.go5
-rw-r--r--cmd/platform/mattermost.go4
-rw-r--r--cmd/platform/oldcommands.go107
-rw-r--r--cmd/platform/roles.go6
-rw-r--r--cmd/platform/server.go37
-rw-r--r--cmd/platform/team.go14
-rw-r--r--cmd/platform/teamargs.go6
-rw-r--r--cmd/platform/test.go21
-rw-r--r--cmd/platform/user.go20
-rw-r--r--cmd/platform/userargs.go8
-rw-r--r--cmd/platform/version.go4
-rw-r--r--config/config.json13
-rw-r--r--einterfaces/cluster.go2
-rw-r--r--glide.lock10
-rw-r--r--glide.yaml4
-rw-r--r--i18n/de.json114
-rw-r--r--i18n/en.json142
-rw-r--r--i18n/es.json180
-rw-r--r--i18n/fr.json154
-rw-r--r--i18n/ja.json130
-rw-r--r--i18n/ko.json108
-rw-r--r--i18n/nl.json108
-rw-r--r--i18n/pt-BR.json146
-rw-r--r--i18n/ru.json170
-rw-r--r--i18n/zh_CN.json114
-rw-r--r--i18n/zh_TW.json106
-rw-r--r--manualtesting/manual_testing.go22
-rw-r--r--model/authorization.go41
-rw-r--r--model/client.go38
-rw-r--r--model/config.go26
-rw-r--r--model/file.go4
-rw-r--r--model/post.go4
-rw-r--r--model/team.go8
-rw-r--r--model/user.go7
-rw-r--r--model/websocket_client.go4
-rw-r--r--store/sql_channel_store.go31
-rw-r--r--store/sql_channel_store_test.go43
-rw-r--r--store/sql_post_store.go21
-rw-r--r--store/sql_post_store_test.go23
-rw-r--r--store/sql_preference_store.go22
-rw-r--r--store/sql_preference_store_test.go45
-rw-r--r--store/sql_team_store.go4
-rw-r--r--store/sql_team_store_test.go2
-rw-r--r--store/sql_upgrade.go10
-rw-r--r--store/sql_user_store.go25
-rw-r--r--store/store.go4
-rw-r--r--tests/test-markdown-lists.md29
-rw-r--r--utils/authorization.go33
-rw-r--r--utils/config.go13
-rw-r--r--utils/diagnostic.go3
-rw-r--r--utils/inbucket.go115
-rw-r--r--utils/mail_test.go61
-rw-r--r--utils/urlencode_test.go35
-rw-r--r--utils/utils.go18
-rw-r--r--vendor/github.com/dyatlov/go-opengraph/.gitignore24
-rw-r--r--vendor/github.com/dyatlov/go-opengraph/LICENSE22
-rw-r--r--vendor/github.com/dyatlov/go-opengraph/README.md118
-rw-r--r--vendor/github.com/dyatlov/go-opengraph/examples/advanced.go58
-rw-r--r--vendor/github.com/dyatlov/go-opengraph/examples/simple.go27
-rw-r--r--vendor/github.com/dyatlov/go-opengraph/opengraph/opengraph.go329
-rw-r--r--vendor/github.com/dyatlov/go-opengraph/opengraph/opengraph_test.go131
-rw-r--r--web/web.go3
-rw-r--r--web/web_test.go60
-rw-r--r--webapp/actions/admin_actions.jsx404
-rw-r--r--webapp/actions/channel_actions.jsx172
-rw-r--r--webapp/actions/file_actions.jsx26
-rw-r--r--webapp/actions/global_actions.jsx26
-rw-r--r--webapp/actions/post_actions.jsx89
-rw-r--r--webapp/actions/team_actions.jsx53
-rw-r--r--webapp/actions/user_actions.jsx326
-rw-r--r--webapp/actions/webrtc_actions.jsx16
-rw-r--r--webapp/client/client.jsx35
-rw-r--r--webapp/components/activity_log_modal.jsx20
-rw-r--r--webapp/components/admin_console/admin_settings.jsx5
-rw-r--r--webapp/components/admin_console/admin_sidebar_header.jsx2
-rw-r--r--webapp/components/admin_console/admin_team_members_dropdown.jsx18
-rw-r--r--webapp/components/admin_console/brand_image_setting.jsx3
-rw-r--r--webapp/components/admin_console/cluster_table_container.jsx10
-rw-r--r--webapp/components/admin_console/compliance_reports.jsx3
-rw-r--r--webapp/components/admin_console/email_connection_test.jsx5
-rw-r--r--webapp/components/admin_console/ldap_test_button.jsx5
-rw-r--r--webapp/components/admin_console/license_settings.jsx8
-rw-r--r--webapp/components/admin_console/logs.jsx3
-rw-r--r--webapp/components/admin_console/policy_settings.jsx49
-rw-r--r--webapp/components/admin_console/post_edit_setting.jsx99
-rw-r--r--webapp/components/admin_console/purge_caches.jsx10
-rw-r--r--webapp/components/admin_console/radio_setting.jsx63
-rw-r--r--webapp/components/admin_console/recycle_db.jsx5
-rw-r--r--webapp/components/admin_console/reload_config.jsx8
-rw-r--r--webapp/components/admin_console/reset_password_modal.jsx5
-rw-r--r--webapp/components/admin_console/saml_settings.jsx9
-rw-r--r--webapp/components/admin_console/sync_now_button.jsx5
-rw-r--r--webapp/components/admin_console/team_users.jsx8
-rw-r--r--webapp/components/analytics/system_analytics.jsx34
-rw-r--r--webapp/components/authorize.jsx7
-rw-r--r--webapp/components/channel_header.jsx21
-rw-r--r--webapp/components/channel_invite_modal.jsx8
-rw-r--r--webapp/components/channel_members_dropdown.jsx246
-rw-r--r--webapp/components/channel_members_modal.jsx153
-rw-r--r--webapp/components/channel_select.jsx14
-rw-r--r--webapp/components/channel_switch_modal.jsx13
-rw-r--r--webapp/components/claim/components/email_to_ldap.jsx4
-rw-r--r--webapp/components/claim/components/email_to_oauth.jsx4
-rw-r--r--webapp/components/claim/components/oauth_to_email.jsx12
-rw-r--r--webapp/components/create_comment.jsx7
-rw-r--r--webapp/components/create_post.jsx3
-rw-r--r--webapp/components/delete_channel_modal.jsx14
-rw-r--r--webapp/components/delete_post_modal.jsx24
-rw-r--r--webapp/components/do_verify_email.jsx15
-rw-r--r--webapp/components/edit_channel_header_modal.jsx12
-rw-r--r--webapp/components/edit_channel_purpose_modal.jsx7
-rw-r--r--webapp/components/edit_post_modal.jsx27
-rw-r--r--webapp/components/file_attachment_list.jsx2
-rw-r--r--webapp/components/file_preview.jsx5
-rw-r--r--webapp/components/file_upload.jsx40
-rw-r--r--webapp/components/integrations/components/installed_oauth_app.jsx4
-rw-r--r--webapp/components/invite_member_modal.jsx4
-rw-r--r--webapp/components/logged_in.jsx25
-rw-r--r--webapp/components/login/login_controller.jsx77
-rw-r--r--webapp/components/member_list_channel.jsx179
-rw-r--r--webapp/components/member_list_team.jsx16
-rw-r--r--webapp/components/mfa/mfa_controller.jsx23
-rw-r--r--webapp/components/more_channels.jsx9
-rw-r--r--webapp/components/navbar.jsx22
-rw-r--r--webapp/components/new_channel_modal.jsx4
-rw-r--r--webapp/components/password_reset_form.jsx7
-rw-r--r--webapp/components/popover_list_members.jsx94
-rw-r--r--webapp/components/post_view/components/post.jsx16
-rw-r--r--webapp/components/post_view/components/post_attachment_oembed.jsx108
-rw-r--r--webapp/components/post_view/components/post_attachment_opengraph.jsx212
-rw-r--r--webapp/components/post_view/components/post_body.jsx4
-rw-r--r--webapp/components/post_view/components/post_body_additional_content.jsx93
-rw-r--r--webapp/components/post_view/components/post_image.jsx6
-rw-r--r--webapp/components/post_view/components/post_info.jsx40
-rw-r--r--webapp/components/post_view/components/post_list.jsx23
-rw-r--r--webapp/components/post_view/components/post_message_container.jsx2
-rw-r--r--webapp/components/post_view/components/post_message_view.jsx38
-rw-r--r--webapp/components/post_view/components/post_time.jsx5
-rw-r--r--webapp/components/post_view/components/providers.json376
-rw-r--r--webapp/components/post_view/post_view_controller.jsx10
-rw-r--r--webapp/components/profile_popover.jsx35
-rw-r--r--webapp/components/rhs_comment.jsx28
-rw-r--r--webapp/components/rhs_root_post.jsx28
-rw-r--r--webapp/components/root.jsx27
-rw-r--r--webapp/components/search_bar.jsx31
-rw-r--r--webapp/components/search_results.jsx6
-rw-r--r--webapp/components/search_results_item.jsx4
-rw-r--r--webapp/components/setting_item_max.jsx4
-rw-r--r--webapp/components/setting_picture.jsx4
-rw-r--r--webapp/components/should_verify_email.jsx5
-rw-r--r--webapp/components/sidebar_header.jsx13
-rw-r--r--webapp/components/sidebar_header_dropdown.jsx5
-rw-r--r--webapp/components/signup/components/signup_email.jsx29
-rw-r--r--webapp/components/signup/components/signup_ldap.jsx19
-rw-r--r--webapp/components/signup/signup_controller.jsx14
-rw-r--r--webapp/components/suggestion/at_mention_provider.jsx5
-rw-r--r--webapp/components/suggestion/channel_mention_provider.jsx114
-rw-r--r--webapp/components/suggestion/emoticon_provider.jsx23
-rw-r--r--webapp/components/suggestion/search_user_provider.jsx2
-rw-r--r--webapp/components/suggestion/suggestion_box.jsx6
-rw-r--r--webapp/components/suggestion/switch_channel_provider.jsx2
-rw-r--r--webapp/components/team_general_tab.jsx108
-rw-r--r--webapp/components/user_list_row.jsx2
-rw-r--r--webapp/components/user_profile.jsx2
-rw-r--r--webapp/components/user_settings/user_settings_advanced.jsx2
-rw-r--r--webapp/components/user_settings/user_settings_display.jsx10
-rw-r--r--webapp/components/user_settings/user_settings_general.jsx6
-rw-r--r--webapp/components/user_settings/user_settings_notifications.jsx7
-rw-r--r--webapp/components/user_settings/user_settings_security.jsx62
-rw-r--r--webapp/components/webrtc/components/webrtc_notification.jsx2
-rw-r--r--webapp/components/webrtc/webrtc_controller.jsx8
-rw-r--r--webapp/i18n/de.json67
-rw-r--r--webapp/i18n/en.json83
-rw-r--r--webapp/i18n/es.json89
-rw-r--r--webapp/i18n/fr.json403
-rw-r--r--webapp/i18n/ja.json71
-rw-r--r--webapp/i18n/ko.json59
-rw-r--r--webapp/i18n/nl.json61
-rw-r--r--webapp/i18n/pt-BR.json95
-rw-r--r--webapp/i18n/ru.json83
-rw-r--r--webapp/i18n/zh_CN.json79
-rw-r--r--webapp/i18n/zh_TW.json57
-rw-r--r--webapp/package.json2
-rw-r--r--webapp/root.html45
-rw-r--r--webapp/sass/base/_typography.scss5
-rw-r--r--webapp/sass/layout/_post.scss48
-rw-r--r--webapp/sass/layout/_webhooks.scss20
-rw-r--r--webapp/sass/responsive/_mobile.scss5
-rw-r--r--webapp/sass/responsive/_tablet.scss2
-rw-r--r--webapp/stores/channel_store.jsx56
-rw-r--r--webapp/stores/notification_store.jsx2
-rw-r--r--webapp/stores/opengraph_store.jsx68
-rw-r--r--webapp/stores/post_store.jsx34
-rw-r--r--webapp/stores/team_store.jsx2
-rw-r--r--webapp/stores/user_store.jsx2
-rw-r--r--webapp/tests/client_team.test.jsx25
-rw-r--r--webapp/tests/utils_get_nearest_point.test.jsx35
-rw-r--r--webapp/utils/async_client.jsx8
-rw-r--r--webapp/utils/channel_intro_messages.jsx2
-rw-r--r--webapp/utils/channel_utils.jsx10
-rw-r--r--webapp/utils/commons.jsx36
-rw-r--r--webapp/utils/constants.jsx73
-rw-r--r--webapp/utils/markdown.jsx37
-rw-r--r--webapp/utils/post_utils.jsx45
-rw-r--r--webapp/utils/syntax_highlighting.jsx22
-rw-r--r--webapp/utils/text_formatting.jsx13
-rw-r--r--webapp/utils/utils.jsx20
297 files changed, 15668 insertions, 9366 deletions
diff --git a/Makefile b/Makefile
index 173f8dea8..980a9b89d 100644
--- a/Makefile
+++ b/Makefile
@@ -85,6 +85,14 @@ start-docker:
docker start mattermost-webrtc > /dev/null; \
fi
+ @if [ $(shell docker ps -a | grep -ci mattermost-inbucket) -eq 0 ]; then \
+ echo starting mattermost-inbucket; \
+ docker run --name mattermost-inbucket -p 9000:10080 -p 2500:10025 -d jhillyerd/inbucket:latest > /dev/null; \
+ elif [ $(shell docker ps | grep -ci mattermost-inbucket) -eq 0 ]; then \
+ echo restarting mattermost-inbucket; \
+ docker start mattermost-inbucket > /dev/null; \
+ fi
+
ifeq ($(BUILD_ENTERPRISE_READY),true)
@echo Ldap test user test.one
@if [ $(shell docker ps -a | grep -ci mattermost-openldap) -eq 0 ]; then \
@@ -132,6 +140,11 @@ stop-docker:
docker stop mattermost-webrtc > /dev/null; \
fi
+ @if [ $(shell docker ps -a | grep -ci mattermost-inbucket) -eq 1 ]; then \
+ echo stopping mattermost-inbucket; \
+ docker stop mattermost-inbucket > /dev/null; \
+ fi
+
clean-docker:
@echo Removing docker containers
@@ -159,6 +172,12 @@ clean-docker:
docker rm -v mattermost-webrtc > /dev/null; \
fi
+ @if [ $(shell docker ps -a | grep -ci mattermost-inbucket) -eq 1 ]; then \
+ echo removing mattermost-inbucket; \
+ docker stop mattermost-inbucket > /dev/null; \
+ docker rm -v mattermost-inbucket > /dev/null; \
+ fi
+
check-client-style:
@echo Checking client style
@@ -184,17 +203,19 @@ test-server: start-docker prepare-enterprise
echo "mode: count" > cover.out
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=650s -covermode=count -coverprofile=capi.out ./api || exit 1
+ $(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=60s -covermode=count -coverprofile=capp.out ./app || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=60s -covermode=count -coverprofile=cmodel.out ./model || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s -covermode=count -coverprofile=cstore.out ./store || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s -covermode=count -coverprofile=cutils.out ./utils || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s -covermode=count -coverprofile=cweb.out ./web || exit 1
tail -n +2 capi.out >> cover.out
+ tail -n +2 capp.out >> cover.out
tail -n +2 cmodel.out >> cover.out
tail -n +2 cstore.out >> cover.out
tail -n +2 cutils.out >> cover.out
tail -n +2 cweb.out >> cover.out
- rm -f capi.out cmodel.out cstore.out cutils.out cweb.out
+ rm -f capi.out capp.out cmodel.out cstore.out cutils.out cweb.out
ifeq ($(BUILD_ENTERPRISE_READY),true)
@echo Running Enterprise tests
@@ -218,7 +239,7 @@ ifeq ($(BUILD_ENTERPRISE_READY),true)
tail -n +2 csaml.out >> ecover.out
tail -n +2 ccluster.out >> ecover.out
tail -n +2 cmetrics.out >> ecover.out
- tail -n +2 caccount_migration.out >> ecover.out
+ tail -n +2 caccount_migration.out >> ecover.out
rm -f cldap.out ccompliance.out cmfa.out cemoji.out csaml.out ccluster.out cmetrics.out caccount_migration.out
rm -r ldap.test
rm -r compliance.test
@@ -301,6 +322,12 @@ package: build build-client
@# Disable developer settings
sed -i'' -e 's|"ConsoleLevel": "DEBUG"|"ConsoleLevel": "INFO"|g' $(DIST_PATH)/config/config.json
+ @# Reset email sending to original configuration
+ sed -i'' -e 's|"SendEmailNotifications": true,|"SendEmailNotifications": false,|g' $(DIST_PATH)/config/config.json
+ sed -i'' -e 's|"FeedbackEmail": "test@example.com",|"FeedbackEmail": "",|g' $(DIST_PATH)/config/config.json
+ sed -i'' -e 's|"SMTPServer": "dockerhost",|"SMTPServer": "",|g' $(DIST_PATH)/config/config.json
+ sed -i'' -e 's|"SMTPPort": "2500",|"SMTPPort": "",|g' $(DIST_PATH)/config/config.json
+
@# Package webapp
mkdir -p $(DIST_PATH)/webapp/dist
cp -RL $(BUILD_WEBAPP_DIR)/dist $(DIST_PATH)/webapp
@@ -394,7 +421,7 @@ else
echo stopping mattermost $$PID; \
kill $$PID; \
done
-endif
+endif
stop-client:
@echo Stopping mattermost client
diff --git a/api/admin.go b/api/admin.go
index 6ec76caae..3aa1dc67d 100644
--- a/api/admin.go
+++ b/api/admin.go
@@ -4,22 +4,13 @@
package api
import (
- "bufio"
- "io"
- "io/ioutil"
"net/http"
- "os"
"strconv"
- "strings"
- "time"
-
- "runtime/debug"
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
- "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
- "github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
"github.com/mssola/user_agent"
)
@@ -55,68 +46,28 @@ func InitAdmin() {
}
func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
- lines, err := GetLogs()
+ lines, err := app.GetLogs()
if err != nil {
c.Err = err
return
}
- if einterfaces.GetClusterInterface() != nil {
- clines, err := einterfaces.GetClusterInterface().GetLogs()
- if err != nil {
- c.Err = err
- return
- }
-
- lines = append(lines, clines...)
- }
-
w.Write([]byte(model.ArrayToJson(lines)))
}
-func GetLogs() ([]string, *model.AppError) {
- var lines []string
-
- if utils.Cfg.LogSettings.EnableFile {
- file, err := os.Open(utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation))
- if err != nil {
- return nil, model.NewLocAppError("getLogs", "api.admin.file_read_error", nil, err.Error())
- }
-
- defer file.Close()
-
- scanner := bufio.NewScanner(file)
- for scanner.Scan() {
- lines = append(lines, scanner.Text())
- }
- } else {
- lines = append(lines, "")
- }
-
- return lines, nil
-}
-
func getClusterStatus(c *Context, w http.ResponseWriter, r *http.Request) {
- infos := make([]*model.ClusterInfo, 0)
- if einterfaces.GetClusterInterface() != nil {
- infos = einterfaces.GetClusterInterface().GetClusterInfos()
- }
-
+ infos := app.GetClusterStatus()
w.Write([]byte(model.ClusterInfosToJson(infos)))
}
func getAllAudits(c *Context, w http.ResponseWriter, r *http.Request) {
- if result := <-Srv.Store.Audit().Get("", 200); result.Err != nil {
- c.Err = result.Err
+ if audits, err := app.GetAudits("", 200); err != nil {
+ c.Err = err
+ return
+ } else if HandleEtag(audits.Etag(), "Get All Audits", w, r) {
return
} else {
- audits := result.Data.(model.Audits)
etag := audits.Etag()
-
- if HandleEtag(etag, "Get All Audits", w, r) {
- return
- }
-
if len(etag) > 0 {
w.Header().Set(model.HEADER_ETAG_SERVER, etag)
}
@@ -127,38 +78,22 @@ func getAllAudits(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getConfig(c *Context, w http.ResponseWriter, r *http.Request) {
- json := utils.Cfg.ToJson()
- cfg := model.ConfigFromJson(strings.NewReader(json))
-
- cfg.Sanitize()
-
+ cfg := app.GetConfig()
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Write([]byte(cfg.ToJson()))
}
func reloadConfig(c *Context, w http.ResponseWriter, r *http.Request) {
- debug.FreeOSMemory()
- utils.LoadConfig(utils.CfgFileName)
-
- // start/restart email batching job if necessary
- InitEmailBatching()
-
+ app.ReloadConfig()
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
ReturnStatusOK(w)
}
func invalidateAllCaches(c *Context, w http.ResponseWriter, r *http.Request) {
- debug.FreeOSMemory()
-
- InvalidateAllCaches()
-
- if einterfaces.GetClusterInterface() != nil {
- err := einterfaces.GetClusterInterface().InvalidateAllCaches()
- if err != nil {
- c.Err = err
- return
- }
-
+ err := app.InvalidateAllCaches()
+ if err != nil {
+ c.Err = err
+ return
}
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
@@ -172,66 +107,18 @@ func saveConfig(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- cfg.SetDefaults()
- utils.Desanitize(cfg)
-
- if err := cfg.IsValid(); err != nil {
- c.Err = err
- return
- }
-
- if err := utils.ValidateLdapFilter(cfg); err != nil {
+ err := app.SaveConfig(cfg)
+ if err != nil {
c.Err = err
return
}
- if *utils.Cfg.ClusterSettings.Enable {
- c.Err = model.NewLocAppError("saveConfig", "ent.cluster.save_config.error", nil, "")
- return
- }
-
c.LogAudit("")
-
- //oldCfg := utils.Cfg
- utils.SaveConfig(utils.CfgFileName, cfg)
- utils.LoadConfig(utils.CfgFileName)
-
- if einterfaces.GetMetricsInterface() != nil {
- if *utils.Cfg.MetricsSettings.Enable {
- einterfaces.GetMetricsInterface().StartServer()
- } else {
- einterfaces.GetMetricsInterface().StopServer()
- }
- }
-
- // Future feature is to sync the configuration files
- // if einterfaces.GetClusterInterface() != nil {
- // err := einterfaces.GetClusterInterface().ConfigChanged(cfg, oldCfg, true)
- // if err != nil {
- // c.Err = err
- // return
- // }
- // }
-
- // start/restart email batching job if necessary
- InitEmailBatching()
-
- rdata := map[string]string{}
- rdata["status"] = "OK"
- w.Write([]byte(model.MapToJson(rdata)))
+ ReturnStatusOK(w)
}
func recycleDatabaseConnection(c *Context, w http.ResponseWriter, r *http.Request) {
- oldStore := Srv.Store
-
- l4g.Warn(utils.T("api.admin.recycle_db_start.warn"))
- Srv.Store = store.NewSqlStore()
-
- time.Sleep(20 * time.Second)
- oldStore.Close()
-
- l4g.Warn(utils.T("api.admin.recycle_db_end.warn"))
-
+ app.RecycleDatabaseConnection()
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
ReturnStatusOK(w)
}
@@ -243,32 +130,10 @@ func testEmail(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if len(cfg.EmailSettings.SMTPServer) == 0 {
- c.Err = model.NewLocAppError("testEmail", "api.admin.test_email.missing_server", nil, utils.T("api.context.invalid_param.app_error", map[string]interface{}{"Name": "SMTPServer"}))
- return
- }
-
- // if the user hasn't changed their email settings, fill in the actual SMTP password so that
- // the user can verify an existing SMTP connection
- if cfg.EmailSettings.SMTPPassword == model.FAKE_SETTING {
- if cfg.EmailSettings.SMTPServer == utils.Cfg.EmailSettings.SMTPServer &&
- cfg.EmailSettings.SMTPPort == utils.Cfg.EmailSettings.SMTPPort &&
- cfg.EmailSettings.SMTPUsername == utils.Cfg.EmailSettings.SMTPUsername {
- cfg.EmailSettings.SMTPPassword = utils.Cfg.EmailSettings.SMTPPassword
- } else {
- c.Err = model.NewLocAppError("testEmail", "api.admin.test_email.reenter_password", nil, "")
- return
- }
- }
-
- if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
- c.Err = result.Err
+ err := app.TestEmail(c.Session.UserId, cfg)
+ if err != nil {
+ c.Err = err
return
- } else {
- if err := utils.SendMailUsingConfig(result.Data.(*model.User).Email, c.T("api.admin.test_email.subject"), c.T("api.admin.test_email.body"), cfg); err != nil {
- c.Err = err
- return
- }
}
m := make(map[string]string)
@@ -277,26 +142,15 @@ func testEmail(c *Context, w http.ResponseWriter, r *http.Request) {
}
func getComplianceReports(c *Context, w http.ResponseWriter, r *http.Request) {
- if !*utils.Cfg.ComplianceSettings.Enable || !utils.IsLicensed || !*utils.License.Features.Compliance {
- c.Err = model.NewLocAppError("getComplianceReports", "ent.compliance.licence_disable.app_error", nil, "")
- return
- }
-
- if result := <-Srv.Store.Compliance().GetAll(); result.Err != nil {
- c.Err = result.Err
+ crs, err := app.GetComplianceReports()
+ if err != nil {
+ c.Err = err
return
- } else {
- crs := result.Data.(model.Compliances)
- w.Write([]byte(crs.ToJson()))
}
+ w.Write([]byte(crs.ToJson()))
}
func saveComplianceReport(c *Context, w http.ResponseWriter, r *http.Request) {
- if !*utils.Cfg.ComplianceSettings.Enable || !utils.IsLicensed || !*utils.License.Features.Compliance || einterfaces.GetComplianceInterface() == nil {
- c.Err = model.NewLocAppError("saveComplianceReport", "ent.compliance.licence_disable.app_error", nil, "")
- return
- }
-
job := model.ComplianceFromJson(r.Body)
if job == nil {
c.SetInvalidParam("saveComplianceReport", "compliance")
@@ -304,25 +158,18 @@ func saveComplianceReport(c *Context, w http.ResponseWriter, r *http.Request) {
}
job.UserId = c.Session.UserId
- job.Type = model.COMPLIANCE_TYPE_ADHOC
- if result := <-Srv.Store.Compliance().Save(job); result.Err != nil {
- c.Err = result.Err
+ rjob, err := app.SaveComplianceReport(job)
+ if err != nil {
+ c.Err = err
return
- } else {
- job = result.Data.(*model.Compliance)
- go einterfaces.GetComplianceInterface().RunComplianceJob(job)
}
- w.Write([]byte(job.ToJson()))
+ c.LogAudit("")
+ w.Write([]byte(rjob.ToJson()))
}
func downloadComplianceReport(c *Context, w http.ResponseWriter, r *http.Request) {
- if !*utils.Cfg.ComplianceSettings.Enable || !utils.IsLicensed || !*utils.License.Features.Compliance || einterfaces.GetComplianceInterface() == nil {
- c.Err = model.NewLocAppError("downloadComplianceReport", "ent.compliance.licence_disable.app_error", nil, "")
- return
- }
-
params := mux.Vars(r)
id := params["id"]
@@ -331,35 +178,36 @@ func downloadComplianceReport(c *Context, w http.ResponseWriter, r *http.Request
return
}
- if result := <-Srv.Store.Compliance().Get(id); result.Err != nil {
- c.Err = result.Err
+ job, err := app.GetComplianceReport(id)
+ if err != nil {
+ c.Err = err
return
- } else {
- job := result.Data.(*model.Compliance)
- c.LogAudit("downloaded " + job.Desc)
+ }
- if f, err := ioutil.ReadFile(*utils.Cfg.ComplianceSettings.Directory + "compliance/" + job.JobName() + ".zip"); err != nil {
- c.Err = model.NewLocAppError("readFile", "api.file.read_file.reading_local.app_error", nil, err.Error())
- return
- } else {
- w.Header().Set("Cache-Control", "max-age=2592000, public")
- w.Header().Set("Content-Length", strconv.Itoa(len(f)))
- w.Header().Del("Content-Type") // Content-Type will be set automatically by the http writer
+ reportBytes, err := app.GetComplianceFile(job)
+ if err != nil {
+ c.Err = err
+ return
+ }
- // attach extra headers to trigger a download on IE, Edge, and Safari
- ua := user_agent.New(r.UserAgent())
- bname, _ := ua.Browser()
+ c.LogAudit("downloaded " + job.Desc)
- w.Header().Set("Content-Disposition", "attachment;filename=\""+job.JobName()+".zip\"")
+ w.Header().Set("Cache-Control", "max-age=2592000, public")
+ w.Header().Set("Content-Length", strconv.Itoa(len(reportBytes)))
+ w.Header().Del("Content-Type") // Content-Type will be set automatically by the http writer
- if bname == "Edge" || bname == "Internet Explorer" || bname == "Safari" {
- // trim off anything before the final / so we just get the file's name
- w.Header().Set("Content-Type", "application/octet-stream")
- }
+ // attach extra headers to trigger a download on IE, Edge, and Safari
+ ua := user_agent.New(r.UserAgent())
+ bname, _ := ua.Browser()
- w.Write(f)
- }
+ w.Header().Set("Content-Disposition", "attachment;filename=\""+job.JobName()+".zip\"")
+
+ if bname == "Edge" || bname == "Internet Explorer" || bname == "Safari" {
+ // trim off anything before the final / so we just get the file's name
+ w.Header().Set("Content-Type", "application/octet-stream")
}
+
+ w.Write(reportBytes)
}
func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -367,218 +215,18 @@ func getAnalytics(c *Context, w http.ResponseWriter, r *http.Request) {
teamId := params["id"]
name := params["name"]
- skipIntensiveQueries := false
- var systemUserCount int64
- if r := <-Srv.Store.User().AnalyticsUniqueUserCount(""); r.Err != nil {
- c.Err = r.Err
+ rows, err := app.GetAnalytics(name, teamId)
+ if err != nil {
+ c.Err = err
return
- } else {
- systemUserCount = r.Data.(int64)
- if systemUserCount > int64(*utils.Cfg.AnalyticsSettings.MaxUsersForStatistics) {
- l4g.Debug("More than %v users on the system, intensive queries skipped", *utils.Cfg.AnalyticsSettings.MaxUsersForStatistics)
- skipIntensiveQueries = true
- }
}
- if name == "standard" {
- var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 8)
- rows[0] = &model.AnalyticsRow{"channel_open_count", 0}
- rows[1] = &model.AnalyticsRow{"channel_private_count", 0}
- rows[2] = &model.AnalyticsRow{"post_count", 0}
- rows[3] = &model.AnalyticsRow{"unique_user_count", 0}
- rows[4] = &model.AnalyticsRow{"team_count", 0}
- rows[5] = &model.AnalyticsRow{"total_websocket_connections", 0}
- rows[6] = &model.AnalyticsRow{"total_master_db_connections", 0}
- rows[7] = &model.AnalyticsRow{"total_read_db_connections", 0}
-
- openChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN)
- privateChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE)
- teamChan := Srv.Store.Team().AnalyticsTeamCount()
-
- var userChan store.StoreChannel
- if teamId != "" {
- userChan = Srv.Store.User().AnalyticsUniqueUserCount(teamId)
- }
-
- var postChan store.StoreChannel
- if !skipIntensiveQueries {
- postChan = Srv.Store.Post().AnalyticsPostCount(teamId, false, false)
- }
-
- if r := <-openChan; r.Err != nil {
- c.Err = r.Err
- return
- } else {
- rows[0].Value = float64(r.Data.(int64))
- }
-
- if r := <-privateChan; r.Err != nil {
- c.Err = r.Err
- return
- } else {
- rows[1].Value = float64(r.Data.(int64))
- }
-
- if postChan == nil {
- rows[2].Value = -1
- } else {
- if r := <-postChan; r.Err != nil {
- c.Err = r.Err
- return
- } else {
- rows[2].Value = float64(r.Data.(int64))
- }
- }
-
- if userChan == nil {
- rows[3].Value = float64(systemUserCount)
- } else {
- if r := <-userChan; r.Err != nil {
- c.Err = r.Err
- return
- } else {
- rows[3].Value = float64(r.Data.(int64))
- }
- }
-
- if r := <-teamChan; r.Err != nil {
- c.Err = r.Err
- return
- } else {
- rows[4].Value = float64(r.Data.(int64))
- }
-
- // If in HA mode then aggregrate all the stats
- if einterfaces.GetClusterInterface() != nil && *utils.Cfg.ClusterSettings.Enable {
- stats, err := einterfaces.GetClusterInterface().GetClusterStats()
- if err != nil {
- c.Err = err
- return
- }
-
- totalSockets := TotalWebsocketConnections()
- totalMasterDb := Srv.Store.TotalMasterDbConnections()
- totalReadDb := Srv.Store.TotalReadDbConnections()
-
- for _, stat := range stats {
- totalSockets = totalSockets + stat.TotalWebsocketConnections
- totalMasterDb = totalMasterDb + stat.TotalMasterDbConnections
- totalReadDb = totalReadDb + stat.TotalReadDbConnections
- }
-
- rows[5].Value = float64(totalSockets)
- rows[6].Value = float64(totalMasterDb)
- rows[7].Value = float64(totalReadDb)
-
- } else {
- rows[5].Value = float64(TotalWebsocketConnections())
- rows[6].Value = float64(Srv.Store.TotalMasterDbConnections())
- rows[7].Value = float64(Srv.Store.TotalReadDbConnections())
- }
-
- w.Write([]byte(rows.ToJson()))
- } else if name == "post_counts_day" {
- if skipIntensiveQueries {
- rows := model.AnalyticsRows{&model.AnalyticsRow{"", -1}}
- w.Write([]byte(rows.ToJson()))
- return
- }
-
- if r := <-Srv.Store.Post().AnalyticsPostCountsByDay(teamId); r.Err != nil {
- c.Err = r.Err
- return
- } else {
- w.Write([]byte(r.Data.(model.AnalyticsRows).ToJson()))
- }
- } else if name == "user_counts_with_posts_day" {
- if skipIntensiveQueries {
- rows := model.AnalyticsRows{&model.AnalyticsRow{"", -1}}
- w.Write([]byte(rows.ToJson()))
- return
- }
-
- if r := <-Srv.Store.Post().AnalyticsUserCountsWithPostsByDay(teamId); r.Err != nil {
- c.Err = r.Err
- return
- } else {
- w.Write([]byte(r.Data.(model.AnalyticsRows).ToJson()))
- }
- } else if name == "extra_counts" {
- var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 6)
- rows[0] = &model.AnalyticsRow{"file_post_count", 0}
- rows[1] = &model.AnalyticsRow{"hashtag_post_count", 0}
- rows[2] = &model.AnalyticsRow{"incoming_webhook_count", 0}
- rows[3] = &model.AnalyticsRow{"outgoing_webhook_count", 0}
- rows[4] = &model.AnalyticsRow{"command_count", 0}
- rows[5] = &model.AnalyticsRow{"session_count", 0}
-
- iHookChan := Srv.Store.Webhook().AnalyticsIncomingCount(teamId)
- oHookChan := Srv.Store.Webhook().AnalyticsOutgoingCount(teamId)
- commandChan := Srv.Store.Command().AnalyticsCommandCount(teamId)
- sessionChan := Srv.Store.Session().AnalyticsSessionCount()
-
- var fileChan store.StoreChannel
- var hashtagChan store.StoreChannel
- if !skipIntensiveQueries {
- fileChan = Srv.Store.Post().AnalyticsPostCount(teamId, true, false)
- hashtagChan = Srv.Store.Post().AnalyticsPostCount(teamId, false, true)
- }
-
- if fileChan == nil {
- rows[0].Value = -1
- } else {
- if r := <-fileChan; r.Err != nil {
- c.Err = r.Err
- return
- } else {
- rows[0].Value = float64(r.Data.(int64))
- }
- }
-
- if hashtagChan == nil {
- rows[1].Value = -1
- } else {
- if r := <-hashtagChan; r.Err != nil {
- c.Err = r.Err
- return
- } else {
- rows[1].Value = float64(r.Data.(int64))
- }
- }
-
- if r := <-iHookChan; r.Err != nil {
- c.Err = r.Err
- return
- } else {
- rows[2].Value = float64(r.Data.(int64))
- }
-
- if r := <-oHookChan; r.Err != nil {
- c.Err = r.Err
- return
- } else {
- rows[3].Value = float64(r.Data.(int64))
- }
-
- if r := <-commandChan; r.Err != nil {
- c.Err = r.Err
- return
- } else {
- rows[4].Value = float64(r.Data.(int64))
- }
-
- if r := <-sessionChan; r.Err != nil {
- c.Err = r.Err
- return
- } else {
- rows[5].Value = float64(r.Data.(int64))
- }
-
- w.Write([]byte(rows.ToJson()))
- } else {
+ if rows == nil {
c.SetInvalidParam("getAnalytics", "name")
+ return
}
+ w.Write([]byte(rows.ToJson()))
}
func uploadBrandImage(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -614,40 +262,18 @@ func uploadBrandImage(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- brandInterface := einterfaces.GetBrandInterface()
- if brandInterface == nil {
- c.Err = model.NewLocAppError("uploadBrandImage", "api.admin.upload_brand_image.not_available.app_error", nil, "")
- c.Err.StatusCode = http.StatusNotImplemented
- return
- }
-
- if err := brandInterface.SaveBrandImage(imageArray[0]); err != nil {
+ if err := app.SaveBrandImage(imageArray[0]); err != nil {
c.Err = err
return
}
c.LogAudit("")
- rdata := map[string]string{}
- rdata["status"] = "OK"
- w.Write([]byte(model.MapToJson(rdata)))
+ ReturnStatusOK(w)
}
func getBrandImage(c *Context, w http.ResponseWriter, r *http.Request) {
- if len(utils.Cfg.FileSettings.DriverName) == 0 {
- c.Err = model.NewLocAppError("getBrandImage", "api.admin.get_brand_image.storage.app_error", nil, "")
- c.Err.StatusCode = http.StatusNotImplemented
- return
- }
-
- brandInterface := einterfaces.GetBrandInterface()
- if brandInterface == nil {
- c.Err = model.NewLocAppError("getBrandImage", "api.admin.get_brand_image.not_available.app_error", nil, "")
- c.Err.StatusCode = http.StatusNotImplemented
- return
- }
-
- if img, err := brandInterface.GetBrandImage(); err != nil {
+ if img, err := app.GetBrandImage(); err != nil {
w.Write(nil)
} else {
w.Header().Set("Content-Type", "image/png")
@@ -664,7 +290,7 @@ func adminResetMfa(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if err := DeactivateMfa(userId); err != nil {
+ if err := app.DeactivateMfa(userId); err != nil {
c.Err = err
return
}
@@ -691,7 +317,7 @@ func adminResetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if err := ResetPassword(c, userId, newPassword); err != nil {
+ if err := app.UpdatePasswordByUserIdSendEmail(userId, newPassword, c.T("api.user.reset_password.method"), c.GetSiteURL()); err != nil {
c.Err = err
return
}
@@ -704,15 +330,7 @@ func adminResetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
}
func ldapSyncNow(c *Context, w http.ResponseWriter, r *http.Request) {
- go func() {
- if utils.IsLicensed && *utils.License.Features.LDAP && *utils.Cfg.LdapSettings.Enable {
- if ldapI := einterfaces.GetLdapInterface(); ldapI != nil {
- ldapI.SyncNow()
- } else {
- l4g.Error("%v", model.NewLocAppError("ldapSyncNow", "ent.ldap.disabled.app_error", nil, "").Error())
- }
- }
- }()
+ app.SyncLdap()
rdata := map[string]string{}
rdata["status"] = "ok"
@@ -720,33 +338,18 @@ func ldapSyncNow(c *Context, w http.ResponseWriter, r *http.Request) {
}
func ldapTest(c *Context, w http.ResponseWriter, r *http.Request) {
- if ldapI := einterfaces.GetLdapInterface(); ldapI != nil && utils.IsLicensed && *utils.License.Features.LDAP && *utils.Cfg.LdapSettings.Enable {
- if err := ldapI.RunTest(); err != nil {
- c.Err = err
- c.Err.StatusCode = 500
- }
- } else {
- c.Err = model.NewLocAppError("ldapTest", "ent.ldap.disabled.app_error", nil, "")
- c.Err.StatusCode = http.StatusNotImplemented
+ if err := app.TestLdap(); err != nil {
+ c.Err = err
+ return
}
- if c.Err == nil {
- rdata := map[string]string{}
- rdata["status"] = "ok"
- w.Write([]byte(model.MapToJson(rdata)))
- }
+ rdata := map[string]string{}
+ rdata["status"] = "ok"
+ w.Write([]byte(model.MapToJson(rdata)))
}
func samlMetadata(c *Context, w http.ResponseWriter, r *http.Request) {
- samlInterface := einterfaces.GetSamlInterface()
-
- if samlInterface == nil {
- c.Err = model.NewLocAppError("loginWithSaml", "api.admin.saml.not_available.app_error", nil, "")
- c.Err.StatusCode = http.StatusFound
- return
- }
-
- if result, err := samlInterface.GetMetadata(); err != nil {
+ if result, err := app.GetSamlMetadata(); err != nil {
c.Err = model.NewLocAppError("loginWithSaml", "api.admin.saml.metadata.app_error", nil, "err="+err.Message)
return
} else {
@@ -780,58 +383,38 @@ func addCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
fileData := fileArray[0]
- file, err := fileData.Open()
- defer file.Close()
- if err != nil {
- c.Err = model.NewLocAppError("addCertificate", "api.admin.add_certificate.open.app_error", nil, err.Error())
- return
- }
-
- out, err := os.Create(utils.FindDir("config") + fileData.Filename)
- if err != nil {
- c.Err = model.NewLocAppError("addCertificate", "api.admin.add_certificate.saving.app_error", nil, err.Error())
+ if err := app.AddSamlCertificate(fileData); err != nil {
+ c.Err = err
return
}
- defer out.Close()
-
- io.Copy(out, file)
ReturnStatusOK(w)
}
func removeCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJson(r.Body)
- filename := props["filename"]
- if err := os.Remove(utils.FindConfigFile(filename)); err != nil {
- c.Err = model.NewLocAppError("removeCertificate", "api.admin.remove_certificate.delete.app_error",
- map[string]interface{}{"Filename": filename}, err.Error())
+ if err := app.RemoveSamlCertificate(props["filename"]); err != nil {
+ c.Err = err
return
}
+
ReturnStatusOK(w)
}
func samlCertificateStatus(c *Context, w http.ResponseWriter, r *http.Request) {
- status := make(map[string]interface{})
-
- status["IdpCertificateFile"] = utils.FileExistsInConfigFolder(*utils.Cfg.SamlSettings.IdpCertificateFile)
- status["PrivateKeyFile"] = utils.FileExistsInConfigFolder(*utils.Cfg.SamlSettings.PrivateKeyFile)
- status["PublicCertificateFile"] = utils.FileExistsInConfigFolder(*utils.Cfg.SamlSettings.PublicCertificateFile)
-
+ status := app.GetSamlCertificateStatus()
w.Write([]byte(model.StringInterfaceToJson(status)))
}
func getRecentlyActiveUsers(c *Context, w http.ResponseWriter, r *http.Request) {
- if result := <-Srv.Store.User().GetRecentlyActiveUsersForTeam(c.TeamId); result.Err != nil {
- c.Err = result.Err
+ if profiles, err := app.GetRecentlyActiveUsersForTeam(c.TeamId); err != nil {
+ c.Err = err
return
} else {
- profiles := result.Data.(map[string]*model.User)
-
for _, p := range profiles {
sanitizeProfile(c, p)
}
w.Write([]byte(model.UserMapToJson(profiles)))
}
-
}
diff --git a/api/admin_test.go b/api/admin_test.go
index 3af45892d..801ad8f21 100644
--- a/api/admin_test.go
+++ b/api/admin_test.go
@@ -7,6 +7,7 @@ import (
"strings"
"testing"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
@@ -159,6 +160,22 @@ func TestRecycleDatabaseConnection(t *testing.T) {
func TestEmailTest(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
+ SendEmailNotifications := utils.Cfg.EmailSettings.SendEmailNotifications
+ SMTPServer := utils.Cfg.EmailSettings.SMTPServer
+ SMTPPort := utils.Cfg.EmailSettings.SMTPPort
+ FeedbackEmail := utils.Cfg.EmailSettings.FeedbackEmail
+ defer func() {
+ utils.Cfg.EmailSettings.SendEmailNotifications = SendEmailNotifications
+ utils.Cfg.EmailSettings.SMTPServer = SMTPServer
+ utils.Cfg.EmailSettings.SMTPPort = SMTPPort
+ utils.Cfg.EmailSettings.FeedbackEmail = FeedbackEmail
+ }()
+
+ utils.Cfg.EmailSettings.SendEmailNotifications = false
+ utils.Cfg.EmailSettings.SMTPServer = ""
+ utils.Cfg.EmailSettings.SMTPPort = ""
+ utils.Cfg.EmailSettings.FeedbackEmail = ""
+
if _, err := th.BasicClient.TestEmail(utils.Cfg); err == nil {
t.Fatal("Shouldn't have permissions")
}
@@ -333,7 +350,7 @@ func TestGetPostCount(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
// manually update creation time, since it's always set to 0 upon saving and we only retrieve posts < today
- Srv.Store.(*store.SqlStore).GetMaster().Exec("UPDATE Posts SET CreateAt = :CreateAt WHERE ChannelId = :ChannelId",
+ app.Srv.Store.(*store.SqlStore).GetMaster().Exec("UPDATE Posts SET CreateAt = :CreateAt WHERE ChannelId = :ChannelId",
map[string]interface{}{"ChannelId": th.BasicChannel.Id, "CreateAt": utils.MillisFromTime(utils.Yesterday())})
if _, err := th.BasicClient.GetTeamAnalytics(th.BasicTeam.Id, "post_counts_day"); err == nil {
@@ -375,7 +392,7 @@ func TestUserCountsWithPostsByDay(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
// manually update creation time, since it's always set to 0 upon saving and we only retrieve posts < today
- Srv.Store.(*store.SqlStore).GetMaster().Exec("UPDATE Posts SET CreateAt = :CreateAt WHERE ChannelId = :ChannelId",
+ app.Srv.Store.(*store.SqlStore).GetMaster().Exec("UPDATE Posts SET CreateAt = :CreateAt WHERE ChannelId = :ChannelId",
map[string]interface{}{"ChannelId": th.BasicChannel.Id, "CreateAt": utils.MillisFromTime(utils.Yesterday())})
if _, err := th.BasicClient.GetTeamAnalytics(th.BasicTeam.Id, "user_counts_with_posts_day"); err == nil {
@@ -579,7 +596,7 @@ func TestAdminResetPassword(t *testing.T) {
user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
LinkUserToTeam(user, team)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user.Id))
if _, err := Client.AdminResetPassword("", "newpwd1"); err == nil {
t.Fatal("Should have errored - empty user id")
@@ -601,7 +618,7 @@ func TestAdminResetPassword(t *testing.T) {
user2 := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", AuthData: &authData, AuthService: "random"}
user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
LinkUserToTeam(user2, team)
- store.Must(Srv.Store.User().VerifyEmail(user2.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user2.Id))
if _, err := Client.AdminResetPassword(user.Id, "newpwd1"); err != nil {
t.Fatal(err)
diff --git a/api/api.go b/api/api.go
index 122a6b933..59c547b8c 100644
--- a/api/api.go
+++ b/api/api.go
@@ -7,6 +7,7 @@ import (
"net/http"
"github.com/gorilla/mux"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
@@ -53,16 +54,20 @@ type Routes struct {
Emoji *mux.Router // 'api/v3/emoji'
Webrtc *mux.Router // 'api/v3/webrtc'
-
- WebSocket *WebSocketRouter // websocket api
}
var BaseRoutes *Routes
+func InitRouter() {
+ app.Srv.Router = mux.NewRouter()
+ app.Srv.Router.NotFoundHandler = http.HandlerFunc(Handle404)
+ app.Srv.WebSocketRouter = app.NewWebSocketRouter()
+}
+
func InitApi() {
BaseRoutes = &Routes{}
- BaseRoutes.Root = Srv.Router
- BaseRoutes.ApiRoot = Srv.Router.PathPrefix(model.API_URL_SUFFIX).Subrouter()
+ BaseRoutes.Root = app.Srv.Router
+ BaseRoutes.ApiRoot = app.Srv.Router.PathPrefix(model.API_URL_SUFFIX).Subrouter()
BaseRoutes.Users = BaseRoutes.ApiRoot.PathPrefix("/users").Subrouter()
BaseRoutes.NeedUser = BaseRoutes.Users.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
BaseRoutes.Teams = BaseRoutes.ApiRoot.PathPrefix("/teams").Subrouter()
@@ -86,8 +91,6 @@ func InitApi() {
BaseRoutes.Emoji = BaseRoutes.ApiRoot.PathPrefix("/emoji").Subrouter()
BaseRoutes.Webrtc = BaseRoutes.ApiRoot.PathPrefix("/webrtc").Subrouter()
- BaseRoutes.WebSocket = NewWebSocketRouter()
-
InitUser()
InitTeam()
InitChannel()
@@ -108,11 +111,11 @@ func InitApi() {
InitDeprecated()
// 404 on any api route before web.go has a chance to serve it
- Srv.Router.Handle("/api/{anything:.*}", http.HandlerFunc(Handle404))
+ app.Srv.Router.Handle("/api/{anything:.*}", http.HandlerFunc(Handle404))
utils.InitHTML()
- InitEmailBatching()
+ app.InitEmailBatching()
}
func HandleEtag(etag string, routeName string, w http.ResponseWriter, r *http.Request) bool {
diff --git a/api/apitestlib.go b/api/apitestlib.go
index 8fb030488..475469a36 100644
--- a/api/apitestlib.go
+++ b/api/apitestlib.go
@@ -6,6 +6,7 @@ package api
import (
"time"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
@@ -28,7 +29,7 @@ type TestHelper struct {
}
func SetupEnterprise() *TestHelper {
- if Srv == nil {
+ if app.Srv == nil {
utils.TranslationsPreInit()
utils.LoadConfig("config.json")
utils.InitTranslations(utils.Cfg.LocalizationSettings)
@@ -36,14 +37,14 @@ func SetupEnterprise() *TestHelper {
*utils.Cfg.RateLimitSettings.Enable = false
utils.DisableDebugLogForTest()
utils.License.Features.SetDefaults()
- NewServer()
- InitStores()
+ app.NewServer()
+ app.InitStores()
InitRouter()
- StartServer()
+ app.StartServer()
utils.InitHTML()
InitApi()
utils.EnableDebugLogForTest()
- Srv.Store.MarkSystemRanUnitTests()
+ app.Srv.Store.MarkSystemRanUnitTests()
*utils.Cfg.TeamSettings.EnableOpenServer = true
}
@@ -52,20 +53,24 @@ func SetupEnterprise() *TestHelper {
}
func Setup() *TestHelper {
- if Srv == nil {
+ if app.Srv == nil {
utils.TranslationsPreInit()
utils.LoadConfig("config.json")
utils.InitTranslations(utils.Cfg.LocalizationSettings)
utils.Cfg.TeamSettings.MaxUsersPerTeam = 50
*utils.Cfg.RateLimitSettings.Enable = false
+ utils.Cfg.EmailSettings.SendEmailNotifications = true
+ utils.Cfg.EmailSettings.SMTPServer = "dockerhost"
+ utils.Cfg.EmailSettings.SMTPPort = "2500"
+ utils.Cfg.EmailSettings.FeedbackEmail = "test@example.com"
utils.DisableDebugLogForTest()
- NewServer()
- InitStores()
+ app.NewServer()
+ app.InitStores()
InitRouter()
- StartServer()
+ app.StartServer()
InitApi()
utils.EnableDebugLogForTest()
- Srv.Store.MarkSystemRanUnitTests()
+ app.Srv.Store.MarkSystemRanUnitTests()
*utils.Cfg.TeamSettings.EnableOpenServer = true
}
@@ -75,13 +80,14 @@ func Setup() *TestHelper {
func (me *TestHelper) InitBasic() *TestHelper {
me.BasicClient = me.CreateClient()
- me.BasicTeam = me.CreateTeam(me.BasicClient)
me.BasicUser = me.CreateUser(me.BasicClient)
+ me.LoginBasic()
+ me.BasicTeam = me.CreateTeam(me.BasicClient)
LinkUserToTeam(me.BasicUser, me.BasicTeam)
+ UpdateUserToNonTeamAdmin(me.BasicUser, me.BasicTeam)
me.BasicUser2 = me.CreateUser(me.BasicClient)
LinkUserToTeam(me.BasicUser2, me.BasicTeam)
me.BasicClient.SetTeamId(me.BasicTeam.Id)
- me.LoginBasic()
me.BasicChannel = me.CreateChannel(me.BasicClient, me.BasicTeam)
me.BasicPost = me.CreatePost(me.BasicClient, me.BasicChannel)
@@ -90,13 +96,13 @@ func (me *TestHelper) InitBasic() *TestHelper {
func (me *TestHelper) InitSystemAdmin() *TestHelper {
me.SystemAdminClient = me.CreateClient()
- me.SystemAdminTeam = me.CreateTeam(me.SystemAdminClient)
me.SystemAdminUser = me.CreateUser(me.SystemAdminClient)
- LinkUserToTeam(me.SystemAdminUser, me.SystemAdminTeam)
- me.SystemAdminClient.SetTeamId(me.SystemAdminTeam.Id)
- UpdateUserRoles(me.SystemAdminUser, model.ROLE_SYSTEM_USER.Id+" "+model.ROLE_SYSTEM_ADMIN.Id)
me.SystemAdminUser.Password = "Password1"
me.LoginSystemAdmin()
+ me.SystemAdminTeam = me.CreateTeam(me.SystemAdminClient)
+ LinkUserToTeam(me.SystemAdminUser, me.SystemAdminTeam)
+ me.SystemAdminClient.SetTeamId(me.SystemAdminTeam.Id)
+ app.UpdateUserRoles(me.SystemAdminUser.Id, model.ROLE_SYSTEM_USER.Id+" "+model.ROLE_SYSTEM_ADMIN.Id)
me.SystemAdminChannel = me.CreateChannel(me.SystemAdminClient, me.SystemAdminTeam)
return me
@@ -138,7 +144,7 @@ func (me *TestHelper) CreateUser(client *model.Client) *model.User {
utils.DisableDebugLogForTest()
ruser := client.Must(client.CreateUser(user, "")).Data.(*model.User)
ruser.Password = "Password1"
- store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser.Id))
utils.EnableDebugLogForTest()
return ruser
}
@@ -146,7 +152,7 @@ func (me *TestHelper) CreateUser(client *model.Client) *model.User {
func LinkUserToTeam(user *model.User, team *model.Team) {
utils.DisableDebugLogForTest()
- err := JoinUserToTeam(team, user)
+ err := app.JoinUserToTeam(team, user)
if err != nil {
l4g.Error(err.Error())
l4g.Close()
@@ -161,7 +167,21 @@ func UpdateUserToTeamAdmin(user *model.User, team *model.Team) {
utils.DisableDebugLogForTest()
tm := &model.TeamMember{TeamId: team.Id, UserId: user.Id, Roles: model.ROLE_TEAM_USER.Id + " " + model.ROLE_TEAM_ADMIN.Id}
- if tmr := <-Srv.Store.Team().UpdateMember(tm); tmr.Err != nil {
+ if tmr := <-app.Srv.Store.Team().UpdateMember(tm); tmr.Err != nil {
+ utils.EnableDebugLogForTest()
+ l4g.Error(tmr.Err.Error())
+ l4g.Close()
+ time.Sleep(time.Second)
+ panic(tmr.Err)
+ }
+ utils.EnableDebugLogForTest()
+}
+
+func UpdateUserToNonTeamAdmin(user *model.User, team *model.Team) {
+ utils.DisableDebugLogForTest()
+
+ tm := &model.TeamMember{TeamId: team.Id, UserId: user.Id, Roles: model.ROLE_TEAM_USER.Id}
+ if tmr := <-app.Srv.Store.Team().UpdateMember(tm); tmr.Err != nil {
utils.EnableDebugLogForTest()
l4g.Error(tmr.Err.Error())
l4g.Close()
@@ -174,10 +194,10 @@ func UpdateUserToTeamAdmin(user *model.User, team *model.Team) {
func MakeUserChannelAdmin(user *model.User, channel *model.Channel) {
utils.DisableDebugLogForTest()
- if cmr := <-Srv.Store.Channel().GetMember(channel.Id, user.Id); cmr.Err == nil {
- cm := cmr.Data.(model.ChannelMember)
+ if cmr := <-app.Srv.Store.Channel().GetMember(channel.Id, user.Id); cmr.Err == nil {
+ cm := cmr.Data.(*model.ChannelMember)
cm.Roles = "channel_admin channel_user"
- if sr := <-Srv.Store.Channel().UpdateMember(&cm); sr.Err != nil {
+ if sr := <-app.Srv.Store.Channel().UpdateMember(cm); sr.Err != nil {
utils.EnableDebugLogForTest()
panic(sr.Err)
}
@@ -192,10 +212,10 @@ func MakeUserChannelAdmin(user *model.User, channel *model.Channel) {
func MakeUserChannelUser(user *model.User, channel *model.Channel) {
utils.DisableDebugLogForTest()
- if cmr := <-Srv.Store.Channel().GetMember(channel.Id, user.Id); cmr.Err == nil {
- cm := cmr.Data.(model.ChannelMember)
+ if cmr := <-app.Srv.Store.Channel().GetMember(channel.Id, user.Id); cmr.Err == nil {
+ cm := cmr.Data.(*model.ChannelMember)
cm.Roles = "channel_user"
- if sr := <-Srv.Store.Channel().UpdateMember(&cm); sr.Err != nil {
+ if sr := <-app.Srv.Store.Channel().UpdateMember(cm); sr.Err != nil {
utils.EnableDebugLogForTest()
panic(sr.Err)
}
@@ -264,7 +284,7 @@ func (me *TestHelper) LoginSystemAdmin() {
}
func TearDown() {
- if Srv != nil {
- StopServer()
+ if app.Srv != nil {
+ app.StopServer()
}
}
diff --git a/api/authentication.go b/api/authentication.go
index fbfdb2cf4..ab649ee10 100644
--- a/api/authentication.go
+++ b/api/authentication.go
@@ -4,6 +4,7 @@
package api
import (
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
@@ -39,13 +40,13 @@ func doubleCheckPassword(user *model.User, password string) *model.AppError {
func checkUserPassword(user *model.User, password string) *model.AppError {
if !model.ComparePassword(user.Password, password) {
- if result := <-Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); result.Err != nil {
+ if result := <-app.Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); result.Err != nil {
return result.Err
}
return model.NewLocAppError("checkUserPassword", "api.user.check_user_password.invalid.app_error", nil, "user_id="+user.Id)
} else {
- if result := <-Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, 0); result.Err != nil {
+ if result := <-app.Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, 0); result.Err != nil {
return result.Err
}
diff --git a/api/authorization.go b/api/authorization.go
deleted file mode 100644
index e931c4b33..000000000
--- a/api/authorization.go
+++ /dev/null
@@ -1,187 +0,0 @@
-// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-package api
-
-import (
- "net/http"
- "strings"
-
- l4g "github.com/alecthomas/log4go"
- "github.com/mattermost/platform/model"
-)
-
-func HasPermissionToContext(c *Context, permission *model.Permission) bool {
- userRoles := c.Session.GetUserRoles()
- if !CheckIfRolesGrantPermission(userRoles, permission.Id) {
- c.Err = model.NewLocAppError("HasPermissionToContext", "api.context.permissions.app_error", nil, "userId="+c.Session.UserId+", teamId="+c.TeamId+" permission="+permission.Id+" "+model.RoleIdsToString(userRoles))
- c.Err.StatusCode = http.StatusForbidden
- return false
- }
-
- return true
-}
-
-func HasPermissionTo(user *model.User, permission *model.Permission) bool {
- roles := user.GetRoles()
-
- return CheckIfRolesGrantPermission(roles, permission.Id)
-}
-
-func HasPermissionToCurrentTeamContext(c *Context, permission *model.Permission) bool {
- return HasPermissionToTeamContext(c, c.TeamId, permission)
-}
-
-func HasPermissionToTeamContext(c *Context, teamId string, permission *model.Permission) bool {
- teamMember := c.Session.GetTeamByTeamId(teamId)
- if teamMember != nil {
- roles := teamMember.GetRoles()
-
- if CheckIfRolesGrantPermission(roles, permission.Id) {
- return true
- }
- }
-
- if HasPermissionToContext(c, permission) {
- return true
- }
-
- c.Err = model.NewLocAppError("HasPermissionToTeamContext", "api.context.permissions.app_error", nil, "userId="+c.Session.UserId+", teamId="+c.TeamId+" permission="+permission.Id)
- c.Err.StatusCode = http.StatusForbidden
- return false
-}
-
-func HasPermissionToTeam(user *model.User, teamMember *model.TeamMember, permission *model.Permission) bool {
- if teamMember == nil {
- return false
- }
-
- roles := teamMember.GetRoles()
-
- if CheckIfRolesGrantPermission(roles, permission.Id) {
- return true
- }
-
- return HasPermissionTo(user, permission)
-}
-
-func HasPermissionToChannelContext(c *Context, channelId string, permission *model.Permission) bool {
- cmc := Srv.Store.Channel().GetAllChannelMembersForUser(c.Session.UserId, true)
-
- var channelRoles []string
- if cmcresult := <-cmc; cmcresult.Err == nil {
- ids := cmcresult.Data.(map[string]string)
- if roles, ok := ids[channelId]; ok {
- channelRoles = strings.Fields(roles)
- if CheckIfRolesGrantPermission(channelRoles, permission.Id) {
- return true
- }
- }
- }
-
- cc := Srv.Store.Channel().Get(channelId, true)
- if ccresult := <-cc; ccresult.Err == nil {
- channel := ccresult.Data.(*model.Channel)
-
- if teamMember := c.Session.GetTeamByTeamId(channel.TeamId); teamMember != nil {
- roles := teamMember.GetRoles()
-
- if CheckIfRolesGrantPermission(roles, permission.Id) {
- return true
- }
- }
-
- }
-
- if HasPermissionToContext(c, permission) {
- return true
- }
-
- c.Err = model.NewLocAppError("HasPermissionToChannelContext", "api.context.permissions.app_error", nil, "userId="+c.Session.UserId+", "+"permission="+permission.Id+" channelRoles="+model.RoleIdsToString(channelRoles))
- c.Err.StatusCode = http.StatusForbidden
- return false
-}
-
-func HasPermissionToChannel(user *model.User, teamMember *model.TeamMember, channelMember *model.ChannelMember, permission *model.Permission) bool {
- if channelMember == nil {
- return false
- }
-
- roles := channelMember.GetRoles()
-
- if CheckIfRolesGrantPermission(roles, permission.Id) {
- return true
- }
-
- return HasPermissionToTeam(user, teamMember, permission)
-}
-
-func HasPermissionToChannelByPostContext(c *Context, postId string, permission *model.Permission) bool {
- cmc := Srv.Store.Channel().GetMemberForPost(postId, c.Session.UserId)
-
- var channelRoles []string
- if cmcresult := <-cmc; cmcresult.Err == nil {
- channelMember := cmcresult.Data.(*model.ChannelMember)
- channelRoles = channelMember.GetRoles()
-
- if CheckIfRolesGrantPermission(channelRoles, permission.Id) {
- return true
- }
- }
-
- cc := Srv.Store.Channel().GetForPost(postId)
- if ccresult := <-cc; ccresult.Err == nil {
- channel := ccresult.Data.(*model.Channel)
-
- if teamMember := c.Session.GetTeamByTeamId(channel.TeamId); teamMember != nil {
- roles := teamMember.GetRoles()
-
- if CheckIfRolesGrantPermission(roles, permission.Id) {
- return true
- }
- }
-
- }
-
- if HasPermissionToContext(c, permission) {
- return true
- }
-
- c.Err = model.NewLocAppError("HasPermissionToChannelByPostContext", "api.context.permissions.app_error", nil, "userId="+c.Session.UserId+", "+"permission="+permission.Id+" channelRoles="+model.RoleIdsToString(channelRoles))
- c.Err.StatusCode = http.StatusForbidden
- return false
-}
-
-func HasPermissionToUser(c *Context, userId string) bool {
- // You are the user (users autmaticly have permissions to themselves)
- if c.Session.UserId == userId {
- return true
- }
-
- // You have permission
- if HasPermissionToContext(c, model.PERMISSION_EDIT_OTHER_USERS) {
- return true
- }
-
- c.Err = model.NewLocAppError("HasPermissionToUser", "api.context.permissions.app_error", nil, "userId="+userId)
- c.Err.StatusCode = http.StatusForbidden
- return false
-}
-
-func CheckIfRolesGrantPermission(roles []string, permissionId string) bool {
- for _, roleId := range roles {
- if role, ok := model.BuiltInRoles[roleId]; !ok {
- l4g.Debug("Bad role in system " + roleId)
- return false
- } else {
- permissions := role.Permissions
- for _, permission := range permissions {
- if permission == permissionId {
- return true
- }
- }
- }
- }
-
- return false
-}
diff --git a/api/auto_users.go b/api/auto_users.go
index 7439de96e..f1f7b483c 100644
--- a/api/auto_users.go
+++ b/api/auto_users.go
@@ -4,6 +4,7 @@
package api
import (
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
@@ -49,8 +50,8 @@ func CreateBasicUser(client *model.Client) *model.AppError {
return err
}
ruser := result.Data.(*model.User)
- store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
- store.Must(Srv.Store.Team().SaveMember(&model.TeamMember{TeamId: basicteam.Id, UserId: ruser.Id}))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser.Id))
+ store.Must(app.Srv.Store.Team().SaveMember(&model.TeamMember{TeamId: basicteam.Id, UserId: ruser.Id}))
}
return nil
}
@@ -81,14 +82,14 @@ func (cfg *AutoUserCreator) createRandomUser() (*model.User, bool) {
ruser := result.Data.(*model.User)
status := &model.Status{ruser.Id, model.STATUS_ONLINE, false, model.GetMillis(), ""}
- if result := <-Srv.Store.Status().SaveOrUpdate(status); result.Err != nil {
+ if result := <-app.Srv.Store.Status().SaveOrUpdate(status); result.Err != nil {
result.Err.Translate(utils.T)
l4g.Error(result.Err.Error())
return nil, false
}
// We need to cheat to verify the user's email
- store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser.Id))
return result.Data.(*model.User), true
}
diff --git a/api/channel.go b/api/channel.go
index ae92ab618..474c41d07 100644
--- a/api/channel.go
+++ b/api/channel.go
@@ -11,8 +11,8 @@ import (
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
- "github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
)
@@ -45,6 +45,7 @@ func InitChannel() {
BaseRoutes.NeedChannel.Handle("/delete", ApiUserRequired(deleteChannel)).Methods("POST")
BaseRoutes.NeedChannel.Handle("/add", ApiUserRequired(addMember)).Methods("POST")
BaseRoutes.NeedChannel.Handle("/remove", ApiUserRequired(removeMember)).Methods("POST")
+ BaseRoutes.NeedChannel.Handle("/update_member_roles", ApiUserRequired(updateChannelMemberRoles)).Methods("POST")
}
func createChannel(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -70,23 +71,24 @@ func createChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if channel.Type == model.CHANNEL_OPEN && !HasPermissionToTeamContext(c, channel.TeamId, model.PERMISSION_CREATE_PUBLIC_CHANNEL) {
+ if channel.Type == model.CHANNEL_OPEN && !app.SessionHasPermissionToTeam(c.Session, channel.TeamId, model.PERMISSION_CREATE_PUBLIC_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_CREATE_PUBLIC_CHANNEL)
return
}
- if channel.Type == model.CHANNEL_PRIVATE && !HasPermissionToTeamContext(c, channel.TeamId, model.PERMISSION_CREATE_PRIVATE_CHANNEL) {
+ if channel.Type == model.CHANNEL_PRIVATE && !app.SessionHasPermissionToTeam(c.Session, channel.TeamId, model.PERMISSION_CREATE_PRIVATE_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_CREATE_PRIVATE_CHANNEL)
return
}
if channel.TeamId == c.TeamId {
// Get total number of channels on current team
- if result := <-Srv.Store.Channel().GetTeamChannels(channel.TeamId); result.Err != nil {
- c.Err = model.NewLocAppError("createChannel", "api.channel.get_channels.error", nil, result.Err.Message)
+ if count, err := app.GetNumberOfChannelsOnTeam(channel.TeamId); err != nil {
+ c.Err = model.NewLocAppError("createChannel", "api.channel.get_channels.error", nil, err.Error())
return
} else {
- data := result.Data.(*model.ChannelList)
- if int64(len(*data)+1) > *utils.Cfg.TeamSettings.MaxChannelsPerTeam {
+ if int64(count+1) > *utils.Cfg.TeamSettings.MaxChannelsPerTeam {
c.Err = model.NewLocAppError("createChannel", "api.channel.create_channel.max_channel_limit.app_error", map[string]interface{}{"MaxChannelsPerTeam": *utils.Cfg.TeamSettings.MaxChannelsPerTeam}, "")
return
}
@@ -95,43 +97,18 @@ func createChannel(c *Context, w http.ResponseWriter, r *http.Request) {
channel.CreatorId = c.Session.UserId
- if sc, err := CreateChannel(c, channel, true); err != nil {
+ if sc, err := app.CreateChannel(channel, true); err != nil {
c.Err = err
return
} else {
- w.Write([]byte(sc.ToJson()))
- }
-}
-
-func CreateChannel(c *Context, channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) {
- if result := <-Srv.Store.Channel().Save(channel); result.Err != nil {
- return nil, result.Err
- } else {
- sc := result.Data.(*model.Channel)
-
- if addMember {
- cm := &model.ChannelMember{
- ChannelId: sc.Id,
- UserId: c.Session.UserId,
- Roles: model.ROLE_CHANNEL_USER.Id + " " + model.ROLE_CHANNEL_ADMIN.Id,
- NotifyProps: model.GetDefaultChannelNotifyProps(),
- }
-
- if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
- return nil, cmresult.Err
- }
-
- InvalidateCacheForUser(c.Session.UserId)
- }
-
c.LogAudit("name=" + channel.Name)
-
- return sc, nil
+ w.Write([]byte(sc.ToJson()))
}
}
func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) {
- if !HasPermissionToContext(c, model.PERMISSION_CREATE_DIRECT_CHANNEL) {
+ if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_CREATE_DIRECT_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_CREATE_DIRECT_CHANNEL)
return
}
@@ -143,7 +120,7 @@ func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if sc, err := CreateDirectChannel(c.Session.UserId, userId); err != nil {
+ if sc, err := app.CreateDirectChannel(c.Session.UserId, userId); err != nil {
c.Err = err
return
} else {
@@ -151,56 +128,14 @@ func createDirectChannel(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
-func CreateDirectChannel(userId string, otherUserId string) (*model.Channel, *model.AppError) {
- uc := Srv.Store.User().Get(otherUserId)
-
- if uresult := <-uc; uresult.Err != nil {
- return nil, model.NewLocAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, otherUserId)
- }
-
- if result := <-Srv.Store.Channel().CreateDirectChannel(userId, otherUserId); result.Err != nil {
- if result.Err.Id == store.CHANNEL_EXISTS_ERROR {
- return result.Data.(*model.Channel), nil
- } else {
- return nil, result.Err
- }
- } else {
- channel := result.Data.(*model.Channel)
-
- InvalidateCacheForUser(userId)
- InvalidateCacheForUser(otherUserId)
-
- message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_DIRECT_ADDED, "", channel.Id, "", nil)
- message.Add("teammate_id", otherUserId)
- go Publish(message)
-
- return channel, nil
- }
-}
-
-func CreateDefaultChannels(c *Context, teamId string) ([]*model.Channel, *model.AppError) {
- townSquare := &model.Channel{DisplayName: c.T("api.channel.create_default_channels.town_square"), Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: teamId}
-
- if _, err := CreateChannel(c, townSquare, false); err != nil {
- return nil, err
- }
-
- offTopic := &model.Channel{DisplayName: c.T("api.channel.create_default_channels.off_topic"), Name: "off-topic", Type: model.CHANNEL_OPEN, TeamId: teamId}
-
- if _, err := CreateChannel(c, offTopic, false); err != nil {
- return nil, err
- }
-
- channels := []*model.Channel{townSquare, offTopic}
- return channels, nil
-}
-
func CanManageChannel(c *Context, channel *model.Channel) bool {
- if channel.Type == model.CHANNEL_OPEN && !HasPermissionToChannelContext(c, channel.Id, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES) {
+ if channel.Type == model.CHANNEL_OPEN && !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES)
return false
}
- if channel.Type == model.CHANNEL_PRIVATE && !HasPermissionToChannelContext(c, channel.Id, model.PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES) {
+ if channel.Type == model.CHANNEL_PRIVATE && !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES)
return false
}
@@ -216,66 +151,66 @@ func updateChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- sc := Srv.Store.Channel().Get(channel.Id, true)
- cmc := Srv.Store.Channel().GetMember(channel.Id, c.Session.UserId)
+ var oldChannel *model.Channel
+ var err *model.AppError
+ if oldChannel, err = app.GetChannel(channel.Id); err != nil {
+ c.Err = err
+ return
+ }
- if cresult := <-sc; cresult.Err != nil {
- c.Err = cresult.Err
+ if _, err = app.GetChannelMember(channel.Id, c.Session.UserId); err != nil {
+ c.Err = err
return
- } else if cmcresult := <-cmc; cmcresult.Err != nil {
- c.Err = cmcresult.Err
+ }
+
+ if !CanManageChannel(c, channel) {
return
- } else {
- oldChannel := cresult.Data.(*model.Channel)
- // Don't need to do anything with channel member, just wanted to confirm it exists
+ }
- if !CanManageChannel(c, channel) {
- return
- }
+ if oldChannel.DeleteAt > 0 {
+ c.Err = model.NewLocAppError("updateChannel", "api.channel.update_channel.deleted.app_error", nil, "")
+ c.Err.StatusCode = http.StatusBadRequest
+ return
+ }
- if oldChannel.DeleteAt > 0 {
- c.Err = model.NewLocAppError("updateChannel", "api.channel.update_channel.deleted.app_error", nil, "")
+ if oldChannel.Name == model.DEFAULT_CHANNEL {
+ if (len(channel.Name) > 0 && channel.Name != oldChannel.Name) || (len(channel.Type) > 0 && channel.Type != oldChannel.Type) {
+ c.Err = model.NewLocAppError("updateChannel", "api.channel.update_channel.tried.app_error", map[string]interface{}{"Channel": model.DEFAULT_CHANNEL}, "")
c.Err.StatusCode = http.StatusBadRequest
return
}
+ }
- if oldChannel.Name == model.DEFAULT_CHANNEL {
- if (len(channel.Name) > 0 && channel.Name != oldChannel.Name) || (len(channel.Type) > 0 && channel.Type != oldChannel.Type) {
- c.Err = model.NewLocAppError("updateChannel", "api.channel.update_channel.tried.app_error", map[string]interface{}{"Channel": model.DEFAULT_CHANNEL}, "")
- c.Err.StatusCode = http.StatusBadRequest
- return
- }
- }
-
- oldChannel.Header = channel.Header
- oldChannel.Purpose = channel.Purpose
+ oldChannel.Header = channel.Header
+ oldChannel.Purpose = channel.Purpose
- oldChannelDisplayName := oldChannel.DisplayName
+ oldChannelDisplayName := oldChannel.DisplayName
- if len(channel.DisplayName) > 0 {
- oldChannel.DisplayName = channel.DisplayName
- }
+ if len(channel.DisplayName) > 0 {
+ oldChannel.DisplayName = channel.DisplayName
+ }
- if len(channel.Name) > 0 {
- oldChannel.Name = channel.Name
- }
+ if len(channel.Name) > 0 {
+ oldChannel.Name = channel.Name
+ }
- if len(channel.Type) > 0 {
- oldChannel.Type = channel.Type
- }
+ if len(channel.Type) > 0 {
+ oldChannel.Type = channel.Type
+ }
- InvalidateCacheForChannel(oldChannel.Id)
- if ucresult := <-Srv.Store.Channel().Update(oldChannel); ucresult.Err != nil {
- c.Err = ucresult.Err
- return
- } else {
- if oldChannelDisplayName != channel.DisplayName {
- go PostUpdateChannelDisplayNameMessage(c, channel.Id, oldChannelDisplayName, channel.DisplayName)
+ if _, err := app.UpdateChannel(oldChannel); err != nil {
+ c.Err = err
+ return
+ } else {
+ if oldChannelDisplayName != channel.DisplayName {
+ if err := app.PostUpdateChannelDisplayNameMessage(c.Session.UserId, channel.Id, c.TeamId, oldChannelDisplayName, channel.DisplayName); err != nil {
+ l4g.Error(err.Error())
}
- c.LogAudit("name=" + channel.Name)
- w.Write([]byte(oldChannel.ToJson()))
}
+ c.LogAudit("name=" + channel.Name)
+ w.Write([]byte(oldChannel.ToJson()))
}
+
}
func updateChannelHeader(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -293,102 +228,39 @@ func updateChannelHeader(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- sc := Srv.Store.Channel().Get(channelId, true)
- cmc := Srv.Store.Channel().GetMember(channelId, c.Session.UserId)
-
- if cresult := <-sc; cresult.Err != nil {
- c.Err = cresult.Err
- return
- } else if cmcresult := <-cmc; cmcresult.Err != nil {
- c.Err = cmcresult.Err
+ var channel *model.Channel
+ var err *model.AppError
+ if channel, err = app.GetChannel(channelId); err != nil {
+ c.Err = err
return
- } else {
- channel := cresult.Data.(*model.Channel)
- // Don't need to do anything with channel member, just wanted to confirm it exists
-
- if !CanManageChannel(c, channel) {
- return
- }
-
- oldChannelHeader := channel.Header
- channel.Header = channelHeader
-
- InvalidateCacheForChannel(channel.Id)
- if ucresult := <-Srv.Store.Channel().Update(channel); ucresult.Err != nil {
- c.Err = ucresult.Err
- return
- } else {
- go PostUpdateChannelHeaderMessage(c, channel.Id, oldChannelHeader, channelHeader)
- c.LogAudit("name=" + channel.Name)
- w.Write([]byte(channel.ToJson()))
- }
}
-}
-
-func PostUpdateChannelHeaderMessage(c *Context, channelId string, oldChannelHeader, newChannelHeader string) {
- uc := Srv.Store.User().Get(c.Session.UserId)
- if uresult := <-uc; uresult.Err != nil {
- l4g.Error(utils.T("api.channel.post_update_channel_header_message_and_forget.retrieve_user.error"), uresult.Err)
+ if _, err = app.GetChannelMember(channelId, c.Session.UserId); err != nil {
+ c.Err = err
return
- } else {
- user := uresult.Data.(*model.User)
-
- var message string
- if oldChannelHeader == "" {
- message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.updated_to"), user.Username, newChannelHeader)
- } else if newChannelHeader == "" {
- message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.removed"), user.Username, oldChannelHeader)
- } else {
- message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.updated_from"), user.Username, oldChannelHeader, newChannelHeader)
- }
-
- post := &model.Post{
- ChannelId: channelId,
- Message: message,
- Type: model.POST_HEADER_CHANGE,
- UserId: c.Session.UserId,
- Props: model.StringInterface{
- "old_header": oldChannelHeader,
- "new_header": newChannelHeader,
- },
- }
+ }
- if _, err := CreatePost(c, post, false); err != nil {
- l4g.Error(utils.T("api.channel.post_update_channel_header_message_and_forget.join_leave.error"), err)
- }
+ if !CanManageChannel(c, channel) {
+ return
}
-}
-func PostUpdateChannelDisplayNameMessage(c *Context, channelId string, oldChannelDisplayName, newChannelDisplayName string) {
- uc := Srv.Store.User().Get(c.Session.UserId)
+ oldChannelHeader := channel.Header
+ channel.Header = channelHeader
- if uresult := <-uc; uresult.Err != nil {
- l4g.Error(utils.T("api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error"), uresult.Err)
+ if _, err := app.UpdateChannel(channel); err != nil {
+ c.Err = err
return
} else {
- user := uresult.Data.(*model.User)
-
- message := fmt.Sprintf(utils.T("api.channel.post_update_channel_displayname_message_and_forget.updated_from"), user.Username, oldChannelDisplayName, newChannelDisplayName)
-
- post := &model.Post{
- ChannelId: channelId,
- Message: message,
- Type: model.POST_DISPLAYNAME_CHANGE,
- UserId: c.Session.UserId,
- Props: model.StringInterface{
- "old_displayname": oldChannelDisplayName,
- "new_displayname": newChannelDisplayName,
- },
- }
-
- if _, err := CreatePost(c, post, false); err != nil {
- l4g.Error(utils.T("api.channel.post_update_channel_displayname_message_and_forget.create_post.error"), err)
+ if err := app.PostUpdateChannelHeaderMessage(c.Session.UserId, channel.Id, c.TeamId, oldChannelHeader, channelHeader); err != nil {
+ l4g.Error(err.Error())
}
+ c.LogAudit("name=" + channel.Name)
+ w.Write([]byte(channel.ToJson()))
}
}
func updateChannelPurpose(c *Context, w http.ResponseWriter, r *http.Request) {
+
props := model.MapFromJson(r.Body)
channelId := props["channel_id"]
if len(channelId) != 26 {
@@ -402,33 +274,34 @@ func updateChannelPurpose(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- sc := Srv.Store.Channel().Get(channelId, true)
- cmc := Srv.Store.Channel().GetMember(channelId, c.Session.UserId)
-
- if cresult := <-sc; cresult.Err != nil {
- c.Err = cresult.Err
+ var channel *model.Channel
+ var err *model.AppError
+ if channel, err = app.GetChannel(channelId); err != nil {
+ c.Err = err
return
- } else if cmcresult := <-cmc; cmcresult.Err != nil {
- c.Err = cmcresult.Err
+ }
+
+ if _, err = app.GetChannelMember(channelId, c.Session.UserId); err != nil {
+ c.Err = err
return
- } else {
- channel := cresult.Data.(*model.Channel)
- // Don't need to do anything with channel member, just wanted to confirm it exists
+ }
- if !CanManageChannel(c, channel) {
- return
- }
+ if !CanManageChannel(c, channel) {
+ return
+ }
- channel.Purpose = channelPurpose
+ oldChannelPurpose := channel.Purpose
+ channel.Purpose = channelPurpose
- InvalidateCacheForChannel(channel.Id)
- if ucresult := <-Srv.Store.Channel().Update(channel); ucresult.Err != nil {
- c.Err = ucresult.Err
- return
- } else {
- c.LogAudit("name=" + channel.Name)
- w.Write([]byte(channel.ToJson()))
+ if _, err := app.UpdateChannel(channel); err != nil {
+ c.Err = err
+ return
+ } else {
+ if err := app.PostUpdateChannelPurposeMessage(c.Session.UserId, channel.Id, c.TeamId, oldChannelPurpose, channelPurpose); err != nil {
+ l4g.Error(err.Error())
}
+ c.LogAudit("name=" + channel.Name)
+ w.Write([]byte(channel.ToJson()))
}
}
@@ -440,24 +313,24 @@ func getChannels(c *Context, w http.ResponseWriter, r *http.Request) {
}
// user is already in the team
// Get's all channels the user is a member of
- if result := <-Srv.Store.Channel().GetChannels(c.TeamId, c.Session.UserId); result.Err != nil {
- if result.Err.Id == "store.sql_channel.get_channels.not_found.app_error" {
+
+ if channels, err := app.GetChannelsForUser(c.TeamId, c.Session.UserId); err != nil {
+ if err.Id == "store.sql_channel.get_channels.not_found.app_error" {
// lets make sure the user is valid
- if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
- c.Err = result.Err
+ if _, err := app.GetUser(c.Session.UserId); err != nil {
+ c.Err = err
c.RemoveSessionCookie(w, r)
l4g.Error(utils.T("api.channel.get_channels.error"), c.Session.UserId)
return
}
}
- c.Err = result.Err
+ c.Err = err
return
- } else if HandleEtag(result.Data.(*model.ChannelList).Etag(), "Get Channels", w, r) {
+ } else if HandleEtag(channels.Etag(), "Get Channels", w, r) {
return
} else {
- data := result.Data.(*model.ChannelList)
- w.Header().Set(model.HEADER_ETAG_SERVER, data.Etag())
- w.Write([]byte(data.ToJson()))
+ w.Header().Set(model.HEADER_ETAG_SERVER, channels.Etag())
+ w.Write([]byte(channels.ToJson()))
}
}
@@ -477,17 +350,17 @@ func getMoreChannelsPage(c *Context, w http.ResponseWriter, r *http.Request) {
}
// user is already in the team
- if !HasPermissionToTeamContext(c, c.TeamId, model.PERMISSION_LIST_TEAM_CHANNELS) {
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_LIST_TEAM_CHANNELS) {
+ c.SetPermissionError(model.PERMISSION_LIST_TEAM_CHANNELS)
return
}
- if result := <-Srv.Store.Channel().GetMoreChannels(c.TeamId, c.Session.UserId, offset, limit); result.Err != nil {
- c.Err = result.Err
+ if channels, err := app.GetChannelsUserNotIn(c.TeamId, c.Session.UserId, offset, limit); err != nil {
+ c.Err = err
return
} else {
- data := result.Data.(*model.ChannelList)
- w.Header().Set(model.HEADER_ETAG_SERVER, data.Etag())
- w.Write([]byte(data.ToJson()))
+ w.Header().Set(model.HEADER_ETAG_SERVER, channels.Etag())
+ w.Write([]byte(channels.ToJson()))
}
}
@@ -495,15 +368,14 @@ func getChannelCounts(c *Context, w http.ResponseWriter, r *http.Request) {
// user is already in the team
- if result := <-Srv.Store.Channel().GetChannelCounts(c.TeamId, c.Session.UserId); result.Err != nil {
- c.Err = model.NewLocAppError("getChannelCounts", "api.channel.get_channel_counts.app_error", nil, result.Err.Message)
+ if counts, err := app.GetChannelCounts(c.TeamId, c.Session.UserId); err != nil {
+ c.Err = model.NewLocAppError("getChannelCounts", "api.channel.get_channel_counts.app_error", nil, err.Message)
return
- } else if HandleEtag(result.Data.(*model.ChannelCounts).Etag(), "Get Channel Counts", w, r) {
+ } else if HandleEtag(counts.Etag(), "Get Channel Counts", w, r) {
return
} else {
- data := result.Data.(*model.ChannelCounts)
- w.Header().Set(model.HEADER_ETAG_SERVER, data.Etag())
- w.Write([]byte(data.ToJson()))
+ w.Header().Set(model.HEADER_ETAG_SERVER, counts.Etag())
+ w.Write([]byte(counts.ToJson()))
}
}
@@ -513,200 +385,35 @@ func join(c *Context, w http.ResponseWriter, r *http.Request) {
channelId := params["channel_id"]
channelName := params["channel_name"]
- var outChannel *model.Channel = nil
+ var channel *model.Channel
+ var err *model.AppError
if channelId != "" {
- if err, channel := JoinChannelById(c, c.Session.UserId, channelId); err != nil {
- c.Err = err
- c.Err.StatusCode = http.StatusForbidden
- return
- } else {
- outChannel = channel
- }
+ channel, err = app.GetChannel(channelId)
} else if channelName != "" {
- if err, channel := JoinChannelByName(c, c.Session.UserId, c.TeamId, channelName); err != nil {
- c.Err = err
- c.Err.StatusCode = http.StatusForbidden
- return
- } else {
- outChannel = channel
- }
+ channel, err = app.GetChannelByName(channelName, c.TeamId)
} else {
c.SetInvalidParam("join", "channel_id, channel_name")
return
}
- w.Write([]byte(outChannel.ToJson()))
-}
-
-func JoinChannelByName(c *Context, userId string, teamId string, channelName string) (*model.AppError, *model.Channel) {
- channelChannel := Srv.Store.Channel().GetByName(teamId, channelName)
- userChannel := Srv.Store.User().Get(userId)
-
- return joinChannel(c, channelChannel, userChannel)
-}
-
-func JoinChannelById(c *Context, userId string, channelId string) (*model.AppError, *model.Channel) {
- channelChannel := Srv.Store.Channel().Get(channelId, true)
- userChannel := Srv.Store.User().Get(userId)
-
- return joinChannel(c, channelChannel, userChannel)
-}
-
-func joinChannel(c *Context, channelChannel store.StoreChannel, userChannel store.StoreChannel) (*model.AppError, *model.Channel) {
- if cresult := <-channelChannel; cresult.Err != nil {
- return cresult.Err, nil
- } else if uresult := <-userChannel; uresult.Err != nil {
- return uresult.Err, nil
- } else {
- channel := cresult.Data.(*model.Channel)
- user := uresult.Data.(*model.User)
-
- if mresult := <-Srv.Store.Channel().GetMember(channel.Id, user.Id); mresult.Err == nil && mresult.Data != nil {
- // the user is already in the channel so just return successful
- return nil, channel
- }
-
- if !HasPermissionToTeamContext(c, channel.TeamId, model.PERMISSION_JOIN_PUBLIC_CHANNELS) {
- return c.Err, nil
- }
-
- if channel.Type == model.CHANNEL_OPEN {
- if _, err := AddUserToChannel(user, channel); err != nil {
- return err, nil
- }
- go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.join_channel.post_and_forget"), user.Username), model.POST_JOIN_LEAVE)
- } else {
- return model.NewLocAppError("join", "api.channel.join_channel.permissions.app_error", nil, ""), nil
- }
- return nil, channel
- }
-}
-
-func PostUserAddRemoveMessage(c *Context, channelId string, message, postType string) {
- post := &model.Post{
- ChannelId: channelId,
- Message: message,
- Type: postType,
- UserId: c.Session.UserId,
- }
- if _, err := CreatePost(c, post, false); err != nil {
- l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err)
- }
-}
-
-func AddUserToChannel(user *model.User, channel *model.Channel) (*model.ChannelMember, *model.AppError) {
- if channel.DeleteAt > 0 {
- return nil, model.NewLocAppError("AddUserToChannel", "api.channel.add_user_to_channel.deleted.app_error", nil, "")
- }
- if channel.Type != model.CHANNEL_OPEN && channel.Type != model.CHANNEL_PRIVATE {
- return nil, model.NewLocAppError("AddUserToChannel", "api.channel.add_user_to_channel.type.app_error", nil, "")
- }
-
- tmchan := Srv.Store.Team().GetMember(channel.TeamId, user.Id)
- cmchan := Srv.Store.Channel().GetMember(channel.Id, user.Id)
-
- if result := <-tmchan; result.Err != nil {
- return nil, result.Err
- } else {
- teamMember := result.Data.(model.TeamMember)
- if teamMember.DeleteAt > 0 {
- return nil, model.NewLocAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.deleted.app_error", nil, "")
- }
- }
-
- if result := <-cmchan; result.Err != nil {
- if result.Err.Id != store.MISSING_CHANNEL_MEMBER_ERROR {
- return nil, result.Err
- }
- } else {
- channelMember := result.Data.(model.ChannelMember)
- return &channelMember, nil
- }
-
- newMember := &model.ChannelMember{
- ChannelId: channel.Id,
- UserId: user.Id,
- NotifyProps: model.GetDefaultChannelNotifyProps(),
- Roles: model.ROLE_CHANNEL_USER.Id,
- }
- if result := <-Srv.Store.Channel().SaveMember(newMember); result.Err != nil {
- l4g.Error("Failed to add member user_id=%v channel_id=%v err=%v", user.Id, channel.Id, result.Err)
- return nil, model.NewLocAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.app_error", nil, "")
- }
-
- InvalidateCacheForUser(user.Id)
- InvalidateCacheForChannel(channel.Id)
-
- message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_ADDED, "", channel.Id, "", nil)
- message.Add("user_id", user.Id)
- message.Add("team_id", channel.TeamId)
- go Publish(message)
-
- return newMember, nil
-}
-
-func JoinDefaultChannels(teamId string, user *model.User, channelRole string) *model.AppError {
- // We don't call JoinChannel here since c.Session is not populated on user creation
-
- var err *model.AppError = nil
-
- fakeContext := &Context{
- Session: model.Session{
- UserId: user.Id,
- },
- TeamId: teamId,
- T: utils.TfuncWithFallback(user.Locale),
+ if err != nil {
+ c.Err = err
+ return
}
- if result := <-Srv.Store.Channel().GetByName(teamId, "town-square"); result.Err != nil {
- err = result.Err
- } else {
- cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id,
- Roles: channelRole, NotifyProps: model.GetDefaultChannelNotifyProps()}
-
- if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil {
- err = cmResult.Err
- }
-
- post := &model.Post{
- ChannelId: result.Data.(*model.Channel).Id,
- Message: fmt.Sprintf(utils.T("api.channel.join_channel.post_and_forget"), user.Username),
- Type: model.POST_JOIN_LEAVE,
- UserId: user.Id,
- }
-
- InvalidateCacheForChannel(result.Data.(*model.Channel).Id)
-
- if _, err := CreatePost(fakeContext, post, false); err != nil {
- l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err)
+ if channel.Type == model.CHANNEL_OPEN {
+ if !app.SessionHasPermissionToTeam(c.Session, channel.TeamId, model.PERMISSION_JOIN_PUBLIC_CHANNELS) {
+ c.SetPermissionError(model.PERMISSION_JOIN_PUBLIC_CHANNELS)
+ return
}
}
- if result := <-Srv.Store.Channel().GetByName(teamId, "off-topic"); result.Err != nil {
- err = result.Err
- } else {
- cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id,
- Roles: channelRole, NotifyProps: model.GetDefaultChannelNotifyProps()}
-
- if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil {
- err = cmResult.Err
- }
-
- post := &model.Post{
- ChannelId: result.Data.(*model.Channel).Id,
- Message: fmt.Sprintf(utils.T("api.channel.join_channel.post_and_forget"), user.Username),
- Type: model.POST_JOIN_LEAVE,
- UserId: user.Id,
- }
-
- InvalidateCacheForChannel(result.Data.(*model.Channel).Id)
-
- if _, err := CreatePost(fakeContext, post, false); err != nil {
- l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err)
- }
+ if err = app.JoinChannel(channel, c.Session.UserId); err != nil {
+ c.Err = err
+ return
}
- return err
+ w.Write([]byte(channel.ToJson()))
}
func leave(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -714,55 +421,15 @@ func leave(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["channel_id"]
- sc := Srv.Store.Channel().Get(id, true)
- uc := Srv.Store.User().Get(c.Session.UserId)
- ccm := Srv.Store.Channel().GetMemberCount(id, false)
-
- if cresult := <-sc; cresult.Err != nil {
- c.Err = cresult.Err
- return
- } else if uresult := <-uc; uresult.Err != nil {
- c.Err = cresult.Err
- return
- } else if ccmresult := <-ccm; ccmresult.Err != nil {
- c.Err = ccmresult.Err
+ err := app.LeaveChannel(id, c.Session.UserId)
+ if err != nil {
+ c.Err = err
return
- } else {
- channel := cresult.Data.(*model.Channel)
- user := uresult.Data.(*model.User)
- membersCount := ccmresult.Data.(int64)
-
- if channel.Type == model.CHANNEL_DIRECT {
- c.Err = model.NewLocAppError("leave", "api.channel.leave.direct.app_error", nil, "")
- c.Err.StatusCode = http.StatusBadRequest
- return
- }
-
- if channel.Type == model.CHANNEL_PRIVATE && membersCount == 1 {
- c.Err = model.NewLocAppError("leave", "api.channel.leave.last_member.app_error", nil, "userId="+user.Id)
- c.Err.StatusCode = http.StatusBadRequest
- return
- }
-
- if channel.Name == model.DEFAULT_CHANNEL {
- c.Err = model.NewLocAppError("leave", "api.channel.leave.default.app_error", map[string]interface{}{"Channel": model.DEFAULT_CHANNEL}, "")
- c.Err.StatusCode = http.StatusBadRequest
- return
- }
-
- if cmresult := <-Srv.Store.Channel().RemoveMember(channel.Id, c.Session.UserId); cmresult.Err != nil {
- c.Err = cmresult.Err
- return
- }
-
- RemoveUserFromChannel(c.Session.UserId, c.Session.UserId, channel)
-
- go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.leave.left"), user.Username), model.POST_JOIN_LEAVE)
-
- result := make(map[string]string)
- result["id"] = channel.Id
- w.Write([]byte(model.MapToJson(result)))
}
+
+ result := make(map[string]string)
+ result["id"] = id
+ w.Write([]byte(model.MapToJson(result)))
}
func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -770,181 +437,102 @@ func deleteChannel(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["channel_id"]
- sc := Srv.Store.Channel().Get(id, true)
- scm := Srv.Store.Channel().GetMember(id, c.Session.UserId)
- cmc := Srv.Store.Channel().GetMemberCount(id, false)
- uc := Srv.Store.User().Get(c.Session.UserId)
- ihc := Srv.Store.Webhook().GetIncomingByChannel(id)
- ohc := Srv.Store.Webhook().GetOutgoingByChannel(id)
-
- if cresult := <-sc; cresult.Err != nil {
- c.Err = cresult.Err
- return
- } else if uresult := <-uc; uresult.Err != nil {
- c.Err = cresult.Err
- return
- } else if scmresult := <-scm; scmresult.Err != nil {
- c.Err = scmresult.Err
- return
- } else if cmcresult := <-cmc; cmcresult.Err != nil {
- c.Err = cmcresult.Err
- return
- } else if ihcresult := <-ihc; ihcresult.Err != nil {
- c.Err = ihcresult.Err
- return
- } else if ohcresult := <-ohc; ohcresult.Err != nil {
- c.Err = ohcresult.Err
+ var channel *model.Channel
+ var err *model.AppError
+ if channel, err = app.GetChannel(id); err != nil {
+ c.Err = err
return
- } else {
- channel := cresult.Data.(*model.Channel)
- memberCount := cmcresult.Data.(int64)
- user := uresult.Data.(*model.User)
- incomingHooks := ihcresult.Data.([]*model.IncomingWebhook)
- outgoingHooks := ohcresult.Data.([]*model.OutgoingWebhook)
- // Don't need to do anything with channel member, just wanted to confirm it exists
-
- // Allow delete if user is the only member left in channel
- if memberCount > 1 {
- if channel.Type == model.CHANNEL_OPEN && !HasPermissionToChannelContext(c, channel.Id, model.PERMISSION_DELETE_PUBLIC_CHANNEL) {
- return
- }
-
- if channel.Type == model.CHANNEL_PRIVATE && !HasPermissionToChannelContext(c, channel.Id, model.PERMISSION_DELETE_PRIVATE_CHANNEL) {
- return
- }
- }
+ }
- if channel.DeleteAt > 0 {
- c.Err = model.NewLocAppError("deleteChannel", "api.channel.delete_channel.deleted.app_error", nil, "")
- c.Err.StatusCode = http.StatusBadRequest
- return
- }
+ var memberCount int64
+ if memberCount, err = app.GetChannelMemberCount(id); err != nil {
+ c.Err = err
+ return
+ }
- if channel.Name == model.DEFAULT_CHANNEL {
- c.Err = model.NewLocAppError("deleteChannel", "api.channel.delete_channel.cannot.app_error", map[string]interface{}{"Channel": model.DEFAULT_CHANNEL}, "")
- c.Err.StatusCode = http.StatusBadRequest
+ // Allow delete if user is the only member left in channel
+ if memberCount > 1 {
+ if channel.Type == model.CHANNEL_OPEN && !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_DELETE_PUBLIC_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_DELETE_PUBLIC_CHANNEL)
return
}
- post := &model.Post{
- ChannelId: channel.Id,
- Message: fmt.Sprintf(c.T("api.channel.delete_channel.archived"), user.Username),
- Type: model.POST_CHANNEL_DELETED,
- UserId: c.Session.UserId,
- }
-
- if _, err := CreatePost(c, post, false); err != nil {
- l4g.Error(utils.T("api.channel.delete_channel.failed_post.error"), err)
- }
-
- now := model.GetMillis()
- for _, hook := range incomingHooks {
- if result := <-Srv.Store.Webhook().DeleteIncoming(hook.Id, now); result.Err != nil {
- l4g.Error(utils.T("api.channel.delete_channel.incoming_webhook.error"), hook.Id)
- }
- }
-
- for _, hook := range outgoingHooks {
- if result := <-Srv.Store.Webhook().DeleteOutgoing(hook.Id, now); result.Err != nil {
- l4g.Error(utils.T("api.channel.delete_channel.outgoing_webhook.error"), hook.Id)
- }
- }
-
- if dresult := <-Srv.Store.Channel().Delete(channel.Id, model.GetMillis()); dresult.Err != nil {
- c.Err = dresult.Err
+ if channel.Type == model.CHANNEL_PRIVATE && !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_DELETE_PRIVATE_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_DELETE_PRIVATE_CHANNEL)
return
}
- InvalidateCacheForChannel(channel.Id)
-
- c.LogAudit("name=" + channel.Name)
+ }
- message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_DELETED, c.TeamId, "", "", nil)
- message.Add("channel_id", channel.Id)
+ err = app.DeleteChannel(channel, c.Session.UserId)
+ if err != nil {
+ c.Err = err
+ return
+ }
- Publish(message)
+ c.LogAudit("name=" + channel.Name)
- result := make(map[string]string)
- result["id"] = channel.Id
- w.Write([]byte(model.MapToJson(result)))
- }
+ result := make(map[string]string)
+ result["id"] = channel.Id
+ w.Write([]byte(model.MapToJson(result)))
}
func getChannel(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["channel_id"]
- cchan := Srv.Store.Channel().Get(id, true)
- cmchan := Srv.Store.Channel().GetMember(id, c.Session.UserId)
-
- if cresult := <-cchan; cresult.Err != nil {
- c.Err = cresult.Err
- return
- } else if cmresult := <-cmchan; cmresult.Err != nil {
- c.Err = cmresult.Err
+ var channel *model.Channel
+ var err *model.AppError
+ if channel, err = app.GetChannel(id); err != nil {
+ c.Err = err
return
- } else {
- data := &model.ChannelData{}
- data.Channel = cresult.Data.(*model.Channel)
- member := cmresult.Data.(model.ChannelMember)
- data.Member = &member
-
- if data.Channel.TeamId != c.TeamId && data.Channel.Type != model.CHANNEL_DIRECT {
- c.Err = model.NewLocAppError("getChannel", "api.channel.get_channel.wrong_team.app_error", map[string]interface{}{"ChannelId": id, "TeamId": c.TeamId}, "")
- return
- }
+ }
- if HandleEtag(data.Etag(), "Get Channel", w, r) {
- return
- } else {
- w.Header().Set(model.HEADER_ETAG_SERVER, data.Etag())
- w.Write([]byte(data.ToJson()))
- }
+ if channel.TeamId != c.TeamId && channel.Type != model.CHANNEL_DIRECT {
+ c.Err = model.NewLocAppError("getChannel", "api.channel.get_channel.wrong_team.app_error", map[string]interface{}{"ChannelId": id, "TeamId": c.TeamId}, "")
+ return
}
-}
-func SetActiveChannel(userId string, channelId string) *model.AppError {
- status, err := GetStatus(userId)
- if err != nil {
- status = &model.Status{userId, model.STATUS_ONLINE, false, model.GetMillis(), channelId}
- } else {
- status.ActiveChannel = channelId
- if !status.Manual {
- status.Status = model.STATUS_ONLINE
- }
- status.LastActivityAt = model.GetMillis()
+ var member *model.ChannelMember
+ if member, err = app.GetChannelMember(id, c.Session.UserId); err != nil {
+ c.Err = err
+ return
}
- AddStatusCache(status)
+ data := &model.ChannelData{}
+ data.Channel = channel
+ data.Member = member
- return nil
+ if HandleEtag(data.Etag(), "Get Channel", w, r) {
+ return
+ } else {
+ w.Header().Set(model.HEADER_ETAG_SERVER, data.Etag())
+ w.Write([]byte(data.ToJson()))
+ }
}
func getChannelByName(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
channelName := params["channel_name"]
- cchan := Srv.Store.Channel().GetByName(c.TeamId, channelName)
-
- if cresult := <-cchan; cresult.Err != nil {
- c.Err = cresult.Err
+ if channel, err := app.GetChannelByName(channelName, c.TeamId); err != nil {
+ c.Err = err
return
} else {
- data := cresult.Data.(*model.Channel)
-
- if !HasPermissionToChannelContext(c, data.Id, model.PERMISSION_READ_CHANNEL) {
+ if !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_READ_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
- if data.TeamId != c.TeamId && data.Type != model.CHANNEL_DIRECT {
+ if channel.TeamId != c.TeamId && channel.Type != model.CHANNEL_DIRECT {
c.Err = model.NewLocAppError("getChannel", "api.channel.get_channel.wrong_team.app_error", map[string]interface{}{"ChannelName": channelName, "TeamId": c.TeamId}, "")
return
}
- if HandleEtag(data.Etag(), "Get Channel By Name", w, r) {
+ if HandleEtag(channel.Etag(), "Get Channel By Name", w, r) {
return
} else {
- w.Header().Set(model.HEADER_ETAG_SERVER, data.Etag())
- w.Write([]byte(data.ToJson()))
+ w.Header().Set(model.HEADER_ETAG_SERVER, channel.Etag())
+ w.Write([]byte(channel.ToJson()))
}
}
}
@@ -953,33 +541,30 @@ func getChannelStats(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["channel_id"]
- sc := Srv.Store.Channel().Get(id, true)
var channel *model.Channel
- if result := <-sc; result.Err != nil {
- c.Err = result.Err
+ var err *model.AppError
+ if channel, err = app.GetChannel(id); err != nil {
+ c.Err = err
return
- } else {
- channel = result.Data.(*model.Channel)
}
- if result := <-Srv.Store.Channel().GetMemberCount(id, true); result.Err != nil {
- c.Err = result.Err
+ if channel.DeleteAt > 0 {
+ c.Err = model.NewLocAppError("getChannelStats", "api.channel.get_channel_extra_info.deleted.app_error", nil, "")
+ c.Err.StatusCode = http.StatusBadRequest
return
- } else {
- memberCount := result.Data.(int64)
-
- if channel.DeleteAt > 0 {
- c.Err = model.NewLocAppError("getChannelStats", "api.channel.get_channel_extra_info.deleted.app_error", nil, "")
- c.Err.StatusCode = http.StatusBadRequest
- return
- }
+ }
- if !HasPermissionToChannelContext(c, channel.Id, model.PERMISSION_READ_CHANNEL) {
- return
- }
+ if !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_READ_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
+ return
+ }
- data := model.ChannelStats{ChannelId: channel.Id, MemberCount: memberCount}
- w.Write([]byte(data.ToJson()))
+ if memberCount, err := app.GetChannelMemberCount(id); err != nil {
+ c.Err = err
+ return
+ } else {
+ stats := model.ChannelStats{ChannelId: channel.Id, MemberCount: memberCount}
+ w.Write([]byte(stats.ToJson()))
}
}
@@ -988,26 +573,25 @@ func getChannelMember(c *Context, w http.ResponseWriter, r *http.Request) {
channelId := params["channel_id"]
userId := params["user_id"]
- if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
+ if !app.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_READ_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
- if result := <-Srv.Store.Channel().GetMember(channelId, userId); result.Err != nil {
- c.Err = result.Err
+ if member, err := app.GetChannelMember(channelId, userId); err != nil {
+ c.Err = err
return
} else {
- member := result.Data.(model.ChannelMember)
w.Write([]byte(member.ToJson()))
}
}
func getMyChannelMembers(c *Context, w http.ResponseWriter, r *http.Request) {
- if result := <-Srv.Store.Channel().GetMembersForUser(c.TeamId, c.Session.UserId); result.Err != nil {
- c.Err = result.Err
+ if members, err := app.GetChannelMembersForUser(c.TeamId, c.Session.UserId); err != nil {
+ c.Err = err
return
} else {
- data := result.Data.(*model.ChannelMembers)
- w.Write([]byte(data.ToJson()))
+ w.Write([]byte(members.ToJson()))
}
}
@@ -1023,47 +607,47 @@ func addMember(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- sc := Srv.Store.Channel().Get(id, true)
- ouc := Srv.Store.User().Get(c.Session.UserId)
- nuc := Srv.Store.User().Get(userId)
- if nresult := <-nuc; nresult.Err != nil {
- c.Err = model.NewLocAppError("addMember", "api.channel.add_member.find_user.app_error", nil, "")
+ var channel *model.Channel
+ var err *model.AppError
+ if channel, err = app.GetChannel(id); err != nil {
+ c.Err = err
return
- } else if cresult := <-sc; cresult.Err != nil {
- c.Err = model.NewLocAppError("addMember", "api.channel.add_member.find_channel.app_error", nil, "")
+ }
+
+ if channel.Type == model.CHANNEL_OPEN && !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS)
return
- } else {
- channel := cresult.Data.(*model.Channel)
- nUser := nresult.Data.(*model.User)
+ }
- if channel.Type == model.CHANNEL_OPEN && !HasPermissionToChannelContext(c, channel.Id, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS) {
- return
- }
+ if channel.Type == model.CHANNEL_PRIVATE && !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS)
+ return
+ }
- if channel.Type == model.CHANNEL_PRIVATE && !HasPermissionToChannelContext(c, channel.Id, model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS) {
- return
- }
+ var nUser *model.User
+ if nUser, err = app.GetUser(userId); err != nil {
+ c.Err = model.NewLocAppError("addMember", "api.channel.add_member.find_user.app_error", nil, err.Error())
+ return
+ }
- if oresult := <-ouc; oresult.Err != nil {
- c.Err = model.NewLocAppError("addMember", "api.channel.add_member.user_adding.app_error", nil, "")
- return
- } else {
- oUser := oresult.Data.(*model.User)
+ cm, err := app.AddUserToChannel(nUser, channel)
+ if err != nil {
+ c.Err = err
+ return
+ }
- cm, err := AddUserToChannel(nUser, channel)
- if err != nil {
- c.Err = err
- return
- }
+ c.LogAudit("name=" + channel.Name + " user_id=" + userId)
- c.LogAudit("name=" + channel.Name + " user_id=" + userId)
+ var oUser *model.User
+ if oUser, err = app.GetUser(c.Session.UserId); err != nil {
+ c.Err = model.NewLocAppError("addMember", "api.channel.add_member.user_adding.app_error", nil, err.Error())
+ return
+ }
- go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.add_member.added"), nUser.Username, oUser.Username), model.POST_ADD_REMOVE)
+ go app.PostUserAddRemoveMessage(c.Session.UserId, channel.Id, channel.TeamId, fmt.Sprintf(utils.T("api.channel.add_member.added"), nUser.Username, oUser.Username), model.POST_ADD_REMOVE)
- <-Srv.Store.Channel().UpdateLastViewedAt([]string{id}, oUser.Id)
- w.Write([]byte(cm.ToJson()))
- }
- }
+ app.UpdateChannelLastViewedAt([]string{id}, oUser.Id)
+ w.Write([]byte(cm.ToJson()))
}
func removeMember(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -1078,78 +662,47 @@ func removeMember(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- sc := Srv.Store.Channel().Get(channelId, true)
- cmc := Srv.Store.Channel().GetMember(channelId, c.Session.UserId)
- ouc := Srv.Store.User().Get(userIdToRemove)
-
- if oresult := <-ouc; oresult.Err != nil {
- c.Err = model.NewLocAppError("removeMember", "api.channel.remove_member.user.app_error", nil, "")
+ var channel *model.Channel
+ var err *model.AppError
+ if channel, err = app.GetChannel(channelId); err != nil {
+ c.Err = err
return
- } else {
- oUser := oresult.Data.(*model.User)
-
- if cresult := <-sc; cresult.Err != nil {
- c.Err = cresult.Err
- return
- } else if cmcresult := <-cmc; cmcresult.Err != nil {
- c.Err = cmcresult.Err
- return
- } else {
- channel := cresult.Data.(*model.Channel)
-
- if channel.Type == model.CHANNEL_OPEN && !HasPermissionToChannelContext(c, channel.Id, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS) {
- return
- }
-
- if channel.Type == model.CHANNEL_PRIVATE && !HasPermissionToChannelContext(c, channel.Id, model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS) {
- return
- }
-
- if err := RemoveUserFromChannel(userIdToRemove, c.Session.UserId, channel); err != nil {
- c.Err = model.NewLocAppError("updateChannel", "api.channel.remove_member.unable.app_error", nil, err.Message)
- return
- }
-
- c.LogAudit("name=" + channel.Name + " user_id=" + userIdToRemove)
-
- go PostUserAddRemoveMessage(c, channel.Id, fmt.Sprintf(utils.T("api.channel.remove_member.removed"), oUser.Username), model.POST_ADD_REMOVE)
+ }
- result := make(map[string]string)
- result["channel_id"] = channel.Id
- result["removed_user_id"] = userIdToRemove
- w.Write([]byte(model.MapToJson(result)))
- }
+ if channel.Type == model.CHANNEL_OPEN && !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS)
+ return
}
-}
-func RemoveUserFromChannel(userIdToRemove string, removerUserId string, channel *model.Channel) *model.AppError {
- if channel.DeleteAt > 0 {
- return model.NewLocAppError("RemoveUserFromChannel", "api.channel.remove_user_from_channel.deleted.app_error", nil, "")
+ if channel.Type == model.CHANNEL_PRIVATE && !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS)
+ return
}
- if channel.Name == model.DEFAULT_CHANNEL {
- return model.NewLocAppError("RemoveUserFromChannel", "api.channel.remove.default.app_error", map[string]interface{}{"Channel": model.DEFAULT_CHANNEL}, "")
+ if _, err = app.GetChannelMember(channel.Id, c.Session.UserId); err != nil {
+ c.Err = err
+ return
}
- if cmresult := <-Srv.Store.Channel().RemoveMember(channel.Id, userIdToRemove); cmresult.Err != nil {
- return cmresult.Err
+ if err = app.RemoveUserFromChannel(userIdToRemove, c.Session.UserId, channel); err != nil {
+ c.Err = model.NewLocAppError("removeMember", "api.channel.remove_member.unable.app_error", nil, err.Message)
+ return
}
- InvalidateCacheForUser(userIdToRemove)
- InvalidateCacheForChannel(channel.Id)
+ c.LogAudit("name=" + channel.Name + " user_id=" + userIdToRemove)
- message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_REMOVED, "", channel.Id, "", nil)
- message.Add("user_id", userIdToRemove)
- message.Add("remover_id", removerUserId)
- go Publish(message)
+ var user *model.User
+ if user, err = app.GetUser(userIdToRemove); err != nil {
+ c.Err = err
+ return
+ }
- // because the removed user no longer belongs to the channel we need to send a separate websocket event
- userMsg := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_REMOVED, "", "", userIdToRemove, nil)
- userMsg.Add("channel_id", channel.Id)
- userMsg.Add("remover_id", removerUserId)
- go Publish(userMsg)
+ go app.PostUserAddRemoveMessage(c.Session.UserId, channel.Id, channel.TeamId, fmt.Sprintf(utils.T("api.channel.remove_member.removed"), user.Username), model.POST_ADD_REMOVE)
- return nil
+ result := make(map[string]string)
+ result["channel_id"] = channel.Id
+ result["removed_user_id"] = userIdToRemove
+ w.Write([]byte(model.MapToJson(result)))
}
func updateNotifyProps(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -1157,47 +710,28 @@ func updateNotifyProps(c *Context, w http.ResponseWriter, r *http.Request) {
userId := data["user_id"]
if len(userId) != 26 {
- c.SetInvalidParam("updateMarkUnreadLevel", "user_id")
+ c.SetInvalidParam("updateNotifyProps", "user_id")
return
}
channelId := data["channel_id"]
if len(channelId) != 26 {
- c.SetInvalidParam("updateMarkUnreadLevel", "channel_id")
+ c.SetInvalidParam("updateNotifyProps", "channel_id")
return
}
- if !HasPermissionToUser(c, userId) {
+ if !app.SessionHasPermissionToUser(c.Session, userId) {
+ c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS)
return
}
- result := <-Srv.Store.Channel().GetMember(channelId, userId)
- if result.Err != nil {
- c.Err = result.Err
- return
- }
-
- member := result.Data.(model.ChannelMember)
-
- // update whichever notify properties have been provided, but don't change the others
- if markUnread, exists := data["mark_unread"]; exists {
- member.NotifyProps["mark_unread"] = markUnread
- }
-
- if desktop, exists := data["desktop"]; exists {
- member.NotifyProps["desktop"] = desktop
- }
-
- if result := <-Srv.Store.Channel().UpdateMember(&member); result.Err != nil {
- c.Err = result.Err
+ member, err := app.UpdateChannelMemberNotifyProps(data, channelId, userId)
+ if err != nil {
+ c.Err = err
return
- } else {
- InvalidateCacheForUser(userId)
-
- // return the updated notify properties including any unchanged ones
- w.Write([]byte(model.MapToJson(member.NotifyProps)))
}
+ w.Write([]byte(model.MapToJson(member.NotifyProps)))
}
func searchMoreChannels(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -1208,7 +742,8 @@ func searchMoreChannels(c *Context, w http.ResponseWriter, r *http.Request) {
}
if c.Session.GetTeamByTeamId(c.TeamId) == nil {
- if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
return
}
}
@@ -1218,11 +753,10 @@ func searchMoreChannels(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.Channel().SearchMore(c.Session.UserId, c.TeamId, props.Term); result.Err != nil {
- c.Err = result.Err
+ if channels, err := app.SearchChannelsUserNotIn(c.TeamId, c.Session.UserId, props.Term); err != nil {
+ c.Err = err
return
} else {
- channels := result.Data.(*model.ChannelList)
w.Write([]byte(channels.ToJson()))
}
}
@@ -1231,27 +765,25 @@ func autocompleteChannels(c *Context, w http.ResponseWriter, r *http.Request) {
term := r.URL.Query().Get("term")
if c.Session.GetTeamByTeamId(c.TeamId) == nil {
- if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
return
}
}
- var channels *model.ChannelList
-
- if result := <-Srv.Store.Channel().SearchInTeam(c.TeamId, term); result.Err != nil {
- c.Err = result.Err
+ if channels, err := app.SearchChannels(c.TeamId, term); err != nil {
+ c.Err = err
return
} else {
- channels = result.Data.(*model.ChannelList)
+ w.Write([]byte(channels.ToJson()))
}
- w.Write([]byte(channels.ToJson()))
}
func viewChannel(c *Context, w http.ResponseWriter, r *http.Request) {
view := model.ChannelViewFromJson(r.Body)
- if err := SetActiveChannel(c.Session.UserId, view.ChannelId); err != nil {
+ if err := app.SetActiveChannel(c.Session.UserId, view.ChannelId); err != nil {
c.Err = err
return
}
@@ -1261,39 +793,11 @@ func viewChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- channelIds := []string{view.ChannelId}
-
- var pchan store.StoreChannel
- if len(view.PrevChannelId) > 0 {
- channelIds = append(channelIds, view.PrevChannelId)
-
- if *utils.Cfg.EmailSettings.SendPushNotifications && !c.Session.IsMobileApp() {
- pchan = Srv.Store.User().GetUnreadCountForChannel(c.Session.UserId, view.ChannelId)
- }
- }
-
- uchan := Srv.Store.Channel().UpdateLastViewedAt(channelIds, c.Session.UserId)
-
- if pchan != nil {
- if result := <-pchan; result.Err != nil {
- c.Err = result.Err
- return
- } else {
- if result.Data.(int64) > 0 {
- clearPushNotification(c.Session.UserId, view.ChannelId)
- }
- }
- }
-
- if result := <-uchan; result.Err != nil {
- c.Err = result.Err
+ if err := app.ViewChannel(view, c.TeamId, c.Session.UserId, !c.Session.IsMobileApp()); err != nil {
+ c.Err = err
return
}
- message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_VIEWED, c.TeamId, "", c.Session.UserId, nil)
- message.Add("channel_id", view.ChannelId)
- go Publish(message)
-
ReturnStatusOK(w)
}
@@ -1307,16 +811,48 @@ func getChannelMembersByIds(c *Context, w http.ResponseWriter, r *http.Request)
return
}
- if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
+ if !app.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_READ_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
- if result := <-Srv.Store.Channel().GetMembersByIds(channelId, userIds); result.Err != nil {
- c.Err = result.Err
+ if members, err := app.GetChannelMembersByIds(channelId, userIds); err != nil {
+ c.Err = err
return
} else {
- members := result.Data.(model.ChannelMembers)
w.Write([]byte(members.ToJson()))
+ }
+}
+
+func updateChannelMemberRoles(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+ channelId := params["channel_id"]
+
+ props := model.MapFromJson(r.Body)
+
+ userId := props["user_id"]
+ if len(userId) != 26 {
+ c.SetInvalidParam("updateChannelMemberRoles", "user_id")
return
}
+
+ if !app.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_MANAGE_CHANNEL_ROLES) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_CHANNEL_ROLES)
+ return
+ }
+
+ newRoles := props["new_roles"]
+ if !(model.IsValidUserRoles(newRoles)) {
+ c.SetInvalidParam("updateChannelMemberRoles", "new_roles")
+ return
+ }
+
+ if _, err := app.UpdateChannelMemberRoles(channelId, userId, newRoles); err != nil {
+ c.Err = err
+ return
+ }
+
+ rdata := map[string]string{}
+ rdata["status"] = "ok"
+ w.Write([]byte(model.MapToJson(rdata)))
}
diff --git a/api/channel_test.go b/api/channel_test.go
index 25fd885ca..450c5556e 100644
--- a/api/channel_test.go
+++ b/api/channel_test.go
@@ -9,6 +9,7 @@ import (
"testing"
"time"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
@@ -19,7 +20,7 @@ func TestCreateChannel(t *testing.T) {
Client := th.BasicClient
SystemAdminClient := th.SystemAdminClient
team := th.BasicTeam
- Client.Must(Client.Logout())
+ th.LoginBasic2()
team2 := th.CreateTeam(th.BasicClient)
th.LoginBasic()
th.BasicClient.SetTeamId(team.Id)
@@ -125,6 +126,7 @@ func TestCreateChannel(t *testing.T) {
*utils.Cfg.TeamSettings.RestrictPrivateChannelCreation = model.PERMISSIONS_TEAM_ADMIN
utils.SetDefaultRolesBasedOnConfig()
+ th.LoginBasic2()
channel2.Name = "a" + model.NewId() + "a"
channel3.Name = "a" + model.NewId() + "a"
if _, err := Client.CreateChannel(channel2); err == nil {
@@ -629,6 +631,17 @@ func TestUpdateChannelPurpose(t *testing.T) {
upChannel1 = result.Data.(*model.Channel)
}
+ time.Sleep(100 * time.Millisecond)
+
+ r1 := Client.Must(Client.GetPosts(channel1.Id, 0, 1, "")).Data.(*model.PostList)
+ if len(r1.Order) != 1 {
+ t.Fatal("Purpose update system message was not found")
+ } else if val, ok := r1.Posts[r1.Order[0]].Props["old_purpose"]; !ok || val != "" {
+ t.Fatal("Props should contain old_header with old purpose value")
+ } else if val, ok := r1.Posts[r1.Order[0]].Props["new_purpose"]; !ok || val != "new purpose" {
+ t.Fatal("Props should contain new_header with new purpose value")
+ }
+
if upChannel1.Purpose != data["channel_purpose"] {
t.Fatal("Failed to update purpose")
}
@@ -1067,7 +1080,7 @@ func TestJoinChannelByNameDisabledUser(t *testing.T) {
Client.Must(th.BasicClient.RemoveUserFromTeam(th.BasicTeam.Id, th.BasicUser.Id))
- if _, err := AddUserToChannel(th.BasicUser, channel1); err == nil {
+ if _, err := app.AddUserToChannel(th.BasicUser, channel1); err == nil {
t.Fatal("shoudn't be able to join channel")
} else {
if err.Id != "api.channel.add_user.to.channel.failed.deleted.app_error" {
@@ -1832,7 +1845,7 @@ func TestGetChannelByName(t *testing.T) {
user2 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Jabba the Hutt", Password: "passwd1"}
user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
- store.Must(Srv.Store.User().VerifyEmail(user2.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user2.Id))
Client.SetTeamId(th.BasicTeam.Id)
@@ -1887,7 +1900,7 @@ func TestViewChannel(t *testing.T) {
func TestGetChannelMembersByIds(t *testing.T) {
th := Setup().InitBasic()
- if _, err := AddUserToChannel(th.BasicUser2, th.BasicChannel); err != nil {
+ if _, err := app.AddUserToChannel(th.BasicUser2, th.BasicChannel); err != nil {
t.Fatal("Could not add second user to channel")
}
@@ -1920,3 +1933,76 @@ func TestGetChannelMembersByIds(t *testing.T) {
t.Fatal("should have errored - empty user ids")
}
}
+
+func TestUpdateChannelRoles(t *testing.T) {
+ th := Setup().InitSystemAdmin().InitBasic()
+ th.SystemAdminClient.SetTeamId(th.BasicTeam.Id)
+ LinkUserToTeam(th.SystemAdminUser, th.BasicTeam)
+
+ const CHANNEL_ADMIN = "channel_admin channel_user"
+ const CHANNEL_MEMBER = "channel_user"
+
+ // User 1 creates a channel, making them channel admin by default.
+ createChannel := model.Channel{
+ DisplayName: "Test API Name",
+ Name: "a" + model.NewId() + "a",
+ Type: model.CHANNEL_OPEN,
+ TeamId: th.BasicTeam.Id,
+ }
+
+ rchannel, err := th.BasicClient.CreateChannel(&createChannel)
+ if err != nil {
+ t.Fatal("Failed to create channel:", err)
+ }
+ channel := rchannel.Data.(*model.Channel)
+
+ // User 1 adds User 2 to the channel, making them a channel member by default.
+ if _, err := th.BasicClient.AddChannelMember(channel.Id, th.BasicUser2.Id); err != nil {
+ t.Fatal("Failed to add user 2 to the channel:", err)
+ }
+
+ // System Admin can demote User 1 (channel admin).
+ if data, meta := th.SystemAdminClient.UpdateChannelRoles(channel.Id, th.BasicUser.Id, CHANNEL_MEMBER); data == nil {
+ t.Fatal("System Admin failed to demote channel admin to channel member:", meta)
+ }
+
+ // User 1 (channel_member) cannot promote user 2 (channel_member).
+ if data, meta := th.BasicClient.UpdateChannelRoles(channel.Id, th.BasicUser2.Id, CHANNEL_ADMIN); data != nil {
+ t.Fatal("Channel member should not be able to promote another channel member to channel admin:", meta)
+ }
+
+ // System Admin can promote user 1 (channel member).
+ if data, meta := th.SystemAdminClient.UpdateChannelRoles(channel.Id, th.BasicUser.Id, CHANNEL_ADMIN); data == nil {
+ t.Fatal("System Admin failed to promote channel member to channel admin:", meta)
+ }
+
+ // User 1 (channel_admin) can promote User 2 (channel member).
+ if data, meta := th.BasicClient.UpdateChannelRoles(channel.Id, th.BasicUser2.Id, CHANNEL_ADMIN); data == nil {
+ t.Fatal("Channel admin failed to promote channel member to channel admin:", meta)
+ }
+
+ // User 1 (channel admin) can demote User 2 (channel admin).
+ if data, meta := th.BasicClient.UpdateChannelRoles(channel.Id, th.BasicUser2.Id, CHANNEL_MEMBER); data == nil {
+ t.Fatal("Channel admin failed to demote channel admin to channel member:", meta)
+ }
+
+ // User 1 (channel admin) can demote itself.
+ if data, meta := th.BasicClient.UpdateChannelRoles(channel.Id, th.BasicUser.Id, CHANNEL_MEMBER); data == nil {
+ t.Fatal("Channel admin failed to demote itself to channel member:", meta)
+ }
+
+ // Promote User2 again for next test.
+ if data, meta := th.SystemAdminClient.UpdateChannelRoles(channel.Id, th.BasicUser2.Id, CHANNEL_ADMIN); data == nil {
+ t.Fatal("System Admin failed to promote channel member to channel admin:", meta)
+ }
+
+ // User 1 (channel member) cannot demote user 2 (channel admin).
+ if data, meta := th.BasicClient.UpdateChannelRoles(channel.Id, th.BasicUser2.Id, CHANNEL_MEMBER); data != nil {
+ t.Fatal("Channel member should not be able to demote another channel admin to channel member:", meta)
+ }
+
+ // User 1 (channel member) cannot promote itself.
+ if data, meta := th.BasicClient.UpdateChannelRoles(channel.Id, th.BasicUser.Id, CHANNEL_ADMIN); data != nil {
+ t.Fatal("Channel member should not be able to promote itself to channel admin:", meta)
+ }
+}
diff --git a/api/cli_test.go b/api/cli_test.go
index ed8f764a3..1f60b02cd 100644
--- a/api/cli_test.go
+++ b/api/cli_test.go
@@ -7,6 +7,7 @@ import (
"os/exec"
"testing"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
)
@@ -108,7 +109,7 @@ func TestCliCreateUserWithoutTeam(t *testing.T) {
t.Fatal(err)
}
- if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil {
+ if result := <-app.Srv.Store.User().GetByEmail(email); result.Err != nil {
t.Fatal()
} else {
user := result.Data.(*model.User)
@@ -132,7 +133,7 @@ func TestCliAssignRole(t *testing.T) {
t.Fatal(err)
}
- if result := <-Srv.Store.User().GetByEmail(th.BasicUser.Email); result.Err != nil {
+ if result := <-app.Srv.Store.User().GetByEmail(th.BasicUser.Email); result.Err != nil {
t.Fatal()
} else {
user := result.Data.(*model.User)
@@ -370,7 +371,7 @@ func TestCliLeaveTeam(t *testing.T) {
t.Fatal("profile should not be on team")
}
- if result := <-Srv.Store.Team().GetTeamsByUserId(th.BasicUser.Id); result.Err != nil {
+ if result := <-app.Srv.Store.Team().GetTeamsByUserId(th.BasicUser.Id); result.Err != nil {
teamMembers := result.Data.([]*model.TeamMember)
if len(teamMembers) > 0 {
t.Fatal("Shouldn't be in team")
diff --git a/api/command.go b/api/command.go
index 9c8f60be5..7e0a1e232 100644
--- a/api/command.go
+++ b/api/command.go
@@ -13,6 +13,7 @@ import (
l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
@@ -69,7 +70,7 @@ func listCommands(c *Context, w http.ResponseWriter, r *http.Request) {
}
if *utils.Cfg.ServiceSettings.EnableCommands {
- if result := <-Srv.Store.Command().GetByTeam(c.TeamId); result.Err != nil {
+ if result := <-app.Srv.Store.Command().GetByTeam(c.TeamId); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -96,7 +97,8 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) {
}
if len(commandArgs.ChannelId) > 0 {
- if !HasPermissionToChannelContext(c, commandArgs.ChannelId, model.PERMISSION_USE_SLASH_COMMANDS) {
+ if !app.SessionHasPermissionToChannel(c.Session, commandArgs.ChannelId, model.PERMISSION_USE_SLASH_COMMANDS) {
+ c.SetPermissionError(model.PERMISSION_USE_SLASH_COMMANDS)
return
}
}
@@ -119,11 +121,11 @@ func executeCommand(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- chanChan := Srv.Store.Channel().Get(commandArgs.ChannelId, true)
- teamChan := Srv.Store.Team().Get(c.TeamId)
- userChan := Srv.Store.User().Get(c.Session.UserId)
+ chanChan := app.Srv.Store.Channel().Get(commandArgs.ChannelId, true)
+ teamChan := app.Srv.Store.Team().Get(c.TeamId)
+ userChan := app.Srv.Store.User().Get(c.Session.UserId)
- if result := <-Srv.Store.Command().GetByTeam(c.TeamId); result.Err != nil {
+ if result := <-app.Srv.Store.Command().GetByTeam(c.TeamId); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -221,6 +223,7 @@ func handleResponse(c *Context, w http.ResponseWriter, response *model.CommandRe
post.ChannelId = commandArgs.ChannelId
post.RootId = commandArgs.RootId
post.ParentId = commandArgs.ParentId
+ post.UserId = c.Session.UserId
if !builtIn {
post.AddProp("from_webhook", "true")
@@ -231,8 +234,6 @@ func handleResponse(c *Context, w http.ResponseWriter, response *model.CommandRe
post.AddProp("override_username", cmd.Username)
} else if len(response.Username) != 0 {
post.AddProp("override_username", response.Username)
- } else {
- post.AddProp("override_username", model.DEFAULT_WEBHOOK_USERNAME)
}
}
@@ -246,7 +247,9 @@ func handleResponse(c *Context, w http.ResponseWriter, response *model.CommandRe
}
}
- CreateCommandPost(c, post, response)
+ if _, err := app.CreateCommandPost(post, c.TeamId, response); err != nil {
+ l4g.Error(err.Error())
+ }
w.Write([]byte(response.ToJson()))
}
@@ -258,7 +261,7 @@ func createCommand(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_SLASH_COMMANDS) {
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_SLASH_COMMANDS) {
c.Err = model.NewLocAppError("createCommand", "api.command.admin_only.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
return
@@ -277,7 +280,7 @@ func createCommand(c *Context, w http.ResponseWriter, r *http.Request) {
cmd.CreatorId = c.Session.UserId
cmd.TeamId = c.TeamId
- if result := <-Srv.Store.Command().GetByTeam(c.TeamId); result.Err != nil {
+ if result := <-app.Srv.Store.Command().GetByTeam(c.TeamId); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -297,7 +300,7 @@ func createCommand(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
- if result := <-Srv.Store.Command().Save(cmd); result.Err != nil {
+ if result := <-app.Srv.Store.Command().Save(cmd); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -314,7 +317,7 @@ func updateCommand(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_SLASH_COMMANDS) {
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_SLASH_COMMANDS) {
c.Err = model.NewLocAppError("updateCommand", "api.command.admin_only.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
return
@@ -332,13 +335,13 @@ func updateCommand(c *Context, w http.ResponseWriter, r *http.Request) {
cmd.Trigger = strings.ToLower(cmd.Trigger)
var oldCmd *model.Command
- if result := <-Srv.Store.Command().Get(cmd.Id); result.Err != nil {
+ if result := <-app.Srv.Store.Command().Get(cmd.Id); result.Err != nil {
c.Err = result.Err
return
} else {
oldCmd = result.Data.(*model.Command)
- if c.Session.UserId != oldCmd.CreatorId && !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS) {
+ if c.Session.UserId != oldCmd.CreatorId && !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS) {
c.LogAudit("fail - inappropriate permissions")
c.Err = model.NewLocAppError("updateCommand", "api.command.update.app_error", nil, "user_id="+c.Session.UserId)
return
@@ -358,7 +361,7 @@ func updateCommand(c *Context, w http.ResponseWriter, r *http.Request) {
cmd.TeamId = oldCmd.TeamId
}
- if result := <-Srv.Store.Command().Update(cmd); result.Err != nil {
+ if result := <-app.Srv.Store.Command().Update(cmd); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -373,13 +376,13 @@ func listTeamCommands(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_SLASH_COMMANDS) {
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_SLASH_COMMANDS) {
c.Err = model.NewLocAppError("listTeamCommands", "api.command.admin_only.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
return
}
- if result := <-Srv.Store.Command().GetByTeam(c.TeamId); result.Err != nil {
+ if result := <-app.Srv.Store.Command().GetByTeam(c.TeamId); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -395,7 +398,7 @@ func regenCommandToken(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_SLASH_COMMANDS) {
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_SLASH_COMMANDS) {
c.Err = model.NewLocAppError("regenCommandToken", "api.command.admin_only.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
return
@@ -412,13 +415,13 @@ func regenCommandToken(c *Context, w http.ResponseWriter, r *http.Request) {
}
var cmd *model.Command
- if result := <-Srv.Store.Command().Get(id); result.Err != nil {
+ if result := <-app.Srv.Store.Command().Get(id); result.Err != nil {
c.Err = result.Err
return
} else {
cmd = result.Data.(*model.Command)
- if c.TeamId != cmd.TeamId || (c.Session.UserId != cmd.CreatorId && !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS)) {
+ if c.TeamId != cmd.TeamId || (c.Session.UserId != cmd.CreatorId && !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS)) {
c.LogAudit("fail - inappropriate permissions")
c.Err = model.NewLocAppError("regenToken", "api.command.regen.app_error", nil, "user_id="+c.Session.UserId)
return
@@ -427,7 +430,7 @@ func regenCommandToken(c *Context, w http.ResponseWriter, r *http.Request) {
cmd.Token = model.NewId()
- if result := <-Srv.Store.Command().Update(cmd); result.Err != nil {
+ if result := <-app.Srv.Store.Command().Update(cmd); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -442,7 +445,7 @@ func deleteCommand(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_SLASH_COMMANDS) {
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_SLASH_COMMANDS) {
c.Err = model.NewLocAppError("deleteCommand", "api.command.admin_only.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
return
@@ -458,18 +461,18 @@ func deleteCommand(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.Command().Get(id); result.Err != nil {
+ if result := <-app.Srv.Store.Command().Get(id); result.Err != nil {
c.Err = result.Err
return
} else {
- if c.TeamId != result.Data.(*model.Command).TeamId || (c.Session.UserId != result.Data.(*model.Command).CreatorId && !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS)) {
+ if c.TeamId != result.Data.(*model.Command).TeamId || (c.Session.UserId != result.Data.(*model.Command).CreatorId && !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS)) {
c.LogAudit("fail - inappropriate permissions")
c.Err = model.NewLocAppError("deleteCommand", "api.command.delete.app_error", nil, "user_id="+c.Session.UserId)
return
}
}
- if err := (<-Srv.Store.Command().Delete(id, model.GetMillis())).Err; err != nil {
+ if err := (<-app.Srv.Store.Command().Delete(id, model.GetMillis())).Err; err != nil {
c.Err = err
return
}
diff --git a/api/command_away.go b/api/command_away.go
index 6d6540320..6a488c081 100644
--- a/api/command_away.go
+++ b/api/command_away.go
@@ -4,6 +4,7 @@
package api
import (
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
)
@@ -36,7 +37,7 @@ func (me *AwayProvider) DoCommand(c *Context, args *model.CommandArgs, message s
if len(message) > 0 {
rmsg = message + " " + rmsg
}
- SetStatusAwayIfNeeded(c.Session.UserId, true)
+ app.SetStatusAwayIfNeeded(c.Session.UserId, true)
return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: rmsg}
}
diff --git a/api/command_echo.go b/api/command_echo.go
index c219945c7..2e931e414 100644
--- a/api/command_echo.go
+++ b/api/command_echo.go
@@ -9,6 +9,7 @@ import (
"time"
l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
)
@@ -87,7 +88,7 @@ func (me *EchoProvider) DoCommand(c *Context, args *model.CommandArgs, message s
time.Sleep(time.Duration(delay) * time.Second)
- if _, err := CreatePost(c, post, true); err != nil {
+ if _, err := app.CreatePost(post, c.TeamId, true); err != nil {
l4g.Error(c.T("api.command_echo.create.app_error"), err)
}
}()
diff --git a/api/command_expand_collapse.go b/api/command_expand_collapse.go
index d36893cb0..5adbf4bab 100644
--- a/api/command_expand_collapse.go
+++ b/api/command_expand_collapse.go
@@ -6,6 +6,7 @@ package api
import (
"strconv"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
)
@@ -67,13 +68,13 @@ func setCollapsePreference(c *Context, isCollapse bool) *model.CommandResponse {
Value: strconv.FormatBool(isCollapse),
}
- if result := <-Srv.Store.Preference().Save(&model.Preferences{pref}); result.Err != nil {
+ if result := <-app.Srv.Store.Preference().Save(&model.Preferences{pref}); result.Err != nil {
return &model.CommandResponse{Text: c.T("api.command_expand_collapse.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
}
socketMessage := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCE_CHANGED, "", "", c.Session.UserId, nil)
socketMessage.Add("preference", pref.ToJson())
- go Publish(socketMessage)
+ go app.Publish(socketMessage)
var rmsg string
diff --git a/api/command_invite_people.go b/api/command_invite_people.go
index e6ad252f6..ef2449ebc 100644
--- a/api/command_invite_people.go
+++ b/api/command_invite_people.go
@@ -6,6 +6,7 @@ package api
import (
"strings"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
@@ -40,9 +41,6 @@ func (me *InvitePeopleProvider) DoCommand(c *Context, args *model.CommandArgs, m
return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: c.T("api.command.invite_people.email_off")}
}
- tchan := Srv.Store.Team().Get(c.TeamId)
- uchan := Srv.Store.User().Get(c.Session.UserId)
-
emailList := strings.Fields(message)
for i := len(emailList) - 1; i >= 0; i-- {
@@ -56,23 +54,10 @@ func (me *InvitePeopleProvider) DoCommand(c *Context, args *model.CommandArgs, m
return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: c.T("api.command.invite_people.no_email")}
}
- var team *model.Team
- if result := <-tchan; result.Err != nil {
- c.Err = result.Err
+ if err := app.InviteNewUsersToTeam(emailList, c.TeamId, c.Session.UserId, c.GetSiteURL()); err != nil {
+ c.Err = err
return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: c.T("api.command.invite_people.fail")}
- } else {
- team = result.Data.(*model.Team)
}
- var user *model.User
- if result := <-uchan; result.Err != nil {
- c.Err = result.Err
- return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: c.T("api.command.invite_people.fail")}
- } else {
- user = result.Data.(*model.User)
- }
-
- go InviteMembers(team, user.GetDisplayName(), emailList, c.GetSiteURL())
-
return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: c.T("api.command.invite_people.sent")}
}
diff --git a/api/command_join.go b/api/command_join.go
index 3c997dffd..bad176656 100644
--- a/api/command_join.go
+++ b/api/command_join.go
@@ -4,6 +4,7 @@
package api
import (
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
)
@@ -33,7 +34,7 @@ func (me *JoinProvider) GetCommand(c *Context) *model.Command {
}
func (me *JoinProvider) DoCommand(c *Context, args *model.CommandArgs, message string) *model.CommandResponse {
- if result := <-Srv.Store.Channel().GetByName(c.TeamId, message); result.Err != nil {
+ if result := <-app.Srv.Store.Channel().GetByName(c.TeamId, message); result.Err != nil {
return &model.CommandResponse{Text: c.T("api.command_join.list.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
} else {
channel := result.Data.(*model.Channel)
@@ -44,7 +45,7 @@ func (me *JoinProvider) DoCommand(c *Context, args *model.CommandArgs, message s
return &model.CommandResponse{Text: c.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
}
- if err, _ := JoinChannelById(c, c.Session.UserId, channel.Id); err != nil {
+ if err := app.JoinChannel(channel, c.Session.UserId); err != nil {
return &model.CommandResponse{Text: c.T("api.command_join.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
}
diff --git a/api/command_loadtest.go b/api/command_loadtest.go
index ed3fc37ba..5ad2736a0 100644
--- a/api/command_loadtest.go
+++ b/api/command_loadtest.go
@@ -11,6 +11,7 @@ import (
"strings"
l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
@@ -185,7 +186,7 @@ func (me *LoadTestProvider) SetupCommand(c *Context, channelId string, message s
} else {
var team *model.Team
- if tr := <-Srv.Store.Team().Get(c.TeamId); tr.Err != nil {
+ if tr := <-app.Srv.Store.Team().Get(c.TeamId); tr.Err != nil {
return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
} else {
team = tr.Data.(*model.Team)
@@ -220,7 +221,7 @@ func (me *LoadTestProvider) UsersCommand(c *Context, channelId string, message s
}
var team *model.Team
- if tr := <-Srv.Store.Team().Get(c.TeamId); tr.Err != nil {
+ if tr := <-app.Srv.Store.Team().Get(c.TeamId); tr.Err != nil {
return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
} else {
team = tr.Data.(*model.Team)
@@ -250,7 +251,7 @@ func (me *LoadTestProvider) ChannelsCommand(c *Context, channelId string, messag
}
var team *model.Team
- if tr := <-Srv.Store.Team().Get(c.TeamId); tr.Err != nil {
+ if tr := <-app.Srv.Store.Team().Get(c.TeamId); tr.Err != nil {
return &model.CommandResponse{Text: "Failed to create testing environment", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
} else {
team = tr.Data.(*model.Team)
@@ -289,7 +290,7 @@ func (me *LoadTestProvider) PostsCommand(c *Context, channelId string, message s
}
var usernames []string
- if result := <-Srv.Store.User().GetProfiles(c.TeamId, 0, 1000); result.Err == nil {
+ if result := <-app.Srv.Store.User().GetProfiles(c.TeamId, 0, 1000); result.Err == nil {
profileUsers := result.Data.(map[string]*model.User)
usernames = make([]string, len(profileUsers))
i := 0
@@ -358,7 +359,7 @@ func (me *LoadTestProvider) UrlCommand(c *Context, channelId string, message str
post.ChannelId = channelId
post.UserId = c.Session.UserId
- if _, err := CreatePost(c, post, false); err != nil {
+ if _, err := app.CreatePost(post, c.TeamId, false); err != nil {
return &model.CommandResponse{Text: "Unable to create post", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
}
}
@@ -397,7 +398,7 @@ func (me *LoadTestProvider) JsonCommand(c *Context, channelId string, message st
post.Message = message
}
- if _, err := CreatePost(c, post, false); err != nil {
+ if _, err := app.CreatePost(post, c.TeamId, false); err != nil {
return &model.CommandResponse{Text: "Unable to create post", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
}
return &model.CommandResponse{Text: "Loaded data", ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
diff --git a/api/command_logout.go b/api/command_logout.go
index 00375bb58..0eaa9a0ba 100644
--- a/api/command_logout.go
+++ b/api/command_logout.go
@@ -4,6 +4,7 @@
package api
import (
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
)
@@ -38,8 +39,7 @@ func (me *LogoutProvider) DoCommand(c *Context, args *model.CommandArgs, message
// We can't actually remove the user's cookie from here so we just dump their session and let the browser figure it out
if c.Session.Id != "" {
- RevokeSessionById(c, c.Session.Id)
- if c.Err != nil {
+ if err := app.RevokeSessionById(c.Session.Id); err != nil {
return FAIL
}
return SUCCESS
diff --git a/api/command_msg.go b/api/command_msg.go
index f2d06824d..86203c2cd 100644
--- a/api/command_msg.go
+++ b/api/command_msg.go
@@ -6,6 +6,7 @@ package api
import (
"strings"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
)
@@ -48,7 +49,7 @@ func (me *msgProvider) DoCommand(c *Context, args *model.CommandArgs, message st
targetUsername = strings.TrimPrefix(targetUsername, "@")
var userProfile *model.User
- if result := <-Srv.Store.User().GetByUsername(targetUsername); result.Err != nil {
+ if result := <-app.Srv.Store.User().GetByUsername(targetUsername); result.Err != nil {
c.Err = result.Err
return &model.CommandResponse{Text: c.T("api.command_msg.missing.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
} else {
@@ -63,9 +64,9 @@ func (me *msgProvider) DoCommand(c *Context, args *model.CommandArgs, message st
channelName := model.GetDMNameFromIds(c.Session.UserId, userProfile.Id)
targetChannelId := ""
- if channel := <-Srv.Store.Channel().GetByName(c.TeamId, channelName); channel.Err != nil {
+ if channel := <-app.Srv.Store.Channel().GetByName(c.TeamId, channelName); channel.Err != nil {
if channel.Err.Id == "store.sql_channel.get_by_name.missing.app_error" {
- if directChannel, err := CreateDirectChannel(c.Session.UserId, userProfile.Id); err != nil {
+ if directChannel, err := app.CreateDirectChannel(c.Session.UserId, userProfile.Id); err != nil {
c.Err = err
return &model.CommandResponse{Text: c.T("api.command_msg.dm_fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
} else {
@@ -79,13 +80,13 @@ func (me *msgProvider) DoCommand(c *Context, args *model.CommandArgs, message st
targetChannelId = channel.Data.(*model.Channel).Id
}
- makeDirectChannelVisible(targetChannelId)
+ app.MakeDirectChannelVisible(targetChannelId)
if len(parsedMessage) > 0 {
post := &model.Post{}
post.Message = parsedMessage
post.ChannelId = targetChannelId
post.UserId = c.Session.UserId
- if _, err := CreatePost(c, post, true); err != nil {
+ if _, err := app.CreatePost(post, c.TeamId, true); err != nil {
return &model.CommandResponse{Text: c.T("api.command_msg.fail.app_error"), ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL}
}
}
diff --git a/api/command_offline.go b/api/command_offline.go
index 1349ac18f..a4bcdf8a5 100644
--- a/api/command_offline.go
+++ b/api/command_offline.go
@@ -4,6 +4,7 @@
package api
import (
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
)
@@ -36,7 +37,7 @@ func (me *OfflineProvider) DoCommand(c *Context, args *model.CommandArgs, messag
if len(message) > 0 {
rmsg = message + " " + rmsg
}
- SetStatusOffline(c.Session.UserId, true)
+ app.SetStatusOffline(c.Session.UserId, true)
return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: rmsg}
}
diff --git a/api/command_online.go b/api/command_online.go
index 887aa2c47..81d3e1fd6 100644
--- a/api/command_online.go
+++ b/api/command_online.go
@@ -4,6 +4,7 @@
package api
import (
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
)
@@ -36,7 +37,7 @@ func (me *OnlineProvider) DoCommand(c *Context, args *model.CommandArgs, message
if len(message) > 0 {
rmsg = message + " " + rmsg
}
- SetStatusOnline(c.Session.UserId, c.Session.Id, true)
+ app.SetStatusOnline(c.Session.UserId, c.Session.Id, true)
return &model.CommandResponse{ResponseType: model.COMMAND_RESPONSE_TYPE_EPHEMERAL, Text: rmsg}
}
diff --git a/api/context.go b/api/context.go
index 7f95fdfbc..e998138a6 100644
--- a/api/context.go
+++ b/api/context.go
@@ -5,7 +5,6 @@ package api
import (
"fmt"
- "net"
"net/http"
"net/url"
"strings"
@@ -15,32 +14,21 @@ import (
"github.com/gorilla/mux"
goi18n "github.com/nicksnyder/go-i18n/i18n"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/model"
- "github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
)
-var sessionCache *utils.Cache = utils.NewLru(model.SESSION_CACHE_SIZE)
-
-var allowedMethods []string = []string{
- "POST",
- "GET",
- "OPTIONS",
- "PUT",
- "PATCH",
- "DELETE",
-}
-
type Context struct {
Session model.Session
RequestId string
IpAddress string
Path string
Err *model.AppError
+ siteURL string
teamURLValid bool
teamURL string
- siteURL string
T goi18n.TranslateFunc
Locale string
TeamId string
@@ -116,7 +104,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c := &Context{}
c.T, c.Locale = utils.GetTranslationsAndLocale(w, r)
c.RequestId = model.NewId()
- c.IpAddress = GetIpAddress(r)
+ c.IpAddress = utils.GetIpAddress(r)
c.TeamId = mux.Vars(r)["team_id"]
token := ""
@@ -153,11 +141,11 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
isTokenFromQueryString = true
}
- if *utils.Cfg.ServiceSettings.SiteURL != "" {
- c.SetSiteURL(*utils.Cfg.ServiceSettings.SiteURL)
- } else {
+ if utils.GetSiteURL() == "" {
protocol := GetProtocol(r)
c.SetSiteURL(protocol + "://" + r.Host)
+ } else {
+ c.SetSiteURL(utils.GetSiteURL())
}
w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId)
@@ -180,9 +168,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
if len(token) != 0 {
- session := GetSession(token)
+ session, err := app.GetSession(token)
- if session == nil || session.IsExpired() {
+ if err != nil {
+ l4g.Error(utils.T("api.context.invalid_session.error"), err.Error())
c.RemoveSessionCookie(w, r)
if h.requireUser || h.requireSystemAdmin {
c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token)
@@ -218,7 +207,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
if c.Err == nil && h.isUserActivity && token != "" && len(c.Session.UserId) > 0 {
- SetStatusOnline(c.Session.UserId, c.Session.Id, false)
+ app.SetStatusOnline(c.Session.UserId, c.Session.Id, false)
}
if c.Err == nil && (h.requireUser || h.requireSystemAdmin) {
@@ -269,31 +258,6 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
-func (cw *CorsWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- if len(*utils.Cfg.ServiceSettings.AllowCorsFrom) > 0 {
- origin := r.Header.Get("Origin")
- if *utils.Cfg.ServiceSettings.AllowCorsFrom == "*" || strings.Contains(*utils.Cfg.ServiceSettings.AllowCorsFrom, origin) {
- w.Header().Set("Access-Control-Allow-Origin", origin)
-
- if r.Method == "OPTIONS" {
- w.Header().Set(
- "Access-Control-Allow-Methods",
- strings.Join(allowedMethods, ", "))
-
- w.Header().Set(
- "Access-Control-Allow-Headers",
- r.Header.Get("Access-Control-Request-Headers"))
- }
- }
- }
-
- if r.Method == "OPTIONS" {
- return
- }
-
- cw.router.ServeHTTP(w, r)
-}
-
func GetProtocol(r *http.Request) string {
if r.Header.Get(model.HEADER_FORWARDED_PROTO) == "https" {
return "https"
@@ -304,7 +268,7 @@ func GetProtocol(r *http.Request) string {
func (c *Context) LogAudit(extraInfo string) {
audit := &model.Audit{UserId: c.Session.UserId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id}
- if r := <-Srv.Store.Audit().Save(audit); r.Err != nil {
+ if r := <-app.Srv.Store.Audit().Save(audit); r.Err != nil {
c.LogError(r.Err)
}
}
@@ -316,7 +280,7 @@ func (c *Context) LogAuditWithUserId(userId, extraInfo string) {
}
audit := &model.Audit{UserId: userId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id}
- if r := <-Srv.Store.Audit().Save(audit); r.Err != nil {
+ if r := <-app.Srv.Store.Audit().Save(audit); r.Err != nil {
c.LogError(r.Err)
}
}
@@ -356,7 +320,7 @@ func (c *Context) MfaRequired() {
return
}
- if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
+ if result := <-app.Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "MfaRequired")
c.Err.StatusCode = http.StatusUnauthorized
return
@@ -383,7 +347,7 @@ func (c *Context) SystemAdminRequired() {
c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "SystemAdminRequired")
c.Err.StatusCode = http.StatusUnauthorized
return
- } else if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ } else if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
c.Err = model.NewLocAppError("", "api.context.permissions.app_error", nil, "AdminRequired")
c.Err.StatusCode = http.StatusForbidden
return
@@ -416,13 +380,18 @@ func (c *Context) SetUnknownError(where string, details string) {
c.Err = model.NewLocAppError(where, "api.context.unknown.app_error", nil, details)
}
+func (c *Context) SetPermissionError(permission *model.Permission) {
+ c.Err = model.NewLocAppError("Permissions", "api.context.permissions.app_error", nil, "userId="+c.Session.UserId+", "+"permission="+permission.Id)
+ c.Err.StatusCode = http.StatusForbidden
+}
+
func (c *Context) setTeamURL(url string, valid bool) {
c.teamURL = url
c.teamURLValid = valid
}
func (c *Context) SetTeamURLFromSession() {
- if result := <-Srv.Store.Team().Get(c.TeamId); result.Err == nil {
+ if result := <-app.Srv.Store.Team().Get(c.TeamId); result.Err == nil {
c.setTeamURL(c.GetSiteURL()+"/"+result.Data.(*model.Team).Name, true)
}
}
@@ -457,20 +426,6 @@ func IsApiCall(r *http.Request) bool {
return strings.Index(r.URL.Path, "/api/") == 0
}
-func GetIpAddress(r *http.Request) string {
- address := r.Header.Get(model.HEADER_FORWARDED)
-
- if len(address) == 0 {
- address = r.Header.Get(model.HEADER_REAL_IP)
- }
-
- if len(address) == 0 {
- address, _, _ = net.SplitHostPort(r.RemoteAddr)
- }
-
- return address
-}
-
func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request) {
T, _ := utils.GetTranslationsAndLocale(w, r)
@@ -501,7 +456,7 @@ func Handle404(w http.ResponseWriter, r *http.Request) {
err.Translate(utils.T)
err.StatusCode = http.StatusNotFound
- l4g.Debug("%v: code=404 ip=%v", r.URL.Path, GetIpAddress(r))
+ l4g.Debug("%v: code=404 ip=%v", r.URL.Path, utils.GetIpAddress(r))
if IsApiCall(r) {
w.WriteHeader(err.StatusCode)
@@ -512,87 +467,16 @@ func Handle404(w http.ResponseWriter, r *http.Request) {
}
}
-func GetSession(token string) *model.Session {
- metrics := einterfaces.GetMetricsInterface()
-
- var session *model.Session
- if ts, ok := sessionCache.Get(token); ok {
- session = ts.(*model.Session)
- if metrics != nil {
- metrics.IncrementMemCacheHitCounter("Session")
- }
- } else {
- if metrics != nil {
- metrics.IncrementMemCacheMissCounter("Session")
- }
- }
-
- if session == nil {
- if sessionResult := <-Srv.Store.Session().Get(token); sessionResult.Err != nil {
- l4g.Error(utils.T("api.context.invalid_token.error"), token, sessionResult.Err.DetailedError)
- } else {
- session = sessionResult.Data.(*model.Session)
-
- if session.IsExpired() || session.Token != token {
- return nil
- } else {
- AddSessionToCache(session)
- return session
- }
- }
- }
-
- return session
-}
-
-func RemoveAllSessionsForUserId(userId string) {
-
- RemoveAllSessionsForUserIdSkipClusterSend(userId)
-
- if einterfaces.GetClusterInterface() != nil {
- einterfaces.GetClusterInterface().RemoveAllSessionsForUserId(userId)
- }
-}
-
-func RemoveAllSessionsForUserIdSkipClusterSend(userId string) {
- keys := sessionCache.Keys()
-
- for _, key := range keys {
- if ts, ok := sessionCache.Get(key); ok {
- session := ts.(*model.Session)
- if session.UserId == userId {
- sessionCache.Remove(key)
- }
- }
- }
-
- InvalidateWebConnSessionCacheForUser(userId)
-
-}
-
-func AddSessionToCache(session *model.Session) {
- sessionCache.AddWithExpiresInSecs(session.Token, session, int64(*utils.Cfg.ServiceSettings.SessionCacheInMinutes*60))
-}
-
-func InvalidateAllCaches() {
- l4g.Info(utils.T("api.context.invalidate_all_caches"))
- sessionCache.Purge()
- ClearStatusCache()
- store.ClearChannelCaches()
- store.ClearUserCaches()
- store.ClearPostCaches()
-}
-
func (c *Context) CheckTeamId() {
if c.TeamId != "" && c.Session.GetTeamByTeamId(c.TeamId) == nil {
- if HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
- if result := <-Srv.Store.Team().Get(c.TeamId); result.Err != nil {
+ if app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ if result := <-app.Srv.Store.Team().Get(c.TeamId); result.Err != nil {
c.Err = result.Err
c.Err.StatusCode = http.StatusBadRequest
return
}
} else {
- // just return because it fail on the HasPermissionToContext and the error is already on the Context c.Err
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
return
}
}
diff --git a/api/context_test.go b/api/context_test.go
index 2227b7f65..cd6ca01aa 100644
--- a/api/context_test.go
+++ b/api/context_test.go
@@ -4,32 +4,9 @@
package api
import (
- "github.com/mattermost/platform/model"
"testing"
)
-func TestCache(t *testing.T) {
- session := &model.Session{
- Id: model.NewId(),
- Token: model.NewId(),
- UserId: model.NewId(),
- }
-
- sessionCache.AddWithExpiresInSecs(session.Token, session, 5*60)
-
- keys := sessionCache.Keys()
- if len(keys) <= 0 {
- t.Fatal("should have items")
- }
-
- RemoveAllSessionsForUserId(session.UserId)
-
- rkeys := sessionCache.Keys()
- if len(rkeys) != len(keys)-1 {
- t.Fatal("should have one less")
- }
-}
-
func TestSiteURL(t *testing.T) {
c := &Context{}
diff --git a/api/deprecated.go b/api/deprecated.go
index 4865ab5e0..9c1d2a4ce 100644
--- a/api/deprecated.go
+++ b/api/deprecated.go
@@ -8,6 +8,7 @@ import (
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
@@ -31,11 +32,12 @@ func InitDeprecated() {
func getMoreChannels(c *Context, w http.ResponseWriter, r *http.Request) {
// user is already in the team
- if !HasPermissionToTeamContext(c, c.TeamId, model.PERMISSION_LIST_TEAM_CHANNELS) {
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_LIST_TEAM_CHANNELS) {
+ c.SetPermissionError(model.PERMISSION_LIST_TEAM_CHANNELS)
return
}
- if result := <-Srv.Store.Channel().GetMoreChannels(c.TeamId, c.Session.UserId, 0, 100000); result.Err != nil {
+ if result := <-app.Srv.Store.Channel().GetMoreChannels(c.TeamId, c.Session.UserId, 0, 100000); result.Err != nil {
c.Err = result.Err
return
} else if HandleEtag(result.Data.(*model.ChannelList).Etag(), "Get More Channels (deprecated)", w, r) {
@@ -61,7 +63,7 @@ func updateLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) {
doClearPush := false
if *utils.Cfg.EmailSettings.SendPushNotifications && !c.Session.IsMobileApp() && active {
- if result := <-Srv.Store.User().GetUnreadCountForChannel(c.Session.UserId, id); result.Err != nil {
+ if result := <-app.Srv.Store.User().GetUnreadCountForChannel(c.Session.UserId, id); result.Err != nil {
l4g.Error(utils.T("api.channel.update_last_viewed_at.get_unread_count_for_channel.error"), c.Session.UserId, id, result.Err.Error())
} else {
if result.Data.(int64) > 0 {
@@ -71,16 +73,16 @@ func updateLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) {
}
go func() {
- if err := SetActiveChannel(c.Session.UserId, id); err != nil {
+ if err := app.SetActiveChannel(c.Session.UserId, id); err != nil {
l4g.Error(err.Error())
}
}()
- Srv.Store.Channel().UpdateLastViewedAt([]string{id}, c.Session.UserId)
+ app.Srv.Store.Channel().UpdateLastViewedAt([]string{id}, c.Session.UserId)
// Must be after update so that unread count is correct
if doClearPush {
- go clearPushNotification(c.Session.UserId, id)
+ go app.ClearPushNotification(c.Session.UserId, id)
}
chanPref := model.Preference{
@@ -97,12 +99,12 @@ func updateLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) {
Value: c.TeamId,
}
- Srv.Store.Preference().Save(&model.Preferences{teamPref, chanPref})
+ app.Srv.Store.Preference().Save(&model.Preferences{teamPref, chanPref})
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_VIEWED, c.TeamId, "", c.Session.UserId, nil)
message.Add("channel_id", id)
- go Publish(message)
+ go app.Publish(message)
result := make(map[string]string)
result["id"] = id
@@ -116,7 +118,7 @@ func setLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) {
data := model.StringInterfaceFromJson(r.Body)
newLastViewedAt := int64(data["last_viewed_at"].(float64))
- Srv.Store.Channel().SetLastViewedAt(id, c.Session.UserId, newLastViewedAt)
+ app.Srv.Store.Channel().SetLastViewedAt(id, c.Session.UserId, newLastViewedAt)
chanPref := model.Preference{
UserId: c.Session.UserId,
@@ -132,12 +134,12 @@ func setLastViewedAt(c *Context, w http.ResponseWriter, r *http.Request) {
Value: c.TeamId,
}
- Srv.Store.Preference().Save(&model.Preferences{teamPref, chanPref})
+ app.Srv.Store.Preference().Save(&model.Preferences{teamPref, chanPref})
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_VIEWED, c.TeamId, "", c.Session.UserId, nil)
message.Add("channel_id", id)
- go Publish(message)
+ go app.Publish(message)
result := make(map[string]string)
result["id"] = id
@@ -154,7 +156,7 @@ func setActiveChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if err := SetActiveChannel(c.Session.UserId, channelId); err != nil {
+ if err := app.SetActiveChannel(c.Session.UserId, channelId); err != nil {
c.Err = err
return
}
diff --git a/api/emoji.go b/api/emoji.go
index 37adace49..8f665fbc1 100644
--- a/api/emoji.go
+++ b/api/emoji.go
@@ -18,6 +18,7 @@ import (
l4g "github.com/alecthomas/log4go"
"github.com/disintegration/imaging"
"github.com/gorilla/mux"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
@@ -46,7 +47,7 @@ func getEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.Emoji().GetAll(); result.Err != nil {
+ if result := <-app.Srv.Store.Emoji().GetAll(); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -114,7 +115,7 @@ func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.Emoji().GetByName(emoji.Name); result.Err == nil && result.Data != nil {
+ if result := <-app.Srv.Store.Emoji().GetByName(emoji.Name); result.Err == nil && result.Data != nil {
c.Err = model.NewLocAppError("createEmoji", "api.emoji.create.duplicate.app_error", nil, "")
c.Err.StatusCode = http.StatusBadRequest
return
@@ -128,7 +129,7 @@ func createEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.Emoji().Save(emoji); result.Err != nil {
+ if result := <-app.Srv.Store.Emoji().Save(emoji); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -162,7 +163,7 @@ func uploadEmojiImage(id string, imageData *multipart.FileHeader) *model.AppErro
if err := gif.EncodeAll(newbuf, resized_gif); err != nil {
return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.gif_encode_error", nil, "")
}
- if err := WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
+ if err := app.WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
return err
}
}
@@ -174,13 +175,13 @@ func uploadEmojiImage(id string, imageData *multipart.FileHeader) *model.AppErro
if err := png.Encode(newbuf, resized_image); err != nil {
return model.NewLocAppError("uploadEmojiImage", "api.emoji.upload.large_image.encode_error", nil, "")
}
- if err := WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
+ if err := app.WriteFile(newbuf.Bytes(), getEmojiImagePath(id)); err != nil {
return err
}
}
}
} else {
- if err := WriteFile(buf.Bytes(), getEmojiImagePath(id)); err != nil {
+ if err := app.WriteFile(buf.Bytes(), getEmojiImagePath(id)); err != nil {
return err
}
}
@@ -210,20 +211,20 @@ func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
}
var emoji *model.Emoji
- if result := <-Srv.Store.Emoji().Get(id); result.Err != nil {
+ if result := <-app.Srv.Store.Emoji().Get(id); result.Err != nil {
c.Err = result.Err
return
} else {
emoji = result.Data.(*model.Emoji)
- if c.Session.UserId != emoji.CreatorId && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ if c.Session.UserId != emoji.CreatorId && !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
c.Err = model.NewLocAppError("deleteEmoji", "api.emoji.delete.permissions.app_error", nil, "user_id="+c.Session.UserId)
c.Err.StatusCode = http.StatusUnauthorized
return
}
}
- if err := (<-Srv.Store.Emoji().Delete(id, model.GetMillis())).Err; err != nil {
+ if err := (<-app.Srv.Store.Emoji().Delete(id, model.GetMillis())).Err; err != nil {
c.Err = err
return
}
@@ -235,13 +236,13 @@ func deleteEmoji(c *Context, w http.ResponseWriter, r *http.Request) {
}
func deleteEmojiImage(id string) {
- if err := MoveFile(getEmojiImagePath(id), "emoji/"+id+"/image_deleted"); err != nil {
+ if err := app.MoveFile(getEmojiImagePath(id), "emoji/"+id+"/image_deleted"); err != nil {
l4g.Error("Failed to rename image when deleting emoji %v", id)
}
}
func deleteReactionsForEmoji(emojiName string) {
- if result := <-Srv.Store.Reaction().DeleteAllWithEmojiName(emojiName); result.Err != nil {
+ if result := <-app.Srv.Store.Reaction().DeleteAllWithEmojiName(emojiName); result.Err != nil {
l4g.Warn(utils.T("api.emoji.delete.delete_reactions.app_error"), emojiName)
l4g.Warn(result.Err)
}
@@ -268,13 +269,13 @@ func getEmojiImage(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.Emoji().Get(id); result.Err != nil {
+ if result := <-app.Srv.Store.Emoji().Get(id); result.Err != nil {
c.Err = result.Err
return
} else {
var img []byte
- if data, err := ReadFile(getEmojiImagePath(id)); err != nil {
+ if data, err := app.ReadFile(getEmojiImagePath(id)); err != nil {
c.Err = model.NewLocAppError("getEmojiImage", "api.emoji.get_image.read.app_error", nil, err.Error())
return
} else {
diff --git a/api/emoji_test.go b/api/emoji_test.go
index efe4fd363..73440ba15 100644
--- a/api/emoji_test.go
+++ b/api/emoji_test.go
@@ -13,6 +13,7 @@ import (
"testing"
"time"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
@@ -44,11 +45,11 @@ func TestGetEmoji(t *testing.T) {
}
for i, emoji := range emojis {
- emojis[i] = store.Must(Srv.Store.Emoji().Save(emoji)).(*model.Emoji)
+ emojis[i] = store.Must(app.Srv.Store.Emoji().Save(emoji)).(*model.Emoji)
}
defer func() {
for _, emoji := range emojis {
- store.Must(Srv.Store.Emoji().Delete(emoji.Id, time.Now().Unix()))
+ store.Must(app.Srv.Store.Emoji().Delete(emoji.Id, time.Now().Unix()))
}
}()
@@ -76,7 +77,7 @@ func TestGetEmoji(t *testing.T) {
Name: model.NewId(),
DeleteAt: 1,
}
- deleted = store.Must(Srv.Store.Emoji().Save(deleted)).(*model.Emoji)
+ deleted = store.Must(app.Srv.Store.Emoji().Save(deleted)).(*model.Emoji)
if returnedEmojis, err := Client.ListEmoji(); err != nil {
t.Fatal(err)
@@ -314,10 +315,10 @@ func createTestPng(t *testing.T, width int, height int) []byte {
}
func createTestEmoji(t *testing.T, emoji *model.Emoji, imageData []byte) *model.Emoji {
- emoji = store.Must(Srv.Store.Emoji().Save(emoji)).(*model.Emoji)
+ emoji = store.Must(app.Srv.Store.Emoji().Save(emoji)).(*model.Emoji)
- if err := WriteFile(imageData, "emoji/"+emoji.Id+"/image"); err != nil {
- store.Must(Srv.Store.Emoji().Delete(emoji.Id, time.Now().Unix()))
+ if err := app.WriteFile(imageData, "emoji/"+emoji.Id+"/image"); err != nil {
+ store.Must(app.Srv.Store.Emoji().Delete(emoji.Id, time.Now().Unix()))
t.Fatalf("failed to write image: %v", err.Error())
}
diff --git a/api/file.go b/api/file.go
index 0774a78b8..bbe06f2da 100644
--- a/api/file.go
+++ b/api/file.go
@@ -5,57 +5,18 @@ package api
import (
"bytes"
- "crypto/sha256"
- "encoding/base64"
- "fmt"
- "image"
- "image/color"
- "image/draw"
_ "image/gif"
- "image/jpeg"
"io"
- "io/ioutil"
"net/http"
"net/url"
- "os"
- "path"
- "path/filepath"
"strconv"
- "strings"
- "sync"
l4g "github.com/alecthomas/log4go"
- "github.com/disintegration/imaging"
"github.com/gorilla/mux"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
- "github.com/rwcarlsen/goexif/exif"
_ "golang.org/x/image/bmp"
-
- s3 "github.com/minio/minio-go"
-)
-
-const (
- /*
- EXIF Image Orientations
- 1 2 3 4 5 6 7 8
-
- 888888 888888 88 88 8888888888 88 88 8888888888
- 88 88 88 88 88 88 88 88 88 88 88 88
- 8888 8888 8888 8888 88 8888888888 8888888888 88
- 88 88 88 88
- 88 88 888888 888888
- */
- Upright = 1
- UprightMirrored = 2
- UpsideDown = 3
- UpsideDownMirrored = 4
- RotatedCWMirrored = 5
- RotatedCCW = 6
- RotatedCCWMirrored = 7
- RotatedCW = 8
-
- MaxImageSize = 6048 * 4032 // 24 megapixels, roughly 36MB as a raw image
)
func InitFile() {
@@ -104,7 +65,8 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_UPLOAD_FILE) {
+ if !app.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_UPLOAD_FILE) {
+ c.SetPermissionError(model.PERMISSION_UPLOAD_FILE)
return
}
@@ -129,7 +91,7 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
io.Copy(buf, file)
data := buf.Bytes()
- info, err := doUploadFile(c.TeamId, channelId, c.Session.UserId, fileHeader.Filename, data)
+ info, err := app.DoUploadFile(c.TeamId, channelId, c.Session.UserId, fileHeader.Filename, data)
if err != nil {
c.Err = err
return
@@ -148,169 +110,11 @@ func uploadFile(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
- handleImages(previewPathList, thumbnailPathList, imageDataList)
+ app.HandleImages(previewPathList, thumbnailPathList, imageDataList)
w.Write([]byte(resStruct.ToJson()))
}
-func doUploadFile(teamId string, channelId string, userId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError) {
- filename := filepath.Base(rawFilename)
-
- info, err := model.GetInfoForBytes(filename, data)
- if err != nil {
- err.StatusCode = http.StatusBadRequest
- return nil, err
- }
-
- info.Id = model.NewId()
- info.CreatorId = userId
-
- pathPrefix := "teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/" + info.Id + "/"
- info.Path = pathPrefix + filename
-
- if info.IsImage() {
- // Check dimensions before loading the whole thing into memory later on
- if info.Width*info.Height > MaxImageSize {
- err := model.NewLocAppError("uploadFile", "api.file.upload_file.large_image.app_error", map[string]interface{}{"Filename": filename}, "")
- err.StatusCode = http.StatusBadRequest
- return nil, err
- }
-
- nameWithoutExtension := filename[:strings.LastIndex(filename, ".")]
- info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg"
- info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg"
- }
-
- if err := WriteFile(data, info.Path); err != nil {
- return nil, err
- }
-
- if result := <-Srv.Store.FileInfo().Save(info); result.Err != nil {
- return nil, result.Err
- }
-
- return info, nil
-}
-
-func handleImages(previewPathList []string, thumbnailPathList []string, fileData [][]byte) {
- for i, data := range fileData {
- go func(i int, data []byte) {
- img, width, height := prepareImage(fileData[i])
- if img != nil {
- go generateThumbnailImage(*img, thumbnailPathList[i], width, height)
- go generatePreviewImage(*img, previewPathList[i], width)
- }
- }(i, data)
- }
-}
-
-func prepareImage(fileData []byte) (*image.Image, int, int) {
- // Decode image bytes into Image object
- img, imgType, err := image.Decode(bytes.NewReader(fileData))
- if err != nil {
- l4g.Error(utils.T("api.file.handle_images_forget.decode.error"), err)
- return nil, 0, 0
- }
-
- width := img.Bounds().Dx()
- height := img.Bounds().Dy()
-
- // Fill in the background of a potentially-transparent png file as white
- if imgType == "png" {
- dst := image.NewRGBA(img.Bounds())
- draw.Draw(dst, dst.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
- draw.Draw(dst, dst.Bounds(), img, img.Bounds().Min, draw.Over)
- img = dst
- }
-
- // Flip the image to be upright
- orientation, _ := getImageOrientation(fileData)
-
- switch orientation {
- case UprightMirrored:
- img = imaging.FlipH(img)
- case UpsideDown:
- img = imaging.Rotate180(img)
- case UpsideDownMirrored:
- img = imaging.FlipV(img)
- case RotatedCWMirrored:
- img = imaging.Transpose(img)
- case RotatedCCW:
- img = imaging.Rotate270(img)
- case RotatedCCWMirrored:
- img = imaging.Transverse(img)
- case RotatedCW:
- img = imaging.Rotate90(img)
- }
-
- return &img, width, height
-}
-
-func getImageOrientation(imageData []byte) (int, error) {
- if exifData, err := exif.Decode(bytes.NewReader(imageData)); err != nil {
- return Upright, err
- } else {
- if tag, err := exifData.Get("Orientation"); err != nil {
- return Upright, err
- } else {
- orientation, err := tag.Int(0)
- if err != nil {
- return Upright, err
- } else {
- return orientation, nil
- }
- }
- }
-}
-
-func generateThumbnailImage(img image.Image, thumbnailPath string, width int, height int) {
- thumbWidth := float64(utils.Cfg.FileSettings.ThumbnailWidth)
- thumbHeight := float64(utils.Cfg.FileSettings.ThumbnailHeight)
- imgWidth := float64(width)
- imgHeight := float64(height)
-
- var thumbnail image.Image
- if imgHeight < thumbHeight && imgWidth < thumbWidth {
- thumbnail = img
- } else if imgHeight/imgWidth < thumbHeight/thumbWidth {
- thumbnail = imaging.Resize(img, 0, utils.Cfg.FileSettings.ThumbnailHeight, imaging.Lanczos)
- } else {
- thumbnail = imaging.Resize(img, utils.Cfg.FileSettings.ThumbnailWidth, 0, imaging.Lanczos)
- }
-
- buf := new(bytes.Buffer)
- if err := jpeg.Encode(buf, thumbnail, &jpeg.Options{Quality: 90}); err != nil {
- l4g.Error(utils.T("api.file.handle_images_forget.encode_jpeg.error"), thumbnailPath, err)
- return
- }
-
- if err := WriteFile(buf.Bytes(), thumbnailPath); err != nil {
- l4g.Error(utils.T("api.file.handle_images_forget.upload_thumb.error"), thumbnailPath, err)
- return
- }
-}
-
-func generatePreviewImage(img image.Image, previewPath string, width int) {
- var preview image.Image
- if width > int(utils.Cfg.FileSettings.PreviewWidth) {
- preview = imaging.Resize(img, utils.Cfg.FileSettings.PreviewWidth, utils.Cfg.FileSettings.PreviewHeight, imaging.Lanczos)
- } else {
- preview = img
- }
-
- buf := new(bytes.Buffer)
-
- if err := jpeg.Encode(buf, preview, &jpeg.Options{Quality: 90}); err != nil {
- l4g.Error(utils.T("api.file.handle_images_forget.encode_preview.error"), previewPath, err)
- return
- }
-
- if err := WriteFile(buf.Bytes(), previewPath); err != nil {
- l4g.Error(utils.T("api.file.handle_images_forget.upload_preview.error"), previewPath, err)
- return
- }
-}
-
func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
info, err := getFileInfoForRequest(c, r, true)
if err != nil {
@@ -318,7 +122,7 @@ func getFile(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if data, err := ReadFile(info.Path); err != nil {
+ if data, err := app.ReadFile(info.Path); err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
} else if err := writeFileResponse(info.Name, info.MimeType, data, w, r); err != nil {
@@ -340,7 +144,7 @@ func getFileThumbnail(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if data, err := ReadFile(info.ThumbnailPath); err != nil {
+ if data, err := app.ReadFile(info.ThumbnailPath); err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
} else if err := writeFileResponse(info.Name, "", data, w, r); err != nil {
@@ -362,7 +166,7 @@ func getFilePreview(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if data, err := ReadFile(info.PreviewPath); err != nil {
+ if data, err := app.ReadFile(info.PreviewPath); err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
} else if err := writeFileResponse(info.Name, "", data, w, r); err != nil {
@@ -399,7 +203,7 @@ func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) {
hash := r.URL.Query().Get("h")
if len(hash) > 0 {
- correctHash := generatePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt)
+ correctHash := app.GeneratePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt)
if hash != correctHash {
c.Err = model.NewLocAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "")
@@ -412,7 +216,7 @@ func getPublicFile(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if data, err := ReadFile(info.Path); err != nil {
+ if data, err := app.ReadFile(info.Path); err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
} else if err := writeFileResponse(info.Name, info.MimeType, data, w, r); err != nil {
@@ -436,7 +240,7 @@ func getFileInfoForRequest(c *Context, r *http.Request, requireFileVisible bool)
}
var info *model.FileInfo
- if result := <-Srv.Store.FileInfo().Get(fileId); result.Err != nil {
+ if result := <-app.Srv.Store.FileInfo().Get(fileId); result.Err != nil {
return nil, result.Err
} else {
info = result.Data.(*model.FileInfo)
@@ -451,7 +255,8 @@ func getFileInfoForRequest(c *Context, r *http.Request, requireFileVisible bool)
}
if requireFileVisible {
- if !HasPermissionToChannelByPostContext(c, info.PostId, model.PERMISSION_READ_CHANNEL) {
+ if !app.SessionHasPermissionToChannelByPost(c.Session, info.PostId, model.PERMISSION_READ_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return nil, c.Err
}
}
@@ -481,7 +286,7 @@ func getPublicFileOld(c *Context, w http.ResponseWriter, r *http.Request) {
hash := r.URL.Query().Get("h")
if len(hash) > 0 {
- correctHash := generatePublicLinkHash(filename, *utils.Cfg.FileSettings.PublicLinkSalt)
+ correctHash := app.GeneratePublicLinkHash(filename, *utils.Cfg.FileSettings.PublicLinkSalt)
if hash != correctHash {
c.Err = model.NewLocAppError("getPublicFile", "api.file.get_file.public_invalid.app_error", nil, "")
@@ -497,7 +302,7 @@ func getPublicFileOld(c *Context, w http.ResponseWriter, r *http.Request) {
path := "teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/" + filename
var info *model.FileInfo
- if result := <-Srv.Store.FileInfo().GetByPath(path); result.Err != nil {
+ if result := <-app.Srv.Store.FileInfo().GetByPath(path); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -510,7 +315,7 @@ func getPublicFileOld(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if data, err := ReadFile(info.Path); err != nil {
+ if data, err := app.ReadFile(info.Path); err != nil {
c.Err = err
c.Err.StatusCode = http.StatusNotFound
} else if err := writeFileResponse(info.Name, info.MimeType, data, w, r); err != nil {
@@ -559,320 +364,5 @@ func getPublicLink(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- w.Write([]byte(model.StringToJson(generatePublicLink(c.GetSiteURL(), info))))
-}
-
-func generatePublicLink(siteURL string, info *model.FileInfo) string {
- hash := generatePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt)
- return fmt.Sprintf("%s%s/public/files/%v/get?h=%s", siteURL, model.API_URL_SUFFIX, info.Id, hash)
-}
-
-func generatePublicLinkHash(fileId, salt string) string {
- hash := sha256.New()
- hash.Write([]byte(salt))
- hash.Write([]byte(fileId))
-
- return base64.RawURLEncoding.EncodeToString(hash.Sum(nil))
-}
-
-var fileMigrationLock sync.Mutex
-
-// Creates and stores FileInfos for a post created before the FileInfos table existed.
-func migrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo {
- if len(post.Filenames) == 0 {
- l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.no_filenames.warn"), post.Id)
- return []*model.FileInfo{}
- }
-
- cchan := Srv.Store.Channel().Get(post.ChannelId, true)
-
- // There's a weird bug that rarely happens where a post ends up with duplicate Filenames so remove those
- filenames := utils.RemoveDuplicatesFromStringArray(post.Filenames)
-
- var channel *model.Channel
- if result := <-cchan; result.Err != nil {
- l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.channel.app_error"), post.Id, post.ChannelId, result.Err)
- return []*model.FileInfo{}
- } else {
- channel = result.Data.(*model.Channel)
- }
-
- // Find the team that was used to make this post since its part of the file path that isn't saved in the Filename
- var teamId string
- if channel.TeamId == "" {
- // This post was made in a cross-team DM channel so we need to find where its files were saved
- teamId = findTeamIdForFilename(post, filenames[0])
- } else {
- teamId = channel.TeamId
- }
-
- // Create FileInfo objects for this post
- infos := make([]*model.FileInfo, 0, len(filenames))
- if teamId == "" {
- l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.team_id.error"), post.Id, filenames)
- } else {
- for _, filename := range filenames {
- info := getInfoForFilename(post, teamId, filename)
- if info == nil {
- continue
- }
-
- infos = append(infos, info)
- }
- }
-
- // Lock to prevent only one migration thread from trying to update the post at once, preventing duplicate FileInfos from being created
- fileMigrationLock.Lock()
- defer fileMigrationLock.Unlock()
-
- if result := <-Srv.Store.Post().Get(post.Id); result.Err != nil {
- l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.get_post_again.app_error"), post.Id, result.Err)
- return []*model.FileInfo{}
- } else if newPost := result.Data.(*model.PostList).Posts[post.Id]; len(newPost.Filenames) != len(post.Filenames) {
- // Another thread has already created FileInfos for this post, so just return those
- if result := <-Srv.Store.FileInfo().GetForPost(post.Id); result.Err != nil {
- l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.get_post_file_infos_again.app_error"), post.Id, result.Err)
- return []*model.FileInfo{}
- } else {
- l4g.Debug(utils.T("api.file.migrate_filenames_to_file_infos.not_migrating_post.debug"), post.Id)
- return result.Data.([]*model.FileInfo)
- }
- }
-
- l4g.Debug(utils.T("api.file.migrate_filenames_to_file_infos.migrating_post.debug"), post.Id)
-
- savedInfos := make([]*model.FileInfo, 0, len(infos))
- fileIds := make([]string, 0, len(filenames))
- for _, info := range infos {
- if result := <-Srv.Store.FileInfo().Save(info); result.Err != nil {
- l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.save_file_info.app_error"), post.Id, info.Id, info.Path, result.Err)
- continue
- }
-
- savedInfos = append(savedInfos, info)
- fileIds = append(fileIds, info.Id)
- }
-
- // Copy and save the updated post
- newPost := &model.Post{}
- *newPost = *post
-
- newPost.Filenames = []string{}
- newPost.FileIds = fileIds
-
- // Update Posts to clear Filenames and set FileIds
- if result := <-Srv.Store.Post().Update(newPost, post); result.Err != nil {
- l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.save_post.app_error"), post.Id, newPost.FileIds, post.Filenames, result.Err)
- return []*model.FileInfo{}
- } else {
- return savedInfos
- }
-}
-
-func findTeamIdForFilename(post *model.Post, filename string) string {
- split := strings.SplitN(filename, "/", 5)
- id := split[3]
- name, _ := url.QueryUnescape(split[4])
-
- // This post is in a direct channel so we need to figure out what team the files are stored under.
- if result := <-Srv.Store.Team().GetTeamsByUserId(post.UserId); result.Err != nil {
- l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.teams.app_error"), post.Id, result.Err)
- } else if teams := result.Data.([]*model.Team); len(teams) == 1 {
- // The user has only one team so the post must've been sent from it
- return teams[0].Id
- } else {
- for _, team := range teams {
- path := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/%s", team.Id, post.ChannelId, post.UserId, id, name)
- if _, err := ReadFile(path); err == nil {
- // Found the team that this file was posted from
- return team.Id
- }
- }
- }
-
- return ""
-}
-
-func getInfoForFilename(post *model.Post, teamId string, filename string) *model.FileInfo {
- // Find the path from the Filename of the form /{channelId}/{userId}/{uid}/{nameWithExtension}
- split := strings.SplitN(filename, "/", 5)
- if len(split) < 5 {
- l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.unexpected_filename.error"), post.Id, filename)
- return nil
- }
-
- channelId := split[1]
- userId := split[2]
- oldId := split[3]
- name, _ := url.QueryUnescape(split[4])
-
- if split[0] != "" || split[1] != post.ChannelId || split[2] != post.UserId || strings.Contains(split[4], "/") {
- l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.mismatched_filename.warn"), post.Id, post.ChannelId, post.UserId, filename)
- }
-
- pathPrefix := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/", teamId, channelId, userId, oldId)
- path := pathPrefix + name
-
- // Open the file and populate the fields of the FileInfo
- var info *model.FileInfo
- if data, err := ReadFile(path); err != nil {
- l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.file_not_found.error"), post.Id, filename, path, err)
- return nil
- } else {
- var err *model.AppError
- info, err = model.GetInfoForBytes(name, data)
- if err != nil {
- l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.info.app_error"), post.Id, filename, err)
- }
- }
-
- // Generate a new ID because with the old system, you could very rarely get multiple posts referencing the same file
- info.Id = model.NewId()
- info.CreatorId = post.UserId
- info.PostId = post.Id
- info.CreateAt = post.CreateAt
- info.UpdateAt = post.UpdateAt
- info.Path = path
-
- if info.IsImage() {
- nameWithoutExtension := name[:strings.LastIndex(name, ".")]
- info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg"
- info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg"
- }
-
- return info
-}
-
-func WriteFile(f []byte, path string) *model.AppError {
- if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
- endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint
- accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId
- secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey
- secure := *utils.Cfg.FileSettings.AmazonS3SSL
- s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure)
- if err != nil {
- return model.NewLocAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error())
- }
- bucket := utils.Cfg.FileSettings.AmazonS3Bucket
- ext := filepath.Ext(path)
-
- if model.IsFileExtImage(ext) {
- _, err = s3Clnt.PutObject(bucket, path, bytes.NewReader(f), model.GetImageMimeType(ext))
- } else {
- _, err = s3Clnt.PutObject(bucket, path, bytes.NewReader(f), "binary/octet-stream")
- }
- if err != nil {
- return model.NewLocAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error())
- }
- } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
- if err := writeFileLocally(f, utils.Cfg.FileSettings.Directory+path); err != nil {
- return err
- }
- } else {
- return model.NewLocAppError("WriteFile", "api.file.write_file.configured.app_error", nil, "")
- }
-
- return nil
-}
-
-func MoveFile(oldPath, newPath string) *model.AppError {
- if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
- endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint
- accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId
- secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey
- secure := *utils.Cfg.FileSettings.AmazonS3SSL
- s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure)
- if err != nil {
- return model.NewLocAppError("moveFile", "api.file.write_file.s3.app_error", nil, err.Error())
- }
- bucket := utils.Cfg.FileSettings.AmazonS3Bucket
-
- var copyConds = s3.NewCopyConditions()
- if err = s3Clnt.CopyObject(bucket, newPath, "/"+path.Join(bucket, oldPath), copyConds); err != nil {
- return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error())
- }
- if err = s3Clnt.RemoveObject(bucket, oldPath); err != nil {
- return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error())
- }
- } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
- if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+newPath), 0774); err != nil {
- return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error())
- }
-
- if err := os.Rename(utils.Cfg.FileSettings.Directory+oldPath, utils.Cfg.FileSettings.Directory+newPath); err != nil {
- return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error())
- }
- } else {
- return model.NewLocAppError("moveFile", "api.file.move_file.configured.app_error", nil, "")
- }
-
- return nil
-}
-
-func writeFileLocally(f []byte, path string) *model.AppError {
- if err := os.MkdirAll(filepath.Dir(path), 0774); err != nil {
- directory, _ := filepath.Abs(filepath.Dir(path))
- return model.NewLocAppError("WriteFile", "api.file.write_file_locally.create_dir.app_error", nil, "directory="+directory+", err="+err.Error())
- }
-
- if err := ioutil.WriteFile(path, f, 0644); err != nil {
- return model.NewLocAppError("WriteFile", "api.file.write_file_locally.writing.app_error", nil, err.Error())
- }
-
- return nil
-}
-
-func ReadFile(path string) ([]byte, *model.AppError) {
- if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
- endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint
- accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId
- secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey
- secure := *utils.Cfg.FileSettings.AmazonS3SSL
- s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure)
- if err != nil {
- return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
- }
- bucket := utils.Cfg.FileSettings.AmazonS3Bucket
- minioObject, err := s3Clnt.GetObject(bucket, path)
- defer minioObject.Close()
- if err != nil {
- return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
- }
- if f, err := ioutil.ReadAll(minioObject); err != nil {
- return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
- } else {
- return f, nil
- }
- } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
- if f, err := ioutil.ReadFile(utils.Cfg.FileSettings.Directory + path); err != nil {
- return nil, model.NewLocAppError("ReadFile", "api.file.read_file.reading_local.app_error", nil, err.Error())
- } else {
- return f, nil
- }
- } else {
- return nil, model.NewLocAppError("ReadFile", "api.file.read_file.configured.app_error", nil, "")
- }
-}
-
-func openFileWriteStream(path string) (io.Writer, *model.AppError) {
- if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
- return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.s3.app_error", nil, "")
- } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
- if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+path), 0774); err != nil {
- return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.creating_dir.app_error", nil, err.Error())
- }
-
- if fileHandle, err := os.Create(utils.Cfg.FileSettings.Directory + path); err != nil {
- return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.local_server.app_error", nil, err.Error())
- } else {
- fileHandle.Chmod(0644)
- return fileHandle, nil
- }
- }
-
- return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.configured.app_error", nil, "")
-}
-
-func closeFileWriteStream(file io.Writer) {
- file.(*os.File).Close()
+ w.Write([]byte(model.StringToJson(app.GeneratePublicLink(c.GetSiteURL(), info))))
}
diff --git a/api/file_test.go b/api/file_test.go
index 5d440f112..ce3e1fab4 100644
--- a/api/file_test.go
+++ b/api/file_test.go
@@ -14,6 +14,7 @@ import (
"testing"
"time"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
@@ -59,7 +60,7 @@ func TestUploadFile(t *testing.T) {
}
var info *model.FileInfo
- if result := <-Srv.Store.FileInfo().Get(uploadInfo.Id); result.Err != nil {
+ if result := <-app.Srv.Store.FileInfo().Get(uploadInfo.Id); result.Err != nil {
t.Fatal(result.Err)
} else {
info = result.Data.(*model.FileInfo)
@@ -154,7 +155,7 @@ func TestGetFileInfo(t *testing.T) {
}
// Hacky way to assign file to a post (usually would be done by CreatePost call)
- store.Must(Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id))
+ store.Must(app.Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id))
// Other user shouldn't be able to get file info for this file if they're not in the channel for it
if _, err := Client.GetFileInfo(fileId); err == nil {
@@ -170,7 +171,7 @@ func TestGetFileInfo(t *testing.T) {
t.Fatal("other user got incorrect file")
}
- if err := cleanupTestFile(store.Must(Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil {
+ if err := cleanupTestFile(store.Must(app.Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil {
t.Fatal(err)
}
}
@@ -223,7 +224,7 @@ func TestGetFile(t *testing.T) {
}
// Hacky way to assign file to a post (usually would be done by CreatePost call)
- store.Must(Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id))
+ store.Must(app.Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id))
// Other user shouldn't be able to get file for this file if they're not in the channel for it
if _, err := Client.GetFile(fileId); err == nil {
@@ -252,7 +253,7 @@ func TestGetFile(t *testing.T) {
body.Close()
}
- if err := cleanupTestFile(store.Must(Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil {
+ if err := cleanupTestFile(store.Must(app.Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil {
t.Fatal(err)
}
}
@@ -292,7 +293,7 @@ func TestGetFileThumbnail(t *testing.T) {
}
// Hacky way to assign file to a post (usually would be done by CreatePost call)
- store.Must(Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id))
+ store.Must(app.Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id))
// Other user shouldn't be able to get thumbnail for this file if they're not in the channel for it
if _, err := Client.GetFileThumbnail(fileId); err == nil {
@@ -308,7 +309,7 @@ func TestGetFileThumbnail(t *testing.T) {
body.Close()
}
- if err := cleanupTestFile(store.Must(Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil {
+ if err := cleanupTestFile(store.Must(app.Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil {
t.Fatal(err)
}
}
@@ -348,7 +349,7 @@ func TestGetFilePreview(t *testing.T) {
}
// Hacky way to assign file to a post (usually would be done by CreatePost call)
- store.Must(Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id))
+ store.Must(app.Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id))
// Other user shouldn't be able to get preview for this file if they're not in the channel for it
if _, err := Client.GetFilePreview(fileId); err == nil {
@@ -364,7 +365,7 @@ func TestGetFilePreview(t *testing.T) {
body.Close()
}
- if err := cleanupTestFile(store.Must(Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil {
+ if err := cleanupTestFile(store.Must(app.Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil {
t.Fatal(err)
}
}
@@ -397,7 +398,7 @@ func TestGetPublicFile(t *testing.T) {
}
// Hacky way to assign file to a post (usually would be done by CreatePost call)
- store.Must(Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id))
+ store.Must(app.Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id))
link := Client.MustGeneric(Client.GetPublicLink(fileId)).(string)
@@ -430,7 +431,7 @@ func TestGetPublicFile(t *testing.T) {
t.Fatal("should've failed to get image with public link after salt changed")
}
- if err := cleanupTestFile(store.Must(Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil {
+ if err := cleanupTestFile(store.Must(app.Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil {
t.Fatal(err)
}
}
@@ -463,7 +464,7 @@ func TestGetPublicFileOld(t *testing.T) {
}
// Hacky way to assign file to a post (usually would be done by CreatePost call)
- store.Must(Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id))
+ store.Must(app.Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id))
// reconstruct old style of link
siteURL := *utils.Cfg.ServiceSettings.SiteURL
@@ -501,13 +502,13 @@ func TestGetPublicFileOld(t *testing.T) {
t.Fatal("should've failed to get image with public link after salt changed")
}
- if err := cleanupTestFile(store.Must(Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil {
+ if err := cleanupTestFile(store.Must(app.Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil {
t.Fatal(err)
}
}
func generatePublicLinkOld(siteURL, teamId, channelId, userId, filename string) string {
- hash := generatePublicLinkHash(filename, *utils.Cfg.FileSettings.PublicLinkSalt)
+ hash := app.GeneratePublicLinkHash(filename, *utils.Cfg.FileSettings.PublicLinkSalt)
return fmt.Sprintf("%s%s/public/files/get/%s/%s/%s/%s?h=%s", siteURL, model.API_URL_SUFFIX, teamId, channelId, userId, filename, hash)
}
@@ -540,7 +541,7 @@ func TestGetPublicLink(t *testing.T) {
}
// Hacky way to assign file to a post (usually would be done by CreatePost call)
- store.Must(Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id))
+ store.Must(app.Srv.Store.FileInfo().AttachToPost(fileId, th.BasicPost.Id))
utils.Cfg.FileSettings.EnablePublicLink = false
@@ -575,34 +576,11 @@ func TestGetPublicLink(t *testing.T) {
// Wait a bit for files to ready
time.Sleep(2 * time.Second)
- if err := cleanupTestFile(store.Must(Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil {
+ if err := cleanupTestFile(store.Must(app.Srv.Store.FileInfo().Get(fileId)).(*model.FileInfo)); err != nil {
t.Fatal(err)
}
}
-func TestGeneratePublicLinkHash(t *testing.T) {
- filename1 := model.NewId() + "/" + model.NewRandomString(16) + ".txt"
- filename2 := model.NewId() + "/" + model.NewRandomString(16) + ".txt"
- salt1 := model.NewRandomString(32)
- salt2 := model.NewRandomString(32)
-
- hash1 := generatePublicLinkHash(filename1, salt1)
- hash2 := generatePublicLinkHash(filename2, salt1)
- hash3 := generatePublicLinkHash(filename1, salt2)
-
- if hash1 != generatePublicLinkHash(filename1, salt1) {
- t.Fatal("hash should be equal for the same file name and salt")
- }
-
- if hash1 == hash2 {
- t.Fatal("hashes for different files should not be equal")
- }
-
- if hash1 == hash3 {
- t.Fatal("hashes for the same file with different salts should not be equal")
- }
-}
-
func TestMigrateFilenamesToFileInfos(t *testing.T) {
th := Setup().InitBasic()
@@ -631,7 +609,7 @@ func TestMigrateFilenamesToFileInfos(t *testing.T) {
}
// Bypass the Client whenever possible since we're trying to simulate a pre-3.5 post
- post1 := store.Must(Srv.Store.Post().Save(&model.Post{
+ post1 := store.Must(app.Srv.Store.Post().Save(&model.Post{
UserId: user1.Id,
ChannelId: channel1.Id,
Message: "test",
@@ -732,19 +710,19 @@ func TestFindTeamIdForFilename(t *testing.T) {
}
// Bypass the Client whenever possible since we're trying to simulate a pre-3.5 post
- post1 := store.Must(Srv.Store.Post().Save(&model.Post{
+ post1 := store.Must(app.Srv.Store.Post().Save(&model.Post{
UserId: user1.Id,
ChannelId: channel1.Id,
Message: "test",
Filenames: []string{fmt.Sprintf("/%s/%s/%s/%s", channel1.Id, user1.Id, fileId1, "test.png")},
})).(*model.Post)
- if teamId := findTeamIdForFilename(post1, post1.Filenames[0]); teamId != team1.Id {
+ if teamId := app.FindTeamIdForFilename(post1, post1.Filenames[0]); teamId != team1.Id {
t.Fatal("file should've been found under team1")
}
Client.SetTeamId(team2.Id)
- post2 := store.Must(Srv.Store.Post().Save(&model.Post{
+ post2 := store.Must(app.Srv.Store.Post().Save(&model.Post{
UserId: user1.Id,
ChannelId: channel2.Id,
Message: "test",
@@ -752,7 +730,7 @@ func TestFindTeamIdForFilename(t *testing.T) {
})).(*model.Post)
Client.SetTeamId(team1.Id)
- if teamId := findTeamIdForFilename(post2, post2.Filenames[0]); teamId != team2.Id {
+ if teamId := app.FindTeamIdForFilename(post2, post2.Filenames[0]); teamId != team2.Id {
t.Fatal("file should've been found under team2")
}
}
@@ -781,20 +759,20 @@ func TestGetInfoForFilename(t *testing.T) {
t.Fatal(err)
} else {
fileId1 = Client.MustGeneric(Client.UploadPostAttachment(data, channel1.Id, "test.png")).(*model.FileUploadResponse).FileInfos[0].Id
- path = store.Must(Srv.Store.FileInfo().Get(fileId1)).(*model.FileInfo).Path
- thumbnailPath = store.Must(Srv.Store.FileInfo().Get(fileId1)).(*model.FileInfo).ThumbnailPath
- previewPath = store.Must(Srv.Store.FileInfo().Get(fileId1)).(*model.FileInfo).PreviewPath
+ path = store.Must(app.Srv.Store.FileInfo().Get(fileId1)).(*model.FileInfo).Path
+ thumbnailPath = store.Must(app.Srv.Store.FileInfo().Get(fileId1)).(*model.FileInfo).ThumbnailPath
+ previewPath = store.Must(app.Srv.Store.FileInfo().Get(fileId1)).(*model.FileInfo).PreviewPath
}
// Bypass the Client whenever possible since we're trying to simulate a pre-3.5 post
- post1 := store.Must(Srv.Store.Post().Save(&model.Post{
+ post1 := store.Must(app.Srv.Store.Post().Save(&model.Post{
UserId: user1.Id,
ChannelId: channel1.Id,
Message: "test",
Filenames: []string{fmt.Sprintf("/%s/%s/%s/%s", channel1.Id, user1.Id, fileId1, "test.png")},
})).(*model.Post)
- if info := getInfoForFilename(post1, team1.Id, post1.Filenames[0]); info == nil {
+ if info := app.GetInfoForFilename(post1, team1.Id, post1.Filenames[0]); info == nil {
t.Fatal("info shouldn't be nil")
} else if info.Id == "" {
t.Fatal("info.Id shouldn't be empty")
diff --git a/api/general.go b/api/general.go
index 24855b821..5c8e45082 100644
--- a/api/general.go
+++ b/api/general.go
@@ -10,6 +10,7 @@ import (
l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
@@ -21,7 +22,7 @@ func InitGeneral() {
BaseRoutes.General.Handle("/log_client", ApiAppHandler(logClient)).Methods("POST")
BaseRoutes.General.Handle("/ping", ApiAppHandler(ping)).Methods("GET")
- BaseRoutes.WebSocket.Handle("ping", ApiWebSocketHandler(webSocketPing))
+ app.Srv.WebSocketRouter.Handle("ping", ApiWebSocketHandler(webSocketPing))
}
func getClientConfig(c *Context, w http.ResponseWriter, r *http.Request) {
diff --git a/api/license.go b/api/license.go
index 65ca90943..41cba914d 100644
--- a/api/license.go
+++ b/api/license.go
@@ -10,6 +10,7 @@ import (
"strings"
l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
@@ -29,7 +30,7 @@ func InitLicense() {
func LoadLicense() {
licenseId := ""
- if result := <-Srv.Store.System().Get(); result.Err == nil {
+ if result := <-app.Srv.Store.System().Get(); result.Err == nil {
props := result.Data.(model.StringMap)
licenseId = props[model.SYSTEM_ACTIVE_LICENSE_ID]
}
@@ -39,7 +40,7 @@ func LoadLicense() {
return
}
- if result := <-Srv.Store.License().Get(licenseId); result.Err == nil {
+ if result := <-app.Srv.Store.License().Get(licenseId); result.Err == nil {
record := result.Data.(*model.LicenseRecord)
utils.LoadLicense([]byte(record.Bytes))
} else {
@@ -104,7 +105,7 @@ func SaveLicense(licenseBytes []byte) (*model.License, *model.AppError) {
if success, licenseStr := utils.ValidateLicense(licenseBytes); success {
license = model.LicenseFromJson(strings.NewReader(licenseStr))
- if result := <-Srv.Store.User().AnalyticsUniqueUserCount(""); result.Err != nil {
+ if result := <-app.Srv.Store.User().AnalyticsUniqueUserCount(""); result.Err != nil {
return nil, model.NewLocAppError("addLicense", "api.license.add_license.invalid_count.app_error", nil, result.Err.Error())
} else {
uniqueUserCount := result.Data.(int64)
@@ -121,12 +122,12 @@ func SaveLicense(licenseBytes []byte) (*model.License, *model.AppError) {
record := &model.LicenseRecord{}
record.Id = license.Id
record.Bytes = string(licenseBytes)
- rchan := Srv.Store.License().Save(record)
+ rchan := app.Srv.Store.License().Save(record)
sysVar := &model.System{}
sysVar.Name = model.SYSTEM_ACTIVE_LICENSE_ID
sysVar.Value = license.Id
- schan := Srv.Store.System().SaveOrUpdate(sysVar)
+ schan := app.Srv.Store.System().SaveOrUpdate(sysVar)
if result := <-rchan; result.Err != nil {
RemoveLicense()
@@ -164,7 +165,7 @@ func RemoveLicense() *model.AppError {
sysVar.Name = model.SYSTEM_ACTIVE_LICENSE_ID
sysVar.Value = ""
- if result := <-Srv.Store.System().SaveOrUpdate(sysVar); result.Err != nil {
+ if result := <-app.Srv.Store.System().SaveOrUpdate(sysVar); result.Err != nil {
utils.RemoveLicense()
return result.Err
}
@@ -173,8 +174,7 @@ func RemoveLicense() *model.AppError {
}
func getClientLicenceConfig(c *Context, w http.ResponseWriter, r *http.Request) {
- useSanitizedLicense := !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM)
- c.Err = nil
+ useSanitizedLicense := !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM)
etag := utils.GetClientLicenseEtag(useSanitizedLicense)
if HandleEtag(etag, "Get Client License Config", w, r) {
diff --git a/api/oauth.go b/api/oauth.go
index 268cf1aed..abb216414 100644
--- a/api/oauth.go
+++ b/api/oauth.go
@@ -16,6 +16,7 @@ import (
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
@@ -53,33 +54,33 @@ func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToContext(c, model.PERMISSION_MANAGE_OAUTH) {
+ if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_OAUTH) {
c.Err = model.NewLocAppError("registerOAuthApp", "api.command.admin_only.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
return
}
- app := model.OAuthAppFromJson(r.Body)
+ oauthApp := model.OAuthAppFromJson(r.Body)
- if app == nil {
+ if oauthApp == nil {
c.SetInvalidParam("registerOAuthApp", "app")
return
}
secret := model.NewId()
- app.ClientSecret = secret
- app.CreatorId = c.Session.UserId
+ oauthApp.ClientSecret = secret
+ oauthApp.CreatorId = c.Session.UserId
- if result := <-Srv.Store.OAuth().SaveApp(app); result.Err != nil {
+ if result := <-app.Srv.Store.OAuth().SaveApp(oauthApp); result.Err != nil {
c.Err = result.Err
return
} else {
- app = result.Data.(*model.OAuthApp)
+ oauthApp = result.Data.(*model.OAuthApp)
- c.LogAudit("client_id=" + app.Id)
+ c.LogAudit("client_id=" + oauthApp.Id)
- w.Write([]byte(app.ToJson()))
+ w.Write([]byte(oauthApp.ToJson()))
return
}
@@ -92,18 +93,18 @@ func getOAuthApps(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToContext(c, model.PERMISSION_MANAGE_OAUTH) {
+ if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_OAUTH) {
c.Err = model.NewLocAppError("getOAuthApps", "api.command.admin_only.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
return
}
var ochan store.StoreChannel
- if HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH) {
- ochan = Srv.Store.OAuth().GetApps()
+ if app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH) {
+ ochan = app.Srv.Store.OAuth().GetApps()
} else {
c.Err = nil
- ochan = Srv.Store.OAuth().GetAppByUser(c.Session.UserId)
+ ochan = app.Srv.Store.OAuth().GetAppByUser(c.Session.UserId)
}
if result := <-ochan; result.Err != nil {
@@ -126,16 +127,16 @@ func getOAuthAppInfo(c *Context, w http.ResponseWriter, r *http.Request) {
clientId := params["client_id"]
- var app *model.OAuthApp
- if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil {
+ var oauthApp *model.OAuthApp
+ if result := <-app.Srv.Store.OAuth().GetApp(clientId); result.Err != nil {
c.Err = model.NewLocAppError("getOAuthAppInfo", "api.oauth.allow_oauth.database.app_error", nil, "")
return
} else {
- app = result.Data.(*model.OAuthApp)
+ oauthApp = result.Data.(*model.OAuthApp)
}
- app.Sanitize()
- w.Write([]byte(app.ToJson()))
+ oauthApp.Sanitize()
+ w.Write([]byte(oauthApp.ToJson()))
}
func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -177,15 +178,15 @@ func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
scope = model.DEFAULT_SCOPE
}
- var app *model.OAuthApp
- if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil {
+ var oauthApp *model.OAuthApp
+ if result := <-app.Srv.Store.OAuth().GetApp(clientId); result.Err != nil {
c.Err = model.NewLocAppError("allowOAuth", "api.oauth.allow_oauth.database.app_error", nil, "")
return
} else {
- app = result.Data.(*model.OAuthApp)
+ oauthApp = result.Data.(*model.OAuthApp)
}
- if !app.IsValidRedirectURL(redirectUri) {
+ if !oauthApp.IsValidRedirectURL(redirectUri) {
c.LogAudit("fail - redirect_uri did not match registered callback")
c.Err = model.NewLocAppError("allowOAuth", "api.oauth.allow_oauth.redirect_callback.app_error", nil, "")
c.Err.StatusCode = http.StatusBadRequest
@@ -209,13 +210,13 @@ func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
Value: scope,
}
- if result := <-Srv.Store.Preference().Save(&model.Preferences{authorizedApp}); result.Err != nil {
+ if result := <-app.Srv.Store.Preference().Save(&model.Preferences{authorizedApp}); result.Err != nil {
responseData["redirect"] = redirectUri + "?error=server_error&state=" + state
w.Write([]byte(model.MapToJson(responseData)))
return
}
- if result := <-Srv.Store.OAuth().SaveAuthData(authData); result.Err != nil {
+ if result := <-app.Srv.Store.OAuth().SaveAuthData(authData); result.Err != nil {
responseData["redirect"] = redirectUri + "?error=server_error&state=" + state
w.Write([]byte(model.MapToJson(responseData)))
return
@@ -234,7 +235,7 @@ func getAuthorizedApps(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- ochan := Srv.Store.OAuth().GetAuthorizedApps(c.Session.UserId)
+ ochan := app.Srv.Store.OAuth().GetAuthorizedApps(c.Session.UserId)
if result := <-ochan; result.Err != nil {
c.Err = result.Err
return
@@ -249,34 +250,8 @@ func getAuthorizedApps(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
-func RevokeAccessToken(token string) *model.AppError {
-
- session := GetSession(token)
- schan := Srv.Store.Session().Remove(token)
-
- if result := <-Srv.Store.OAuth().GetAccessData(token); result.Err != nil {
- return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.get.app_error", nil, "")
- }
-
- tchan := Srv.Store.OAuth().RemoveAccessData(token)
-
- if result := <-tchan; result.Err != nil {
- return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_token.app_error", nil, "")
- }
-
- if result := <-schan; result.Err != nil {
- return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_session.app_error", nil, "")
- }
-
- if session != nil {
- RemoveAllSessionsForUserId(session.UserId)
- }
-
- return nil
-}
-
func GetAuthData(code string) *model.AuthData {
- if result := <-Srv.Store.OAuth().GetAuthData(code); result.Err != nil {
+ if result := <-app.Srv.Store.OAuth().GetAuthData(code); result.Err != nil {
l4g.Error(utils.T("api.oauth.get_auth_data.find.error"), code)
return nil
} else {
@@ -310,7 +285,11 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
action := props["action"]
switch action {
case model.OAUTH_ACTION_SIGNUP:
- CreateOAuthUser(c, w, r, service, body, teamId)
+ if user, err := app.CreateOAuthUser(service, body, teamId); err != nil {
+ c.Err = err
+ } else {
+ doLogin(c, w, r, user, "")
+ }
if c.Err == nil {
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
}
@@ -318,7 +297,7 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
case model.OAUTH_ACTION_LOGIN:
user := LoginByOAuth(c, w, r, service, body)
if len(teamId) > 0 {
- c.Err = JoinUserToTeamById(teamId, user)
+ c.Err = app.AddUserToTeamByTeamId(teamId, user)
}
if c.Err == nil {
if val, ok := props["redirect_to"]; ok {
@@ -372,12 +351,12 @@ func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- var app *model.OAuthApp
- if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil {
+ var oauthApp *model.OAuthApp
+ if result := <-app.Srv.Store.OAuth().GetApp(clientId); result.Err != nil {
c.Err = result.Err
return
} else {
- app = result.Data.(*model.OAuthApp)
+ oauthApp = result.Data.(*model.OAuthApp)
}
// here we should check if the user is logged in
@@ -387,13 +366,13 @@ func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
}
isAuthorized := false
- if result := <-Srv.Store.Preference().Get(c.Session.UserId, model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, clientId); result.Err == nil {
+ if result := <-app.Srv.Store.Preference().Get(c.Session.UserId, model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, clientId); result.Err == nil {
// when we support scopes we should check if the scopes match
isAuthorized = true
}
// Automatically allow if the app is trusted
- if app.IsTrusted || isAuthorized {
+ if oauthApp.IsTrusted || isAuthorized {
closeBody := func(r *http.Response) {
if r.Body != nil {
ioutil.ReadAll(r.Body)
@@ -481,16 +460,16 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- var app *model.OAuthApp
- achan := Srv.Store.OAuth().GetApp(clientId)
+ var oauthApp *model.OAuthApp
+ achan := app.Srv.Store.OAuth().GetApp(clientId)
if result := <-achan; result.Err != nil {
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "")
return
} else {
- app = result.Data.(*model.OAuthApp)
+ oauthApp = result.Data.(*model.OAuthApp)
}
- if app.ClientSecret != secret {
+ if oauthApp.ClientSecret != secret {
c.LogAudit("fail - invalid client credentials")
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "")
return
@@ -510,7 +489,7 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
}
if authData.IsExpired() {
- <-Srv.Store.OAuth().RemoveAuthData(authData.Code)
+ <-app.Srv.Store.OAuth().RemoveAuthData(authData.Code)
c.LogAudit("fail - auth code expired")
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "")
return
@@ -528,7 +507,7 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- uchan := Srv.Store.User().Get(authData.UserId)
+ uchan := app.Srv.Store.User().Get(authData.UserId)
if result := <-uchan; result.Err != nil {
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "")
return
@@ -536,14 +515,14 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
user = result.Data.(*model.User)
}
- tchan := Srv.Store.OAuth().GetPreviousAccessData(user.Id, clientId)
+ tchan := app.Srv.Store.OAuth().GetPreviousAccessData(user.Id, clientId)
if result := <-tchan; result.Err != nil {
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal.app_error", nil, "")
return
} else if result.Data != nil {
accessData := result.Data.(*model.AccessData)
if accessData.IsExpired() {
- if access, err := newSessionUpdateToken(app.Name, accessData, user); err != nil {
+ if access, err := newSessionUpdateToken(oauthApp.Name, accessData, user); err != nil {
c.Err = err
return
} else {
@@ -560,7 +539,7 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
} else {
// create a new session and return new access token
var session *model.Session
- if result, err := newSession(app.Name, user); err != nil {
+ if result, err := newSession(oauthApp.Name, user); err != nil {
c.Err = err
return
} else {
@@ -569,7 +548,7 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
accessData = &model.AccessData{ClientId: clientId, UserId: user.Id, Token: session.Token, RefreshToken: model.NewId(), RedirectUri: redirectUri, ExpiresAt: session.ExpiresAt}
- if result := <-Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil {
+ if result := <-app.Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil {
l4g.Error(result.Err)
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_saving.app_error", nil, "")
return
@@ -583,10 +562,10 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
- <-Srv.Store.OAuth().RemoveAuthData(authData.Code)
+ <-app.Srv.Store.OAuth().RemoveAuthData(authData.Code)
} else {
// when grantType is refresh_token
- if result := <-Srv.Store.OAuth().GetAccessDataByRefreshToken(refreshToken); result.Err != nil {
+ if result := <-app.Srv.Store.OAuth().GetAccessDataByRefreshToken(refreshToken); result.Err != nil {
c.LogAudit("fail - refresh token is invalid")
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.refresh_token.app_error", nil, "")
return
@@ -594,7 +573,7 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
accessData = result.Data.(*model.AccessData)
}
- uchan := Srv.Store.User().Get(accessData.UserId)
+ uchan := app.Srv.Store.User().Get(accessData.UserId)
if result := <-uchan; result.Err != nil {
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "")
return
@@ -602,7 +581,7 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
user = result.Data.(*model.User)
}
- if access, err := newSessionUpdateToken(app.Name, accessData, user); err != nil {
+ if access, err := newSessionUpdateToken(oauthApp.Name, accessData, user); err != nil {
c.Err = err
return
} else {
@@ -668,7 +647,7 @@ func getTeamIdFromQuery(query url.Values) (string, *model.AppError) {
return props["id"], nil
} else if len(inviteId) > 0 {
- if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil {
+ if result := <-app.Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil {
// soft fail, so we still create user but don't auto-join team
l4g.Error("%v", result.Err)
} else {
@@ -844,24 +823,29 @@ func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request,
}
var user *model.User
- if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil {
+ if result := <-app.Srv.Store.User().GetByEmail(email); result.Err != nil {
c.Err = result.Err
return
} else {
user = result.Data.(*model.User)
}
- RevokeAllSession(c, user.Id)
- if c.Err != nil {
+ if err := app.RevokeAllSessions(user.Id); err != nil {
+ c.Err = err
return
}
+ c.LogAuditWithUserId(user.Id, "Revoked all sessions for user")
- if result := <-Srv.Store.User().UpdateAuthData(user.Id, service, &authData, ssoEmail, true); result.Err != nil {
+ if result := <-app.Srv.Store.User().UpdateAuthData(user.Id, service, &authData, ssoEmail, true); result.Err != nil {
c.Err = result.Err
return
}
- go sendSignInChangeEmail(c, user.Email, c.GetSiteURL(), strings.Title(service)+" SSO")
+ go func() {
+ if err := app.SendSignInChangeEmail(user.Email, strings.Title(service)+" SSO", user.Locale, c.GetSiteURL()); err != nil {
+ l4g.Error(err.Error())
+ }
+ }()
}
func deleteOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -871,7 +855,7 @@ func deleteOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToContext(c, model.PERMISSION_MANAGE_OAUTH) {
+ if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_OAUTH) {
c.Err = model.NewLocAppError("deleteOAuthApp", "api.command.admin_only.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
return
@@ -887,18 +871,18 @@ func deleteOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.OAuth().GetApp(id); result.Err != nil {
+ if result := <-app.Srv.Store.OAuth().GetApp(id); result.Err != nil {
c.Err = result.Err
return
} else {
- if c.Session.UserId != result.Data.(*model.OAuthApp).CreatorId && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH) {
+ if c.Session.UserId != result.Data.(*model.OAuthApp).CreatorId && !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH) {
c.LogAudit("fail - inappropriate permissions")
c.Err = model.NewLocAppError("deleteOAuthApp", "api.oauth.delete.permissions.app_error", nil, "user_id="+c.Session.UserId)
return
}
}
- if err := (<-Srv.Store.OAuth().DeleteApp(id)).Err; err != nil {
+ if err := (<-app.Srv.Store.OAuth().DeleteApp(id)).Err; err != nil {
c.Err = err
return
}
@@ -923,19 +907,19 @@ func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
}
// revoke app sessions
- if result := <-Srv.Store.OAuth().GetAccessDataByUserForApp(c.Session.UserId, id); result.Err != nil {
+ if result := <-app.Srv.Store.OAuth().GetAccessDataByUserForApp(c.Session.UserId, id); result.Err != nil {
c.Err = result.Err
return
} else {
accessData := result.Data.([]*model.AccessData)
for _, a := range accessData {
- if err := RevokeAccessToken(a.Token); err != nil {
+ if err := app.RevokeAccessToken(a.Token); err != nil {
c.Err = err
return
}
- if rad := <-Srv.Store.OAuth().RemoveAccessData(a.Token); rad.Err != nil {
+ if rad := <-app.Srv.Store.OAuth().RemoveAccessData(a.Token); rad.Err != nil {
c.Err = rad.Err
return
}
@@ -943,7 +927,7 @@ func deauthorizeOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
}
// Deauthorize the app
- if err := (<-Srv.Store.Preference().Delete(c.Session.UserId, model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, id)).Err; err != nil {
+ if err := (<-app.Srv.Store.Preference().Delete(c.Session.UserId, model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, id)).Err; err != nil {
c.Err = err
return
}
@@ -967,26 +951,26 @@ func regenerateOAuthSecret(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- var app *model.OAuthApp
- if result := <-Srv.Store.OAuth().GetApp(id); result.Err != nil {
+ var oauthApp *model.OAuthApp
+ if result := <-app.Srv.Store.OAuth().GetApp(id); result.Err != nil {
c.Err = model.NewLocAppError("regenerateOAuthSecret", "api.oauth.allow_oauth.database.app_error", nil, "")
return
} else {
- app = result.Data.(*model.OAuthApp)
+ oauthApp = result.Data.(*model.OAuthApp)
- if app.CreatorId != c.Session.UserId && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH) {
+ if oauthApp.CreatorId != c.Session.UserId && !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH) {
c.Err = model.NewLocAppError("registerOAuthApp", "api.command.admin_only.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
return
}
- app.ClientSecret = model.NewId()
- if update := <-Srv.Store.OAuth().UpdateApp(app); update.Err != nil {
+ oauthApp.ClientSecret = model.NewId()
+ if update := <-app.Srv.Store.OAuth().UpdateApp(oauthApp); update.Err != nil {
c.Err = update.Err
return
}
- w.Write([]byte(app.ToJson()))
+ w.Write([]byte(oauthApp.ToJson()))
return
}
}
@@ -999,11 +983,11 @@ func newSession(appName string, user *model.User) (*model.Session, *model.AppErr
session.AddProp(model.SESSION_PROP_OS, "OAuth2")
session.AddProp(model.SESSION_PROP_BROWSER, "OAuth2")
- if result := <-Srv.Store.Session().Save(session); result.Err != nil {
+ if result := <-app.Srv.Store.Session().Save(session); result.Err != nil {
return nil, model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_session.app_error", nil, "")
} else {
session = result.Data.(*model.Session)
- AddSessionToCache(session)
+ app.AddSessionToCache(session)
}
return session, nil
@@ -1011,7 +995,7 @@ func newSession(appName string, user *model.User) (*model.Session, *model.AppErr
func newSessionUpdateToken(appName string, accessData *model.AccessData, user *model.User) (*model.AccessResponse, *model.AppError) {
var session *model.Session
- <-Srv.Store.Session().Remove(accessData.Token) //remove the previous session
+ <-app.Srv.Store.Session().Remove(accessData.Token) //remove the previous session
if result, err := newSession(appName, user); err != nil {
return nil, err
@@ -1021,7 +1005,7 @@ func newSessionUpdateToken(appName string, accessData *model.AccessData, user *m
accessData.Token = session.Token
accessData.ExpiresAt = session.ExpiresAt
- if result := <-Srv.Store.OAuth().UpdateAccessData(accessData); result.Err != nil {
+ if result := <-app.Srv.Store.OAuth().UpdateAccessData(accessData); result.Err != nil {
l4g.Error(result.Err)
return nil, model.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "")
}
diff --git a/api/post.go b/api/post.go
index bdbd32d2e..4d1425c18 100644
--- a/api/post.go
+++ b/api/post.go
@@ -4,38 +4,21 @@
package api
import (
- "crypto/tls"
- "fmt"
- "html"
- "html/template"
- "io"
- "io/ioutil"
"net/http"
- "net/url"
- "path/filepath"
- "regexp"
- "sort"
"strconv"
- "strings"
- "time"
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
- "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
- "github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
- "github.com/nicksnyder/go-i18n/i18n"
-)
-
-const (
- TRIGGERWORDS_FULL = 0
- TRIGGERWORDS_STARTSWITH = 1
)
func InitPost() {
l4g.Debug(utils.T("api.post.init.debug"))
+ BaseRoutes.ApiRoot.Handle("/get_opengraph_metadata", ApiUserRequired(getOpenGraphMetadata)).Methods("POST")
+
BaseRoutes.NeedTeam.Handle("/posts/search", ApiUserRequiredActivity(searchPosts, true)).Methods("POST")
BaseRoutes.NeedTeam.Handle("/posts/flagged/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequired(getFlaggedPosts)).Methods("GET")
BaseRoutes.NeedTeam.Handle("/posts/{post_id}", ApiUserRequired(getPostById)).Methods("GET")
@@ -59,1151 +42,25 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
c.SetInvalidParam("createPost", "post")
return
}
- post.UserId = c.Session.UserId
-
- cchan := Srv.Store.Channel().Get(post.ChannelId, true)
-
- if !HasPermissionToChannelContext(c, post.ChannelId, model.PERMISSION_CREATE_POST) {
- return
- }
-
- // Check that channel has not been deleted
- var channel *model.Channel
- if result := <-cchan; result.Err != nil {
- c.SetInvalidParam("createPost", "post.channelId")
- return
- } else {
- channel = result.Data.(*model.Channel)
- }
-
- if channel.DeleteAt != 0 {
- c.Err = model.NewLocAppError("createPost", "api.post.create_post.can_not_post_to_deleted.error", nil, "")
- c.Err.StatusCode = http.StatusBadRequest
- return
- }
-
- if rp, err := CreatePost(c, post, true); err != nil {
- c.Err = err
-
- if c.Err.Id == "api.post.create_post.root_id.app_error" ||
- c.Err.Id == "api.post.create_post.channel_root_id.app_error" ||
- c.Err.Id == "api.post.create_post.parent_id.app_error" {
- c.Err.StatusCode = http.StatusBadRequest
- }
-
- return
- } else {
- // Update the LastViewAt only if the post does not have from_webhook prop set (eg. Zapier app)
- if _, ok := post.Props["from_webhook"]; !ok {
- if result := <-Srv.Store.Channel().UpdateLastViewedAt([]string{post.ChannelId}, c.Session.UserId); result.Err != nil {
- l4g.Error(utils.T("api.post.create_post.last_viewed.error"), post.ChannelId, c.Session.UserId, result.Err)
- }
- }
-
- w.Write([]byte(rp.ToJson()))
- }
-}
-
-func CreatePost(c *Context, post *model.Post, triggerWebhooks bool) (*model.Post, *model.AppError) {
- var pchan store.StoreChannel
- if len(post.RootId) > 0 {
- pchan = Srv.Store.Post().Get(post.RootId)
- }
-
- // Verify the parent/child relationships are correct
- if pchan != nil {
- if presult := <-pchan; presult.Err != nil {
- return nil, model.NewLocAppError("createPost", "api.post.create_post.root_id.app_error", nil, "")
- } else {
- list := presult.Data.(*model.PostList)
- if len(list.Posts) == 0 || !list.IsChannelId(post.ChannelId) {
- return nil, model.NewLocAppError("createPost", "api.post.create_post.channel_root_id.app_error", nil, "")
- }
-
- if post.ParentId == "" {
- post.ParentId = post.RootId
- }
-
- if post.RootId != post.ParentId {
- parent := list.Posts[post.ParentId]
- if parent == nil {
- return nil, model.NewLocAppError("createPost", "api.post.create_post.parent_id.app_error", nil, "")
- }
- }
- }
- }
-
- if post.CreateAt != 0 && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
- post.CreateAt = 0
- c.Err = nil
- }
-
- post.Hashtags, _ = model.ParseHashtags(post.Message)
-
- var rpost *model.Post
- if result := <-Srv.Store.Post().Save(post); result.Err != nil {
- return nil, result.Err
- } else {
- rpost = result.Data.(*model.Post)
- }
-
- if einterfaces.GetMetricsInterface() != nil {
- einterfaces.GetMetricsInterface().IncrementPostCreate()
- }
-
- if len(post.FileIds) > 0 {
- // There's a rare bug where the client sends up duplicate FileIds so protect against that
- post.FileIds = utils.RemoveDuplicatesFromStringArray(post.FileIds)
-
- for _, fileId := range post.FileIds {
- if result := <-Srv.Store.FileInfo().AttachToPost(fileId, post.Id); result.Err != nil {
- l4g.Error(utils.T("api.post.create_post.attach_files.error"), post.Id, post.FileIds, c.Session.UserId, result.Err)
- }
- }
-
- if einterfaces.GetMetricsInterface() != nil {
- einterfaces.GetMetricsInterface().IncrementPostFileAttachment(len(post.FileIds))
- }
- }
-
- InvalidateCacheForChannel(rpost.ChannelId)
- InvalidateCacheForChannelPosts(rpost.ChannelId)
-
- handlePostEvents(c, rpost, triggerWebhooks)
-
- return rpost, nil
-}
-
-var linkWithTextRegex *regexp.Regexp = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
-
-func CreateWebhookPost(c *Context, channelId, text, overrideUsername, overrideIconUrl string, props model.StringInterface, postType string) (*model.Post, *model.AppError) {
- post := &model.Post{UserId: c.Session.UserId, ChannelId: channelId, Message: text, Type: postType}
- post.AddProp("from_webhook", "true")
-
- if utils.Cfg.ServiceSettings.EnablePostUsernameOverride {
- if len(overrideUsername) != 0 {
- post.AddProp("override_username", overrideUsername)
- } else {
- post.AddProp("override_username", model.DEFAULT_WEBHOOK_USERNAME)
- }
- }
-
- if utils.Cfg.ServiceSettings.EnablePostIconOverride {
- if len(overrideIconUrl) != 0 {
- post.AddProp("override_icon_url", overrideIconUrl)
- }
- }
-
- post.Message = parseSlackLinksToMarkdown(post.Message)
-
- if len(props) > 0 {
- for key, val := range props {
- if key == "attachments" {
- parseSlackAttachment(post, val)
- } else if key != "override_icon_url" && key != "override_username" && key != "from_webhook" {
- post.AddProp(key, val)
- }
- }
- }
-
- if _, err := CreatePost(c, post, false); err != nil {
- return nil, model.NewLocAppError("CreateWebhookPost", "api.post.create_webhook_post.creating.app_error", nil, "err="+err.Message)
- }
- return post, nil
-}
-
-func CreateCommandPost(c *Context, post *model.Post, response *model.CommandResponse) {
- post.Message = parseSlackLinksToMarkdown(response.Text)
post.UserId = c.Session.UserId
- post.CreateAt = model.GetMillis()
-
- if response.Attachments != nil {
- parseSlackAttachment(post, response.Attachments)
- }
-
- switch response.ResponseType {
- case model.COMMAND_RESPONSE_TYPE_IN_CHANNEL:
- if _, err := CreatePost(c, post, true); err != nil {
- c.Err = model.NewLocAppError("command", "api.command.execute_command.save.app_error", nil, "")
- }
- case model.COMMAND_RESPONSE_TYPE_EPHEMERAL:
- if response.Text == "" {
- return
- }
-
- post.ParentId = ""
- SendEphemeralPost(c.TeamId, c.Session.UserId, post)
- }
-}
-
-// This method only parses and processes the attachments,
-// all else should be set in the post which is passed
-func parseSlackAttachment(post *model.Post, attachments interface{}) {
- post.Type = model.POST_SLACK_ATTACHMENT
-
- if list, success := attachments.([]interface{}); success {
- for i, aInt := range list {
- attachment := aInt.(map[string]interface{})
- if aText, ok := attachment["text"].(string); ok {
- aText = linkWithTextRegex.ReplaceAllString(aText, "[${2}](${1})")
- attachment["text"] = aText
- list[i] = attachment
- }
- if aText, ok := attachment["pretext"].(string); ok {
- aText = linkWithTextRegex.ReplaceAllString(aText, "[${2}](${1})")
- attachment["pretext"] = aText
- list[i] = attachment
- }
- if fVal, ok := attachment["fields"]; ok {
- if fields, ok := fVal.([]interface{}); ok {
- // parse attachment field links into Markdown format
- for j, fInt := range fields {
- field := fInt.(map[string]interface{})
- if fValue, ok := field["value"].(string); ok {
- fValue = linkWithTextRegex.ReplaceAllString(fValue, "[${2}](${1})")
- field["value"] = fValue
- fields[j] = field
- }
- }
- attachment["fields"] = fields
- list[i] = attachment
- }
- }
- }
- post.AddProp("attachments", list)
- }
-}
-
-func parseSlackLinksToMarkdown(text string) string {
- return linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
-}
-
-func handlePostEvents(c *Context, post *model.Post, triggerWebhooks bool) {
- tchan := Srv.Store.Team().Get(c.TeamId)
- cchan := Srv.Store.Channel().Get(post.ChannelId, true)
- uchan := Srv.Store.User().Get(post.UserId)
-
- var team *model.Team
- if result := <-tchan; result.Err != nil {
- l4g.Error(utils.T("api.post.handle_post_events_and_forget.team.error"), c.TeamId, result.Err)
- return
- } else {
- team = result.Data.(*model.Team)
- }
-
- var channel *model.Channel
- if result := <-cchan; result.Err != nil {
- l4g.Error(utils.T("api.post.handle_post_events_and_forget.channel.error"), post.ChannelId, result.Err)
- return
- } else {
- channel = result.Data.(*model.Channel)
- }
-
- sendNotifications(c, post, team, channel)
-
- var user *model.User
- if result := <-uchan; result.Err != nil {
- l4g.Error(utils.T("api.post.handle_post_events_and_forget.user.error"), post.UserId, result.Err)
- return
- } else {
- user = result.Data.(*model.User)
- }
-
- if triggerWebhooks {
- go handleWebhookEvents(c, post, team, channel, user)
- }
-
- if channel.Type == model.CHANNEL_DIRECT {
- go makeDirectChannelVisible(post.ChannelId)
- }
-}
-
-func makeDirectChannelVisible(channelId string) {
- var members []model.ChannelMember
- if result := <-Srv.Store.Channel().GetMembers(channelId); result.Err != nil {
- l4g.Error(utils.T("api.post.make_direct_channel_visible.get_members.error"), channelId, result.Err.Message)
- return
- } else {
- members = result.Data.([]model.ChannelMember)
- }
-
- if len(members) != 2 {
- l4g.Error(utils.T("api.post.make_direct_channel_visible.get_2_members.error"), channelId)
- return
- }
-
- // make sure the channel is visible to both members
- for i, member := range members {
- otherUserId := members[1-i].UserId
-
- if result := <-Srv.Store.Preference().Get(member.UserId, model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, otherUserId); result.Err != nil {
- // create a new preference since one doesn't exist yet
- preference := &model.Preference{
- UserId: member.UserId,
- Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
- Name: otherUserId,
- Value: "true",
- }
-
- if saveResult := <-Srv.Store.Preference().Save(&model.Preferences{*preference}); saveResult.Err != nil {
- l4g.Error(utils.T("api.post.make_direct_channel_visible.save_pref.error"), member.UserId, otherUserId, saveResult.Err.Message)
- } else {
- message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCE_CHANGED, "", "", member.UserId, nil)
- message.Add("preference", preference.ToJson())
-
- go Publish(message)
- }
- } else {
- preference := result.Data.(model.Preference)
-
- if preference.Value != "true" {
- // update the existing preference to make the channel visible
- preference.Value = "true"
-
- if updateResult := <-Srv.Store.Preference().Save(&model.Preferences{preference}); updateResult.Err != nil {
- l4g.Error(utils.T("api.post.make_direct_channel_visible.update_pref.error"), member.UserId, otherUserId, updateResult.Err.Message)
- } else {
- message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCE_CHANGED, "", "", member.UserId, nil)
- message.Add("preference", preference.ToJson())
-
- go Publish(message)
- }
- }
- }
- }
-}
-
-func handleWebhookEvents(c *Context, post *model.Post, team *model.Team, channel *model.Channel, user *model.User) {
- if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks {
- return
- }
-
- if channel.Type != model.CHANNEL_OPEN {
- return
- }
-
- hchan := Srv.Store.Webhook().GetOutgoingByTeam(c.TeamId)
- result := <-hchan
- if result.Err != nil {
- l4g.Error(utils.T("api.post.handle_webhook_events_and_forget.getting.error"), result.Err)
- return
- }
-
- hooks := result.Data.([]*model.OutgoingWebhook)
- if len(hooks) == 0 {
- return
- }
-
- splitWords := strings.Fields(post.Message)
- if len(splitWords) == 0 {
- return
- }
- firstWord := splitWords[0]
-
- relevantHooks := []*model.OutgoingWebhook{}
- for _, hook := range hooks {
- if hook.ChannelId == post.ChannelId || len(hook.ChannelId) == 0 {
- if hook.ChannelId == post.ChannelId && len(hook.TriggerWords) == 0 {
- relevantHooks = append(relevantHooks, hook)
- } else if hook.TriggerWhen == TRIGGERWORDS_FULL && hook.HasTriggerWord(firstWord) {
- relevantHooks = append(relevantHooks, hook)
- } else if hook.TriggerWhen == TRIGGERWORDS_STARTSWITH && hook.TriggerWordStartsWith(firstWord) {
- relevantHooks = append(relevantHooks, hook)
- }
- }
- }
-
- for _, hook := range relevantHooks {
- go func(hook *model.OutgoingWebhook) {
- payload := &model.OutgoingWebhookPayload{
- Token: hook.Token,
- TeamId: hook.TeamId,
- TeamDomain: team.Name,
- ChannelId: post.ChannelId,
- ChannelName: channel.Name,
- Timestamp: post.CreateAt,
- UserId: post.UserId,
- UserName: user.Username,
- PostId: post.Id,
- Text: post.Message,
- TriggerWord: firstWord,
- }
- var body io.Reader
- var contentType string
- if hook.ContentType == "application/json" {
- body = strings.NewReader(payload.ToJSON())
- contentType = "application/json"
- } else {
- body = strings.NewReader(payload.ToFormValues())
- contentType = "application/x-www-form-urlencoded"
- }
- tr := &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections},
- }
- client := &http.Client{Transport: tr}
-
- for _, url := range hook.CallbackURLs {
- go func(url string) {
- req, _ := http.NewRequest("POST", url, body)
- req.Header.Set("Content-Type", contentType)
- req.Header.Set("Accept", "application/json")
- if resp, err := client.Do(req); err != nil {
- l4g.Error(utils.T("api.post.handle_webhook_events_and_forget.event_post.error"), err.Error())
- } else {
- defer func() {
- ioutil.ReadAll(resp.Body)
- resp.Body.Close()
- }()
- respProps := model.MapFromJson(resp.Body)
-
- // copy the context and create a mock session for posting the message
- mockSession := model.Session{
- UserId: hook.CreatorId,
- TeamMembers: []*model.TeamMember{{TeamId: hook.TeamId, UserId: hook.CreatorId}},
- IsOAuth: false,
- }
-
- newContext := &Context{
- Session: mockSession,
- RequestId: model.NewId(),
- IpAddress: "",
- Path: c.Path,
- Err: nil,
- teamURLValid: c.teamURLValid,
- teamURL: c.teamURL,
- siteURL: c.siteURL,
- T: c.T,
- Locale: c.Locale,
- TeamId: hook.TeamId,
- }
-
- if text, ok := respProps["text"]; ok {
- if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"], post.Props, post.Type); err != nil {
- l4g.Error(utils.T("api.post.handle_webhook_events_and_forget.create_post.error"), err)
- }
- }
- }
- }(url)
- }
-
- }(hook)
- }
-}
-
-// Given a map of user IDs to profiles, returns a list of mention
-// keywords for all users in the channel.
-func getMentionKeywordsInChannel(profiles map[string]*model.User) map[string][]string {
- keywords := make(map[string][]string)
-
- for id, profile := range profiles {
- userMention := "@" + strings.ToLower(profile.Username)
- keywords[userMention] = append(keywords[userMention], id)
-
- if len(profile.NotifyProps["mention_keys"]) > 0 {
- // Add all the user's mention keys
- splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",")
- for _, k := range splitKeys {
- // note that these are made lower case so that we can do a case insensitive check for them
- key := strings.ToLower(k)
- keywords[key] = append(keywords[key], id)
- }
- }
-
- // If turned on, add the user's case sensitive first name
- if profile.NotifyProps["first_name"] == "true" {
- keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id)
- }
-
- // Add @channel and @all to keywords if user has them turned on
- if int64(len(profiles)) < *utils.Cfg.TeamSettings.MaxNotificationsPerChannel && profile.NotifyProps["channel"] == "true" {
- keywords["@channel"] = append(keywords["@channel"], profile.Id)
- keywords["@all"] = append(keywords["@all"], profile.Id)
- }
- }
-
- return keywords
-}
-
-// Given a message and a map mapping mention keywords to the users who use them, returns a map of mentioned
-// users and a slice of potencial mention users not in the channel and whether or not @here was mentioned.
-func getExplicitMentions(message string, keywords map[string][]string) (map[string]bool, []string, bool, bool, bool) {
- mentioned := make(map[string]bool)
- potentialOthersMentioned := make([]string, 0)
- systemMentions := map[string]bool{"@here": true, "@channel": true, "@all": true}
- hereMentioned := false
- allMentioned := false
- channelMentioned := false
-
- addMentionedUsers := func(ids []string) {
- for _, id := range ids {
- mentioned[id] = true
- }
- }
-
- for _, word := range strings.Fields(message) {
- isMention := false
-
- if word == "@here" {
- hereMentioned = true
- }
-
- if word == "@channel" {
- channelMentioned = true
- }
-
- if word == "@all" {
- allMentioned = true
- }
-
- // Non-case-sensitive check for regular keys
- if ids, match := keywords[strings.ToLower(word)]; match {
- addMentionedUsers(ids)
- isMention = true
- }
-
- // Case-sensitive check for first name
- if ids, match := keywords[word]; match {
- addMentionedUsers(ids)
- isMention = true
- }
-
- if !isMention {
- // No matches were found with the string split just on whitespace so try further splitting
- // the message on punctuation
- splitWords := strings.FieldsFunc(word, func(c rune) bool {
- return model.SplitRunes[c]
- })
-
- for _, splitWord := range splitWords {
- if splitWord == "@here" {
- hereMentioned = true
- }
-
- if splitWord == "@all" {
- allMentioned = true
- }
-
- if splitWord == "@channel" {
- channelMentioned = true
- }
-
- // Non-case-sensitive check for regular keys
- if ids, match := keywords[strings.ToLower(splitWord)]; match {
- addMentionedUsers(ids)
- }
-
- // Case-sensitive check for first name
- if ids, match := keywords[splitWord]; match {
- addMentionedUsers(ids)
- } else if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") {
- username := word[1:len(splitWord)]
- potentialOthersMentioned = append(potentialOthersMentioned, username)
- }
- }
- }
- }
-
- return mentioned, potentialOthersMentioned, hereMentioned, channelMentioned, allMentioned
-}
-
-func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel) []string {
- mentionedUsersList := make([]string, 0)
- var fchan store.StoreChannel
- var senderUsername string
-
- if post.IsSystemMessage() {
- senderUsername = c.T("system.message.name")
- } else {
- pchan := Srv.Store.User().GetProfilesInChannel(channel.Id, -1, -1, true)
- fchan = Srv.Store.FileInfo().GetForPost(post.Id)
-
- var profileMap map[string]*model.User
- if result := <-pchan; result.Err != nil {
- l4g.Error(utils.T("api.post.handle_post_events_and_forget.profiles.error"), c.TeamId, result.Err)
- return nil
- } else {
- profileMap = result.Data.(map[string]*model.User)
- }
-
- // If the user who made the post is mention don't send a notification
- if _, ok := profileMap[post.UserId]; !ok {
- l4g.Error(utils.T("api.post.send_notifications_and_forget.user_id.error"), post.UserId)
- return nil
- }
-
- mentionedUserIds := make(map[string]bool)
- allActivityPushUserIds := []string{}
- hereNotification := false
- channelNotification := false
- allNotification := false
- updateMentionChans := []store.StoreChannel{}
-
- if channel.Type == model.CHANNEL_DIRECT {
- var otherUserId string
- if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId {
- otherUserId = userIds[1]
- } else {
- otherUserId = userIds[0]
- }
-
- mentionedUserIds[otherUserId] = true
- if post.Props["from_webhook"] == "true" {
- mentionedUserIds[post.UserId] = true
- }
- } else {
- keywords := getMentionKeywordsInChannel(profileMap)
-
- var potentialOtherMentions []string
- mentionedUserIds, potentialOtherMentions, hereNotification, channelNotification, allNotification = getExplicitMentions(post.Message, keywords)
-
- // get users that have comment thread mentions enabled
- if len(post.RootId) > 0 {
- if result := <-Srv.Store.Post().Get(post.RootId); result.Err != nil {
- l4g.Error(utils.T("api.post.send_notifications_and_forget.comment_thread.error"), post.RootId, result.Err)
- return nil
- } else {
- list := result.Data.(*model.PostList)
-
- for _, threadPost := range list.Posts {
- if profile, ok := profileMap[threadPost.UserId]; ok {
- if profile.NotifyProps["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == list.Order[0]) {
- mentionedUserIds[threadPost.UserId] = true
- }
- }
- }
- }
- }
-
- // prevent the user from mentioning themselves
- if post.Props["from_webhook"] != "true" {
- delete(mentionedUserIds, post.UserId)
- }
-
- if len(potentialOtherMentions) > 0 {
- if result := <-Srv.Store.User().GetProfilesByUsernames(potentialOtherMentions, team.Id); result.Err == nil {
- outOfChannelMentions := result.Data.(map[string]*model.User)
- go sendOutOfChannelMentions(c, post, outOfChannelMentions)
- }
- }
-
- // find which users in the channel are set up to always receive mobile notifications
- for _, profile := range profileMap {
- if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL &&
- (post.UserId != profile.Id || post.Props["from_webhook"] == "true") {
- allActivityPushUserIds = append(allActivityPushUserIds, profile.Id)
- }
- }
- }
-
- mentionedUsersList = make([]string, 0, len(mentionedUserIds))
- for id := range mentionedUserIds {
- mentionedUsersList = append(mentionedUsersList, id)
- updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id))
- }
-
- var sender *model.User
- senderName := make(map[string]string)
- for _, id := range mentionedUsersList {
- senderName[id] = ""
- if profile, ok := profileMap[post.UserId]; ok {
- if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" {
- senderName[id] = value.(string)
- } else {
- //Get the Display name preference from the receiver
- if result := <-Srv.Store.Preference().Get(id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, "name_format"); result.Err != nil {
- // Show default sender's name if user doesn't set display settings.
- senderName[id] = profile.Username
- } else {
- senderName[id] = profile.GetDisplayNameForPreference(result.Data.(model.Preference).Value)
- }
- }
- sender = profile
- }
- }
-
- if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" {
- senderUsername = value.(string)
- } else {
- senderUsername = profileMap[post.UserId].Username
- }
-
- if utils.Cfg.EmailSettings.SendEmailNotifications {
- for _, id := range mentionedUsersList {
- userAllowsEmails := profileMap[id].NotifyProps["email"] != "false"
-
- var status *model.Status
- var err *model.AppError
- if status, err = GetStatus(id); err != nil {
- status = &model.Status{
- UserId: id,
- Status: model.STATUS_OFFLINE,
- Manual: false,
- LastActivityAt: 0,
- ActiveChannel: "",
- }
- }
-
- if userAllowsEmails && status.Status != model.STATUS_ONLINE {
- sendNotificationEmail(c, post, profileMap[id], channel, team, senderName[id], sender)
- }
- }
- }
-
- // If the channel has more than 1K users then @here is disabled
- if hereNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
- hereNotification = false
- SendEphemeralPost(
- c.TeamId,
- post.UserId,
- &model.Post{
- ChannelId: post.ChannelId,
- Message: c.T("api.post.disabled_here", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}),
- CreateAt: post.CreateAt + 1,
- },
- )
- }
-
- // If the channel has more than 1K users then @channel is disabled
- if channelNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
- SendEphemeralPost(
- c.TeamId,
- post.UserId,
- &model.Post{
- ChannelId: post.ChannelId,
- Message: c.T("api.post.disabled_channel", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}),
- CreateAt: post.CreateAt + 1,
- },
- )
- }
-
- // If the channel has more than 1K users then @all is disabled
- if allNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
- SendEphemeralPost(
- c.TeamId,
- post.UserId,
- &model.Post{
- ChannelId: post.ChannelId,
- Message: c.T("api.post.disabled_all", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}),
- CreateAt: post.CreateAt + 1,
- },
- )
- }
-
- if hereNotification {
- statuses := GetAllStatuses()
- for _, status := range statuses {
- if status.UserId == post.UserId {
- continue
- }
-
- _, profileFound := profileMap[status.UserId]
- _, alreadyMentioned := mentionedUserIds[status.UserId]
-
- if status.Status == model.STATUS_ONLINE && profileFound && !alreadyMentioned {
- mentionedUsersList = append(mentionedUsersList, status.UserId)
- updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, status.UserId))
- }
- }
- }
-
- // Make sure all mention updates are complete to prevent race
- // Probably better to batch these DB updates in the future
- // MUST be completed before push notifications send
- for _, uchan := range updateMentionChans {
- if result := <-uchan; result.Err != nil {
- l4g.Warn(utils.T("api.post.update_mention_count_and_forget.update_error"), post.Id, post.ChannelId, result.Err)
- }
- }
-
- sendPushNotifications := false
- if *utils.Cfg.EmailSettings.SendPushNotifications {
- pushServer := *utils.Cfg.EmailSettings.PushNotificationServer
- if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) {
- l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn"))
- sendPushNotifications = false
- } else {
- sendPushNotifications = true
- }
- }
-
- if sendPushNotifications {
- for _, id := range mentionedUsersList {
- var status *model.Status
- var err *model.AppError
- if status, err = GetStatus(id); err != nil {
- status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""}
- }
-
- if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) {
- sendPushNotification(post, profileMap[id], channel, senderName[id], true)
- }
- }
-
- for _, id := range allActivityPushUserIds {
- if _, ok := mentionedUserIds[id]; !ok {
- var status *model.Status
- var err *model.AppError
- if status, err = GetStatus(id); err != nil {
- status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""}
- }
-
- if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) {
- sendPushNotification(post, profileMap[id], channel, senderName[id], false)
- }
- }
- }
- }
- }
-
- message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POSTED, "", post.ChannelId, "", nil)
- message.Add("post", post.ToJson())
- message.Add("channel_type", channel.Type)
- message.Add("channel_display_name", channel.DisplayName)
- message.Add("channel_name", channel.Name)
- message.Add("sender_name", senderUsername)
- message.Add("team_id", team.Id)
-
- if len(post.FileIds) != 0 && fchan != nil {
- message.Add("otherFile", "true")
-
- var infos []*model.FileInfo
- if result := <-fchan; result.Err != nil {
- l4g.Warn(utils.T("api.post.send_notifications.files.error"), post.Id, result.Err)
- } else {
- infos = result.Data.([]*model.FileInfo)
- }
-
- for _, info := range infos {
- if info.IsImage() {
- message.Add("image", "true")
- break
- }
- }
- }
-
- if len(mentionedUsersList) != 0 {
- message.Add("mentions", model.ArrayToJson(mentionedUsersList))
- }
-
- Publish(message)
- return mentionedUsersList
-}
-
-func sendNotificationEmail(c *Context, post *model.Post, user *model.User, channel *model.Channel, team *model.Team, senderName string, sender *model.User) {
- // skip if inactive
- if user.DeleteAt > 0 {
- return
- }
-
- if channel.Type == model.CHANNEL_DIRECT && channel.TeamId != team.Id {
- // this message is a cross-team DM so it we need to find a team that the recipient is on to use in the link
- if result := <-Srv.Store.Team().GetTeamsByUserId(user.Id); result.Err != nil {
- l4g.Error(utils.T("api.post.send_notifications_and_forget.get_teams.error"), user.Id, result.Err)
- return
- } else {
- // if the recipient isn't in the current user's team, just pick one
- teams := result.Data.([]*model.Team)
- found := false
-
- for i := range teams {
- if teams[i].Id == team.Id {
- found = true
- team = teams[i]
- break
- }
- }
-
- if !found {
- if len(teams) > 0 {
- team = teams[0]
- } else {
- // in case the user hasn't joined any teams we send them to the select_team page
- team = &model.Team{Name: "select_team", DisplayName: utils.Cfg.TeamSettings.SiteName}
- }
- }
- }
- }
-
- if *utils.Cfg.EmailSettings.EnableEmailBatching {
- var sendBatched bool
-
- if result := <-Srv.Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_NOTIFICATIONS, model.PREFERENCE_NAME_EMAIL_INTERVAL); result.Err != nil {
- // if the call fails, assume it hasn't been set and use the default
- sendBatched = false
- } else {
- // default to not using batching if the setting is set to immediate
- sendBatched = result.Data.(model.Preference).Value != model.PREFERENCE_DEFAULT_EMAIL_INTERVAL
- }
-
- if sendBatched {
- if err := AddNotificationEmailToBatch(user, post, team); err == nil {
- return
- }
- }
-
- // fall back to sending a single email if we can't batch it for some reason
- }
-
- var channelName string
- var bodyText string
- var subjectText string
- var mailTemplate string
- var mailParameters map[string]interface{}
-
- teamURL := c.GetSiteURL() + "/" + team.Name
- tm := time.Unix(post.CreateAt/1000, 0)
-
- userLocale := utils.GetUserTranslations(user.Locale)
- month := userLocale(tm.Month().String())
- day := fmt.Sprintf("%d", tm.Day())
- year := fmt.Sprintf("%d", tm.Year())
- zone, _ := tm.Zone()
-
- if channel.Type == model.CHANNEL_DIRECT {
- bodyText = userLocale("api.post.send_notifications_and_forget.message_body")
- subjectText = userLocale("api.post.send_notifications_and_forget.message_subject")
-
- senderDisplayName := senderName
-
- mailTemplate = "api.templates.post_subject_in_direct_message"
- mailParameters = map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName,
- "SenderDisplayName": senderDisplayName, "Month": month, "Day": day, "Year": year}
- } else {
- bodyText = userLocale("api.post.send_notifications_and_forget.mention_body")
- subjectText = userLocale("api.post.send_notifications_and_forget.mention_subject")
- channelName = channel.DisplayName
- mailTemplate = "api.templates.post_subject_in_channel"
- mailParameters = map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName,
- "ChannelName": channelName, "Month": month, "Day": day, "Year": year}
- }
-
- subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, userLocale(mailTemplate, mailParameters))
-
- bodyPage := utils.NewHTMLTemplate("post_body", user.Locale)
- bodyPage.Props["SiteURL"] = c.GetSiteURL()
- bodyPage.Props["PostMessage"] = getMessageForNotification(post, userLocale)
- if team.Name != "select_team" {
- bodyPage.Props["TeamLink"] = teamURL + "/pl/" + post.Id
- } else {
- bodyPage.Props["TeamLink"] = teamURL
- }
-
- bodyPage.Props["BodyText"] = bodyText
- bodyPage.Props["Button"] = userLocale("api.templates.post_body.button")
- bodyPage.Html["Info"] = template.HTML(userLocale("api.templates.post_body.info",
- map[string]interface{}{"ChannelName": channelName, "SenderName": senderName,
- "Hour": fmt.Sprintf("%02d", tm.Hour()), "Minute": fmt.Sprintf("%02d", tm.Minute()),
- "TimeZone": zone, "Month": month, "Day": day}))
-
- go func() {
- if err := utils.SendMail(user.Email, html.UnescapeString(subject), bodyPage.Render()); err != nil {
- l4g.Error(utils.T("api.post.send_notifications_and_forget.send.error"), user.Email, err)
- }
- }()
-
- if einterfaces.GetMetricsInterface() != nil {
- einterfaces.GetMetricsInterface().IncrementPostSentEmail()
- }
-}
-
-func getMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
- if len(strings.TrimSpace(post.Message)) != 0 || len(post.FileIds) == 0 {
- return post.Message
- }
-
- // extract the filenames from their paths and determine what type of files are attached
- var infos []*model.FileInfo
- if result := <-Srv.Store.FileInfo().GetForPost(post.Id); result.Err != nil {
- l4g.Warn(utils.T("api.post.get_message_for_notification.get_files.error"), post.Id, result.Err)
- } else {
- infos = result.Data.([]*model.FileInfo)
- }
-
- filenames := make([]string, len(infos))
- onlyImages := true
- for i, info := range infos {
- if escaped, err := url.QueryUnescape(filepath.Base(info.Name)); err != nil {
- // this should never error since filepath was escaped using url.QueryEscape
- filenames[i] = escaped
- } else {
- filenames[i] = info.Name
- }
-
- onlyImages = onlyImages && info.IsImage()
- }
-
- props := map[string]interface{}{"Filenames": strings.Join(filenames, ", ")}
-
- if onlyImages {
- return translateFunc("api.post.get_message_for_notification.images_sent", len(filenames), props)
- } else {
- return translateFunc("api.post.get_message_for_notification.files_sent", len(filenames), props)
- }
-}
-
-func sendPushNotification(post *model.Post, user *model.User, channel *model.Channel, senderName string, wasMentioned bool) {
- sessions := getMobileAppSessions(user.Id)
-
- if sessions == nil {
- return
- }
-
- var channelName string
-
- if channel.Type == model.CHANNEL_DIRECT {
- channelName = senderName
- } else {
- channelName = channel.DisplayName
- }
-
- userLocale := utils.GetUserTranslations(user.Locale)
-
- msg := model.PushNotification{}
- if badge := <-Srv.Store.User().GetUnreadCount(user.Id); badge.Err != nil {
- msg.Badge = 1
- l4g.Error(utils.T("store.sql_user.get_unread_count.app_error"), user.Id, badge.Err)
- } else {
- msg.Badge = int(badge.Data.(int64))
- }
- msg.Type = model.PUSH_TYPE_MESSAGE
- msg.TeamId = channel.TeamId
- msg.ChannelId = channel.Id
- msg.ChannelName = channel.Name
-
- if *utils.Cfg.EmailSettings.PushNotificationContents == model.FULL_NOTIFICATION {
- if channel.Type == model.CHANNEL_DIRECT {
- msg.Category = model.CATEGORY_DM
- msg.Message = "@" + senderName + ": " + model.ClearMentionTags(post.Message)
- } else {
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_in") + channelName + ": " + model.ClearMentionTags(post.Message)
- }
- } else {
- if channel.Type == model.CHANNEL_DIRECT {
- msg.Category = model.CATEGORY_DM
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message")
- } else if wasMentioned {
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName
- } else {
- msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention") + channelName
- }
- }
-
- l4g.Debug(utils.T("api.post.send_notifications_and_forget.push_notification.debug"), msg.DeviceId, msg.Message)
-
- for _, session := range sessions {
- tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson()))
- tmpMessage.SetDeviceIdAndPlatform(session.DeviceId)
- go sendToPushProxy(tmpMessage)
- if einterfaces.GetMetricsInterface() != nil {
- einterfaces.GetMetricsInterface().IncrementPostSentPush()
- }
- }
-}
-func clearPushNotification(userId string, channelId string) {
- sessions := getMobileAppSessions(userId)
- if sessions == nil {
+ if !app.SessionHasPermissionToChannel(c.Session, post.ChannelId, model.PERMISSION_CREATE_POST) {
+ c.SetPermissionError(model.PERMISSION_CREATE_POST)
return
}
- msg := model.PushNotification{}
- msg.Type = model.PUSH_TYPE_CLEAR
- msg.ChannelId = channelId
- msg.ContentAvailable = 0
- if badge := <-Srv.Store.User().GetUnreadCount(userId); badge.Err != nil {
- msg.Badge = 0
- l4g.Error(utils.T("store.sql_user.get_unread_count.app_error"), userId, badge.Err)
- } else {
- msg.Badge = int(badge.Data.(int64))
- }
-
- l4g.Debug(utils.T("api.post.send_notifications_and_forget.clear_push_notification.debug"), msg.DeviceId, msg.ChannelId)
- for _, session := range sessions {
- tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson()))
- tmpMessage.SetDeviceIdAndPlatform(session.DeviceId)
- go sendToPushProxy(tmpMessage)
- }
-}
-
-func sendToPushProxy(msg model.PushNotification) {
- msg.ServerId = utils.CfgDiagnosticId
-
- tr := &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections},
- }
- httpClient := &http.Client{Transport: tr}
- request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+model.API_URL_SUFFIX_V1+"/send_push", strings.NewReader(msg.ToJson()))
-
- if resp, err := httpClient.Do(request); err != nil {
- l4g.Error(utils.T("api.post.send_notifications_and_forget.push_notification.error"), msg.DeviceId, err)
- } else {
- ioutil.ReadAll(resp.Body)
- resp.Body.Close()
- }
-}
-
-func getMobileAppSessions(userId string) []*model.Session {
- if result := <-Srv.Store.Session().GetSessionsWithActiveDeviceIds(userId); result.Err != nil {
- l4g.Error(utils.T("api.post.send_notifications_and_forget.sessions.error"), userId, result.Err)
- return nil
- } else {
- return result.Data.([]*model.Session)
+ if post.CreateAt != 0 && !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ post.CreateAt = 0
}
-}
-func sendOutOfChannelMentions(c *Context, post *model.Post, profiles map[string]*model.User) {
- if len(profiles) == 0 {
+ rp, err := app.CreatePostAsUser(post, c.TeamId)
+ if err != nil {
+ c.Err = err
return
}
- var usernames []string
- for _, user := range profiles {
- usernames = append(usernames, user.Username)
- }
- sort.Strings(usernames)
-
- var message string
- if len(usernames) == 1 {
- message = c.T("api.post.check_for_out_of_channel_mentions.message.one", map[string]interface{}{
- "Username": usernames[0],
- })
- } else {
- message = c.T("api.post.check_for_out_of_channel_mentions.message.multiple", map[string]interface{}{
- "Usernames": strings.Join(usernames[:len(usernames)-1], ", "),
- "LastUsername": usernames[len(usernames)-1],
- })
- }
-
- SendEphemeralPost(
- c.TeamId,
- post.UserId,
- &model.Post{
- ChannelId: post.ChannelId,
- Message: message,
- CreateAt: post.CreateAt + 1,
- },
- )
-}
-
-func SendEphemeralPost(teamId, userId string, post *model.Post) {
- post.Type = model.POST_EPHEMERAL
-
- // fill in fields which haven't been specified which have sensible defaults
- if post.Id == "" {
- post.Id = model.NewId()
- }
- if post.CreateAt == 0 {
- post.CreateAt = model.GetMillis()
- }
- if post.Props == nil {
- post.Props = model.StringInterface{}
- }
-
- message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_EPHEMERAL_MESSAGE, "", post.ChannelId, userId, nil)
- message.Add("post", post.ToJson())
-
- go Publish(message)
+ w.Write([]byte(rp.ToJson()))
}
func updatePost(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -1214,66 +71,20 @@ func updatePost(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- pchan := Srv.Store.Post().Get(post.Id)
-
- if !HasPermissionToChannelContext(c, post.ChannelId, model.PERMISSION_EDIT_POST) {
+ if !app.SessionHasPermissionToChannel(c.Session, post.ChannelId, model.PERMISSION_EDIT_POST) {
+ c.SetPermissionError(model.PERMISSION_EDIT_POST)
return
}
- var oldPost *model.Post
- if result := <-pchan; result.Err != nil {
- c.Err = result.Err
- return
- } else {
- oldPost = result.Data.(*model.PostList).Posts[post.Id]
-
- if oldPost == nil {
- c.Err = model.NewLocAppError("updatePost", "api.post.update_post.find.app_error", nil, "id="+post.Id)
- c.Err.StatusCode = http.StatusBadRequest
- return
- }
-
- if oldPost.UserId != c.Session.UserId {
- c.Err = model.NewLocAppError("updatePost", "api.post.update_post.permissions.app_error", nil, "oldUserId="+oldPost.UserId)
- c.Err.StatusCode = http.StatusForbidden
- return
- }
-
- if oldPost.DeleteAt != 0 {
- c.Err = model.NewLocAppError("updatePost", "api.post.update_post.permissions.app_error", nil,
- c.T("api.post.update_post.permissions_details.app_error", map[string]interface{}{"PostId": post.Id}))
- c.Err.StatusCode = http.StatusForbidden
- return
- }
-
- if oldPost.IsSystemMessage() {
- c.Err = model.NewLocAppError("updatePost", "api.post.update_post.system_message.app_error", nil, "id="+post.Id)
- c.Err.StatusCode = http.StatusForbidden
- return
- }
- }
-
- newPost := &model.Post{}
- *newPost = *oldPost
-
- newPost.Message = post.Message
- newPost.Hashtags, _ = model.ParseHashtags(post.Message)
+ post.UserId = c.Session.UserId
- if result := <-Srv.Store.Post().Update(newPost, oldPost); result.Err != nil {
- c.Err = result.Err
+ rpost, err := app.UpdatePost(post)
+ if err != nil {
+ c.Err = err
return
- } else {
- rpost := result.Data.(*model.Post)
-
- message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", rpost.ChannelId, "", nil)
- message.Add("post", rpost.ToJson())
-
- go Publish(message)
-
- InvalidateCacheForChannelPosts(rpost.ChannelId)
-
- w.Write([]byte(rpost.ToJson()))
}
+
+ w.Write([]byte(rpost.ToJson()))
}
func getFlaggedPosts(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -1291,16 +102,12 @@ func getFlaggedPosts(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- posts := &model.PostList{}
-
- if result := <-Srv.Store.Post().GetFlaggedPosts(c.Session.UserId, offset, limit); result.Err != nil {
- c.Err = result.Err
+ if posts, err := app.GetFlaggedPosts(c.Session.UserId, offset, limit); err != nil {
+ c.Err = err
return
} else {
- posts = result.Data.(*model.PostList)
+ w.Write([]byte(posts.ToJson()))
}
-
- w.Write([]byte(posts.ToJson()))
}
func getPosts(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -1324,26 +131,21 @@ func getPosts(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- etagChan := Srv.Store.Post().GetEtag(id, true)
-
- if !HasPermissionToChannelContext(c, id, model.PERMISSION_CREATE_POST) {
+ if !app.SessionHasPermissionToChannel(c.Session, id, model.PERMISSION_CREATE_POST) {
+ c.SetPermissionError(model.PERMISSION_CREATE_POST)
return
}
- etag := (<-etagChan).Data.(string)
+ etag := app.GetPostsEtag(id)
if HandleEtag(etag, "Get Posts", w, r) {
return
}
- pchan := Srv.Store.Post().GetPosts(id, offset, limit, true)
-
- if result := <-pchan; result.Err != nil {
- c.Err = result.Err
+ if list, err := app.GetPosts(id, offset, limit); err != nil {
+ c.Err = err
return
} else {
- list := result.Data.(*model.PostList)
-
w.Header().Set(model.HEADER_ETAG_SERVER, etag)
w.Write([]byte(list.ToJson()))
}
@@ -1365,18 +167,15 @@ func getPostsSince(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- pchan := Srv.Store.Post().GetPostsSince(id, time, true)
-
- if !HasPermissionToChannelContext(c, id, model.PERMISSION_READ_CHANNEL) {
+ if !app.SessionHasPermissionToChannel(c.Session, id, model.PERMISSION_READ_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
- if result := <-pchan; result.Err != nil {
- c.Err = result.Err
+ if list, err := app.GetPostsSince(id, time); err != nil {
+ c.Err = err
return
} else {
- list := result.Data.(*model.PostList)
-
w.Write([]byte(list.ToJson()))
}
@@ -1397,20 +196,17 @@ func getPost(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- pchan := Srv.Store.Post().Get(postId)
-
- if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
+ if !app.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_READ_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
- if result := <-pchan; result.Err != nil {
- c.Err = result.Err
+ if list, err := app.GetPostThread(postId); err != nil {
+ c.Err = err
return
- } else if HandleEtag(result.Data.(*model.PostList).Etag(), "Get Post", w, r) {
+ } else if HandleEtag(list.Etag(), "Get Post", w, r) {
return
} else {
- list := result.Data.(*model.PostList)
-
if !list.IsChannelId(channelId) {
c.Err = model.NewLocAppError("getPost", "api.post.get_post.permissions.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
@@ -1431,19 +227,18 @@ func getPostById(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.Post().Get(postId); result.Err != nil {
- c.Err = result.Err
+ if list, err := app.GetPostThread(postId); err != nil {
+ c.Err = err
return
} else {
- list := result.Data.(*model.PostList)
-
if len(list.Order) != 1 {
c.Err = model.NewLocAppError("getPostById", "api.post_get_post_by_id.get.app_error", nil, "")
return
}
post := list.Posts[list.Order[0]]
- if !HasPermissionToChannelContext(c, post.ChannelId, model.PERMISSION_READ_CHANNEL) {
+ if !app.SessionHasPermissionToChannel(c.Session, post.ChannelId, model.PERMISSION_READ_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
@@ -1465,33 +260,17 @@ func getPermalinkTmp(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.Post().Get(postId); result.Err != nil {
- c.Err = result.Err
+ if !app.HasPermissionToChannelByPost(c.Session.UserId, postId, model.PERMISSION_JOIN_PUBLIC_CHANNELS) {
+ c.SetPermissionError(model.PERMISSION_JOIN_PUBLIC_CHANNELS)
return
- } else {
- list := result.Data.(*model.PostList)
-
- if len(list.Order) != 1 {
- c.Err = model.NewLocAppError("getPermalinkTmp", "api.post_get_post_by_id.get.app_error", nil, "")
- return
- }
- post := list.Posts[list.Order[0]]
-
- // Because we confuse permissions and membership in Mattermost's model, we have to just
- // try to join the channel without checking if we already have permission to it. This is
- // because system admins have permissions to every channel but are not nessisary a member
- // of every channel. If we checked here then system admins would skip joining the channel and
- // error when they tried to view it.
- if err, _ := JoinChannelById(c, c.Session.UserId, post.ChannelId); err != nil {
- // On error just return with permissions error
- c.Err = err
- return
- }
-
- if HandleEtag(list.Etag(), "Get Permalink TMP", w, r) {
- return
- }
+ }
+ if list, err := app.GetPermalinkPost(postId, c.Session.UserId); err != nil {
+ c.Err = err
+ return
+ } else if HandleEtag(list.Etag(), "Get Permalink TMP", w, r) {
+ return
+ } else {
w.Header().Set(model.HEADER_ETAG_SERVER, list.Etag())
w.Write([]byte(list.ToJson()))
}
@@ -1512,73 +291,32 @@ func deletePost(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_EDIT_POST) {
+ if !app.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_DELETE_POST) {
+ c.SetPermissionError(model.PERMISSION_DELETE_POST)
return
}
- pchan := Srv.Store.Post().Get(postId)
+ if !app.SessionHasPermissionToPost(c.Session, postId, model.PERMISSION_DELETE_OTHERS_POSTS) {
+ c.SetPermissionError(model.PERMISSION_DELETE_OTHERS_POSTS)
+ return
+ }
- if result := <-pchan; result.Err != nil {
- c.Err = result.Err
+ if post, err := app.DeletePost(postId); err != nil {
+ c.Err = err
return
} else {
-
- post := result.Data.(*model.PostList).Posts[postId]
-
- if post == nil {
- c.SetInvalidParam("deletePost", "postId")
- return
- }
-
if post.ChannelId != channelId {
c.Err = model.NewLocAppError("deletePost", "api.post.delete_post.permissions.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
return
}
- if post.UserId != c.Session.UserId && !HasPermissionToChannelContext(c, post.ChannelId, model.PERMISSION_EDIT_OTHERS_POSTS) {
- c.Err = model.NewLocAppError("deletePost", "api.post.delete_post.permissions.app_error", nil, "")
- c.Err.StatusCode = http.StatusForbidden
- return
- }
-
- if dresult := <-Srv.Store.Post().Delete(postId, model.GetMillis()); dresult.Err != nil {
- c.Err = dresult.Err
- return
- }
-
- message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_DELETED, "", post.ChannelId, "", nil)
- message.Add("post", post.ToJson())
-
- go Publish(message)
- go DeletePostFiles(post)
- go DeleteFlaggedPost(c.Session.UserId, post)
-
- InvalidateCacheForChannelPosts(post.ChannelId)
-
result := make(map[string]string)
result["id"] = postId
w.Write([]byte(model.MapToJson(result)))
}
}
-func DeleteFlaggedPost(userId string, post *model.Post) {
- if result := <-Srv.Store.Preference().Delete(userId, model.PREFERENCE_CATEGORY_FLAGGED_POST, post.Id); result.Err != nil {
- l4g.Warn(utils.T("api.post.delete_flagged_post.app_error.warn"), result.Err)
- return
- }
-}
-
-func DeletePostFiles(post *model.Post) {
- if len(post.FileIds) != 0 {
- return
- }
-
- if result := <-Srv.Store.FileInfo().DeleteForPost(post.Id); result.Err != nil {
- l4g.Warn(utils.T("api.post.delete_post_files.app_error.warn"), post.Id, result.Err)
- }
-}
-
func getPostsBefore(c *Context, w http.ResponseWriter, r *http.Request) {
getPostsBeforeOrAfter(c, w, r, true)
}
@@ -1614,31 +352,22 @@ func getPostsBeforeOrAfter(c *Context, w http.ResponseWriter, r *http.Request, b
return
}
- // We can do better than this etag in this situation
- etagChan := Srv.Store.Post().GetEtag(id, true)
-
- if !HasPermissionToChannelContext(c, id, model.PERMISSION_READ_CHANNEL) {
+ if !app.SessionHasPermissionToChannel(c.Session, id, model.PERMISSION_READ_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
- etag := (<-etagChan).Data.(string)
+ // We can do better than this etag in this situation
+ etag := app.GetPostsEtag(id)
+
if HandleEtag(etag, "Get Posts Before or After", w, r) {
return
}
- var pchan store.StoreChannel
- if before {
- pchan = Srv.Store.Post().GetPostsBefore(id, postId, numPosts, offset)
- } else {
- pchan = Srv.Store.Post().GetPostsAfter(id, postId, numPosts, offset)
- }
-
- if result := <-pchan; result.Err != nil {
- c.Err = result.Err
+ if list, err := app.GetPostsAroundPost(postId, id, offset, numPosts, before); err != nil {
+ c.Err = err
return
} else {
- list := result.Data.(*model.PostList)
-
w.Header().Set(model.HEADER_ETAG_SERVER, etag)
w.Write([]byte(list.ToJson()))
}
@@ -1658,26 +387,10 @@ func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) {
isOrSearch = val.(bool)
}
- paramsList := model.ParseSearchParams(terms)
- channels := []store.StoreChannel{}
-
- for _, params := range paramsList {
- params.OrTerms = isOrSearch
- // don't allow users to search for everything
- if params.Terms != "*" {
- channels = append(channels, Srv.Store.Post().Search(c.TeamId, c.Session.UserId, params))
- }
- }
-
- posts := &model.PostList{}
- for _, channel := range channels {
- if result := <-channel; result.Err != nil {
- c.Err = result.Err
- return
- } else {
- data := result.Data.(*model.PostList)
- posts.Extend(data)
- }
+ posts, err := app.SearchPostsInTeam(terms, c.Session.UserId, c.TeamId, isOrSearch)
+ if err != nil {
+ c.Err = err
+ return
}
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
@@ -1699,44 +412,38 @@ func getFileInfosForPost(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- pchan := Srv.Store.Post().Get(postId)
- fchan := Srv.Store.FileInfo().GetForPost(postId)
-
- if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
+ if !app.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_READ_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
- var infos []*model.FileInfo
- if result := <-fchan; result.Err != nil {
- c.Err = result.Err
+ if infos, err := app.GetFileInfosForPost(postId); err != nil {
+ c.Err = err
+ return
+ } else if HandleEtag(model.GetEtagForFileInfos(infos), "Get File Infos For Post", w, r) {
return
} else {
- infos = result.Data.([]*model.FileInfo)
+ w.Header().Set("Cache-Control", "max-age=2592000, public")
+ w.Header().Set(model.HEADER_ETAG_SERVER, model.GetEtagForFileInfos(infos))
+ w.Write([]byte(model.FileInfosToJson(infos)))
}
+}
- if len(infos) == 0 {
- // No FileInfos were returned so check if they need to be created for this post
- var post *model.Post
- if result := <-pchan; result.Err != nil {
- c.Err = result.Err
- return
- } else {
- post = result.Data.(*model.PostList).Posts[postId]
- }
+func getOpenGraphMetadata(c *Context, w http.ResponseWriter, r *http.Request) {
+ props := model.StringInterfaceFromJson(r.Body)
- if len(post.Filenames) > 0 {
- // The post has Filenames that need to be replaced with FileInfos
- infos = migrateFilenamesToFileInfos(post)
- }
+ url := ""
+ ok := false
+ if url, ok = props["url"].(string); len(url) == 0 || !ok {
+ c.SetInvalidParam("getOpenGraphMetadata", "url")
+ return
}
- etag := model.GetEtagForFileInfos(infos)
+ og := app.GetOpenGraphMetadata(url)
- if HandleEtag(etag, "Get File Infos For Post", w, r) {
- return
- } else {
- w.Header().Set("Cache-Control", "max-age=2592000, public")
- w.Header().Set(model.HEADER_ETAG_SERVER, etag)
- w.Write([]byte(model.FileInfosToJson(infos)))
+ ogJson, err := og.ToJSON()
+ if err != nil {
+ w.Write([]byte(`{"url": ""}`))
}
+ w.Write(ogJson)
}
diff --git a/api/post_test.go b/api/post_test.go
index 509432a25..d382786cc 100644
--- a/api/post_test.go
+++ b/api/post_test.go
@@ -5,6 +5,7 @@ package api
import (
"encoding/json"
+ "fmt"
"net/http"
"net/http/httptest"
"net/url"
@@ -14,6 +15,7 @@ import (
"testing"
"time"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
@@ -47,6 +49,10 @@ func TestCreatePost(t *testing.T) {
t.Fatal("shouldn't have files")
}
+ if rpost1.Data.(*model.Post).EditAt != 0 {
+ t.Fatal("Newly craeted post shouldn't have EditAt set")
+ }
+
post2 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: rpost1.Data.(*model.Post).Id}
rpost2, err := Client.CreatePost(post2)
if err != nil {
@@ -130,7 +136,7 @@ func TestCreatePost(t *testing.T) {
} else if rpost9 := resp.Data.(*model.Post); len(rpost9.FileIds) != 3 {
t.Fatal("post should have 3 files")
} else {
- infos := store.Must(Srv.Store.FileInfo().GetForPost(rpost9.Id)).([]*model.FileInfo)
+ infos := store.Must(app.Srv.Store.FileInfo().GetForPost(rpost9.Id)).([]*model.FileInfo)
if len(infos) != 3 {
t.Fatal("should've attached all 3 files to post")
@@ -310,6 +316,15 @@ func TestUpdatePost(t *testing.T) {
Client := th.BasicClient
channel1 := th.BasicChannel
+ allowEditPost := *utils.Cfg.ServiceSettings.AllowEditPost
+ postEditTimeLimit := *utils.Cfg.ServiceSettings.PostEditTimeLimit
+ defer func() {
+ *utils.Cfg.ServiceSettings.AllowEditPost = allowEditPost
+ *utils.Cfg.ServiceSettings.PostEditTimeLimit = postEditTimeLimit
+ }()
+
+ *utils.Cfg.ServiceSettings.AllowEditPost = model.ALLOW_EDIT_POST_ALWAYS
+
post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
rpost1, err := Client.CreatePost(post1)
if err != nil {
@@ -326,6 +341,10 @@ func TestUpdatePost(t *testing.T) {
t.Fatal(err)
}
+ if rpost2.Data.(*model.Post).EditAt != 0 {
+ t.Fatal("Newly craeted post shouldn't have EditAt set")
+ }
+
msg2 := "a" + model.NewId() + " update post 1"
rpost2.Data.(*model.Post).Message = msg2
if rupost2, err := Client.UpdatePost(rpost2.Data.(*model.Post)); err != nil {
@@ -334,6 +353,9 @@ func TestUpdatePost(t *testing.T) {
if rupost2.Data.(*model.Post).Message != msg2 {
t.Fatal("failed to updates")
}
+ if rupost2.Data.(*model.Post).EditAt == 0 {
+ t.Fatal("EditAt not updated for post")
+ }
}
msg1 := "#hashtag a" + model.NewId() + " update post 2"
@@ -365,6 +387,56 @@ func TestUpdatePost(t *testing.T) {
if _, err := Client.UpdatePost(up3); err == nil {
t.Fatal("shouldn't have been able to update system message")
}
+
+ // Test licensed policy controls for edit post
+ isLicensed := utils.IsLicensed
+ license := utils.License
+ defer func() {
+ utils.IsLicensed = isLicensed
+ utils.License = license
+ }()
+ utils.IsLicensed = true
+ utils.License = &model.License{Features: &model.Features{}}
+ utils.License.Features.SetDefaults()
+
+ *utils.Cfg.ServiceSettings.AllowEditPost = model.ALLOW_EDIT_POST_NEVER
+
+ post4 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: rpost1.Data.(*model.Post).Id}
+ rpost4, err := Client.CreatePost(post4)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ up4 := &model.Post{Id: rpost4.Data.(*model.Post).Id, ChannelId: channel1.Id, Message: "a" + model.NewId() + " update post 4"}
+ if _, err := Client.UpdatePost(up4); err == nil {
+ t.Fatal("shouldn't have been able to update a message when not allowed")
+ }
+
+ *utils.Cfg.ServiceSettings.AllowEditPost = model.ALLOW_EDIT_POST_TIME_LIMIT
+ *utils.Cfg.ServiceSettings.PostEditTimeLimit = 1 //seconds
+
+ post5 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a", RootId: rpost1.Data.(*model.Post).Id}
+ rpost5, err := Client.CreatePost(post5)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ msg5 := "a" + model.NewId() + " update post 5"
+ up5 := &model.Post{Id: rpost5.Data.(*model.Post).Id, ChannelId: channel1.Id, Message: msg5}
+ if rup5, err := Client.UpdatePost(up5); err != nil {
+ t.Fatal(err)
+ } else {
+ if rup5.Data.(*model.Post).Message != up5.Message {
+ t.Fatal("failed to updates")
+ }
+ }
+
+ time.Sleep(1000 * time.Millisecond)
+
+ up6 := &model.Post{Id: rpost5.Data.(*model.Post).Id, ChannelId: channel1.Id, Message: "a" + model.NewId() + " update post 5"}
+ if _, err := Client.UpdatePost(up6); err == nil {
+ t.Fatal("shouldn't have been able to update a message after time limit")
+ }
}
func TestGetPosts(t *testing.T) {
@@ -793,10 +865,18 @@ func TestGetPostsCache(t *testing.T) {
}
func TestDeletePosts(t *testing.T) {
- th := Setup().InitBasic()
+ th := Setup().InitBasic().InitSystemAdmin()
Client := th.BasicClient
channel1 := th.BasicChannel
- UpdateUserToTeamAdmin(th.BasicUser2, th.BasicTeam)
+ team1 := th.BasicTeam
+
+ restrictPostDelete := *utils.Cfg.ServiceSettings.RestrictPostDelete
+ defer func() {
+ *utils.Cfg.ServiceSettings.RestrictPostDelete = restrictPostDelete
+ utils.SetDefaultRolesBasedOnConfig()
+ }()
+ *utils.Cfg.ServiceSettings.RestrictPostDelete = model.PERMISSIONS_DELETE_POST_ALL
+ utils.SetDefaultRolesBasedOnConfig()
time.Sleep(10 * time.Millisecond)
post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
@@ -828,28 +908,138 @@ func TestDeletePosts(t *testing.T) {
r2 := Client.Must(Client.GetPosts(channel1.Id, 0, 10, "")).Data.(*model.PostList)
if len(r2.Posts) != 5 {
- t.Fatal("should have returned 4 items")
+ t.Fatal("should have returned 5 items")
}
time.Sleep(10 * time.Millisecond)
- post4 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
- post4 = Client.Must(Client.CreatePost(post4)).Data.(*model.Post)
+ post4a := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
+ post4a = Client.Must(Client.CreatePost(post4a)).Data.(*model.Post)
+
+ time.Sleep(10 * time.Millisecond)
+ post4b := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
+ post4b = Client.Must(Client.CreatePost(post4b)).Data.(*model.Post)
+
+ SystemAdminClient := th.SystemAdminClient
+ LinkUserToTeam(th.SystemAdminUser, th.BasicTeam)
+ SystemAdminClient.Must(SystemAdminClient.JoinChannel(channel1.Id))
th.LoginBasic2()
Client.Must(Client.JoinChannel(channel1.Id))
- Client.Must(Client.DeletePost(channel1.Id, post4.Id))
+ if _, err := Client.DeletePost(channel1.Id, post4a.Id); err == nil {
+ t.Fatal(err)
+ }
+
+ // Test licensed policy controls for delete post
+ isLicensed := utils.IsLicensed
+ license := utils.License
+ defer func() {
+ utils.IsLicensed = isLicensed
+ utils.License = license
+ }()
+ utils.IsLicensed = true
+ utils.License = &model.License{Features: &model.Features{}}
+ utils.License.Features.SetDefaults()
+
+ UpdateUserToTeamAdmin(th.BasicUser2, th.BasicTeam)
+
+ Client.Logout()
+ th.LoginBasic2()
+ Client.SetTeamId(team1.Id)
+
+ Client.Must(Client.DeletePost(channel1.Id, post4a.Id))
+
+ SystemAdminClient.Must(SystemAdminClient.DeletePost(channel1.Id, post4b.Id))
+
+ *utils.Cfg.ServiceSettings.RestrictPostDelete = model.PERMISSIONS_DELETE_POST_TEAM_ADMIN
+ utils.SetDefaultRolesBasedOnConfig()
+
+ th.LoginBasic()
+
+ time.Sleep(10 * time.Millisecond)
+ post5a := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
+ post5a = Client.Must(Client.CreatePost(post5a)).Data.(*model.Post)
+
+ time.Sleep(10 * time.Millisecond)
+ post5b := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
+ post5b = Client.Must(Client.CreatePost(post5b)).Data.(*model.Post)
+
+ if _, err := Client.DeletePost(channel1.Id, post5a.Id); err == nil {
+ t.Fatal(err)
+ }
+
+ th.LoginBasic2()
+
+ Client.Must(Client.DeletePost(channel1.Id, post5a.Id))
+
+ SystemAdminClient.Must(SystemAdminClient.DeletePost(channel1.Id, post5b.Id))
+
+ *utils.Cfg.ServiceSettings.RestrictPostDelete = model.PERMISSIONS_DELETE_POST_SYSTEM_ADMIN
+ utils.SetDefaultRolesBasedOnConfig()
+
+ th.LoginBasic()
+
+ time.Sleep(10 * time.Millisecond)
+ post6a := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
+ post6a = Client.Must(Client.CreatePost(post6a)).Data.(*model.Post)
+
+ if _, err := Client.DeletePost(channel1.Id, post6a.Id); err == nil {
+ t.Fatal(err)
+ }
+
+ th.LoginBasic2()
+
+ if _, err := Client.DeletePost(channel1.Id, post6a.Id); err == nil {
+ t.Fatal(err)
+ }
+
+ SystemAdminClient.Must(SystemAdminClient.DeletePost(channel1.Id, post6a.Id))
+
}
func TestEmailMention(t *testing.T) {
th := Setup().InitBasic()
Client := th.BasicClient
channel1 := th.BasicChannel
+ Client.Must(Client.AddChannelMember(channel1.Id, th.BasicUser2.Id))
- post1 := &model.Post{ChannelId: channel1.Id, Message: th.BasicUser.Username}
+ th.LoginBasic2()
+ //Set the notification properties
+ data := make(map[string]string)
+ data["user_id"] = th.BasicUser2.Id
+ data["email"] = "true"
+ data["desktop"] = "all"
+ data["desktop_sound"] = "false"
+ data["comments"] = "any"
+ Client.Must(Client.UpdateUserNotify(data))
+
+ store.Must(app.Srv.Store.Preference().Save(&model.Preferences{{
+ UserId: th.BasicUser2.Id,
+ Category: model.PREFERENCE_CATEGORY_NOTIFICATIONS,
+ Name: model.PREFERENCE_NAME_EMAIL_INTERVAL,
+ Value: "0",
+ }}))
+
+ //Delete all the messages before create a mention post
+ utils.DeleteMailBox(th.BasicUser2.Email)
+
+ //Send a mention message from user1 to user2
+ th.LoginBasic()
+ time.Sleep(10 * time.Millisecond)
+ post1 := &model.Post{ChannelId: channel1.Id, Message: "@" + th.BasicUser2.Username + " this is a test"}
post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
- // No easy way to verify the email was sent, but this will at least cause the server to throw errors if the code is broken
+ //Check if the email was send to the rigth email address and the mention
+ if resultsMailbox, err := utils.GetMailBox(th.BasicUser2.Email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], th.BasicUser2.Email) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(th.BasicUser2.Email, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Body.Text, post1.Message) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Received wrong Message")
+ }
+ }
+ }
}
@@ -893,7 +1083,9 @@ func TestMakeDirectChannelVisible(t *testing.T) {
channel := Client.Must(Client.CreateDirectChannel(user2.Id)).Data.(*model.Channel)
- makeDirectChannelVisible(channel.Id)
+ if err := app.MakeDirectChannelVisible(channel.Id); err != nil {
+ t.Fatal(err)
+ }
if result, err := Client.GetPreference(model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, user2.Id); err != nil {
t.Fatal("Errored trying to set direct channel to be visible for user1")
@@ -902,281 +1094,6 @@ func TestMakeDirectChannelVisible(t *testing.T) {
}
}
-func TestGetMentionKeywords(t *testing.T) {
- // user with username or custom mentions enabled
- user1 := &model.User{
- Id: model.NewId(),
- FirstName: "First",
- Username: "User",
- NotifyProps: map[string]string{
- "mention_keys": "User,@User,MENTION",
- },
- }
-
- profiles := map[string]*model.User{user1.Id: user1}
- mentions := getMentionKeywordsInChannel(profiles)
- if len(mentions) != 3 {
- t.Fatal("should've returned three mention keywords")
- } else if ids, ok := mentions["user"]; !ok || ids[0] != user1.Id {
- t.Fatal("should've returned mention key of user")
- } else if ids, ok := mentions["@user"]; !ok || ids[0] != user1.Id {
- t.Fatal("should've returned mention key of @user")
- } else if ids, ok := mentions["mention"]; !ok || ids[0] != user1.Id {
- t.Fatal("should've returned mention key of mention")
- }
-
- // user with first name mention enabled
- user2 := &model.User{
- Id: model.NewId(),
- FirstName: "First",
- Username: "User",
- NotifyProps: map[string]string{
- "first_name": "true",
- },
- }
-
- profiles = map[string]*model.User{user2.Id: user2}
- mentions = getMentionKeywordsInChannel(profiles)
- if len(mentions) != 2 {
- t.Fatal("should've returned two mention keyword")
- } else if ids, ok := mentions["First"]; !ok || ids[0] != user2.Id {
- t.Fatal("should've returned mention key of First")
- }
-
- // user with @channel/@all mentions enabled
- user3 := &model.User{
- Id: model.NewId(),
- FirstName: "First",
- Username: "User",
- NotifyProps: map[string]string{
- "channel": "true",
- },
- }
-
- profiles = map[string]*model.User{user3.Id: user3}
- mentions = getMentionKeywordsInChannel(profiles)
- if len(mentions) != 3 {
- t.Fatal("should've returned three mention keywords")
- } else if ids, ok := mentions["@channel"]; !ok || ids[0] != user3.Id {
- t.Fatal("should've returned mention key of @channel")
- } else if ids, ok := mentions["@all"]; !ok || ids[0] != user3.Id {
- t.Fatal("should've returned mention key of @all")
- }
-
- // user with all types of mentions enabled
- user4 := &model.User{
- Id: model.NewId(),
- FirstName: "First",
- Username: "User",
- NotifyProps: map[string]string{
- "mention_keys": "User,@User,MENTION",
- "first_name": "true",
- "channel": "true",
- },
- }
-
- profiles = map[string]*model.User{user4.Id: user4}
- mentions = getMentionKeywordsInChannel(profiles)
- if len(mentions) != 6 {
- t.Fatal("should've returned six mention keywords")
- } else if ids, ok := mentions["user"]; !ok || ids[0] != user4.Id {
- t.Fatal("should've returned mention key of user")
- } else if ids, ok := mentions["@user"]; !ok || ids[0] != user4.Id {
- t.Fatal("should've returned mention key of @user")
- } else if ids, ok := mentions["mention"]; !ok || ids[0] != user4.Id {
- t.Fatal("should've returned mention key of mention")
- } else if ids, ok := mentions["First"]; !ok || ids[0] != user4.Id {
- t.Fatal("should've returned mention key of First")
- } else if ids, ok := mentions["@channel"]; !ok || ids[0] != user4.Id {
- t.Fatal("should've returned mention key of @channel")
- } else if ids, ok := mentions["@all"]; !ok || ids[0] != user4.Id {
- t.Fatal("should've returned mention key of @all")
- }
-
- dup_count := func(list []string) map[string]int {
-
- duplicate_frequency := make(map[string]int)
-
- for _, item := range list {
- // check if the item/element exist in the duplicate_frequency map
-
- _, exist := duplicate_frequency[item]
-
- if exist {
- duplicate_frequency[item] += 1 // increase counter by 1 if already in the map
- } else {
- duplicate_frequency[item] = 1 // else start counting from 1
- }
- }
- return duplicate_frequency
- }
-
- // multiple users
- profiles = map[string]*model.User{
- user1.Id: user1,
- user2.Id: user2,
- user3.Id: user3,
- user4.Id: user4,
- }
- mentions = getMentionKeywordsInChannel(profiles)
- if len(mentions) != 6 {
- t.Fatal("should've returned six mention keywords")
- } else if ids, ok := mentions["user"]; !ok || len(ids) != 2 || (ids[0] != user1.Id && ids[1] != user1.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) {
- t.Fatal("should've mentioned user1 and user4 with user")
- } else if ids := dup_count(mentions["@user"]); len(ids) != 4 || (ids[user1.Id] != 2) || (ids[user4.Id] != 2) {
- t.Fatal("should've mentioned user1 and user4 with @user")
- } else if ids, ok := mentions["mention"]; !ok || len(ids) != 2 || (ids[0] != user1.Id && ids[1] != user1.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) {
- t.Fatal("should've mentioned user1 and user4 with mention")
- } else if ids, ok := mentions["First"]; !ok || len(ids) != 2 || (ids[0] != user2.Id && ids[1] != user2.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) {
- t.Fatal("should've mentioned user2 and user4 with mention")
- } else if ids, ok := mentions["@channel"]; !ok || len(ids) != 2 || (ids[0] != user3.Id && ids[1] != user3.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) {
- t.Fatal("should've mentioned user3 and user4 with @channel")
- } else if ids, ok := mentions["@all"]; !ok || len(ids) != 2 || (ids[0] != user3.Id && ids[1] != user3.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) {
- t.Fatal("should've mentioned user3 and user4 with @all")
- }
-}
-
-func TestGetExplicitMentionsAtHere(t *testing.T) {
- // test all the boundary cases that we know can break up terms (and those that we know won't)
- cases := map[string]bool{
- "": false,
- "here": false,
- "@here": true,
- " @here ": true,
- "\t@here\t": true,
- "\n@here\n": true,
- // "!@here!": true,
- // "@@here@": true,
- // "#@here#": true,
- // "$@here$": true,
- // "%@here%": true,
- // "^@here^": true,
- // "&@here&": true,
- // "*@here*": true,
- "(@here(": true,
- ")@here)": true,
- // "-@here-": true,
- // "_@here_": true,
- // "=@here=": true,
- "+@here+": true,
- "[@here[": true,
- "{@here{": true,
- "]@here]": true,
- "}@here}": true,
- "\\@here\\": true,
- // "|@here|": true,
- ";@here;": true,
- ":@here:": true,
- // "'@here'": true,
- // "\"@here\"": true,
- ",@here,": true,
- "<@here<": true,
- ".@here.": true,
- ">@here>": true,
- "/@here/": true,
- "?@here?": true,
- // "`@here`": true,
- // "~@here~": true,
- }
-
- for message, shouldMention := range cases {
- if _, _, hereMentioned, _, _ := getExplicitMentions(message, nil); hereMentioned && !shouldMention {
- t.Fatalf("shouldn't have mentioned @here with \"%v\"", message)
- } else if !hereMentioned && shouldMention {
- t.Fatalf("should've have mentioned @here with \"%v\"", message)
- }
- }
-
- // mentioning @here and someone
- id := model.NewId()
- if mentions, potential, hereMentioned, _, _ := getExplicitMentions("@here @user @potential", map[string][]string{"@user": {id}}); !hereMentioned {
- t.Fatal("should've mentioned @here with \"@here @user\"")
- } else if len(mentions) != 1 || !mentions[id] {
- t.Fatal("should've mentioned @user with \"@here @user\"")
- } else if len(potential) > 1 {
- t.Fatal("should've potential mentions for @potential")
- }
-}
-
-func TestGetExplicitMentions(t *testing.T) {
- id1 := model.NewId()
- id2 := model.NewId()
-
- // not mentioning anybody
- message := "this is a message"
- keywords := map[string][]string{}
- if mentions, potential, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 0 || len(potential) != 0 {
- t.Fatal("shouldn't have mentioned anybody or have any potencial mentions")
- }
-
- // mentioning a user that doesn't exist
- message = "this is a message for @user"
- if mentions, _, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 0 {
- t.Fatal("shouldn't have mentioned user that doesn't exist")
- }
-
- // mentioning one person
- keywords = map[string][]string{"@user": {id1}}
- if mentions, _, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] {
- t.Fatal("should've mentioned @user")
- }
-
- // mentioning one person without an @mention
- message = "this is a message for @user"
- keywords = map[string][]string{"this": {id1}}
- if mentions, _, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] {
- t.Fatal("should've mentioned this")
- }
-
- // mentioning multiple people with one word
- message = "this is a message for @user"
- keywords = map[string][]string{"@user": {id1, id2}}
- if mentions, _, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] {
- t.Fatal("should've mentioned two users with @user")
- }
-
- // mentioning only one of multiple people
- keywords = map[string][]string{"@user": {id1}, "@mention": {id2}}
- if mentions, _, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] {
- t.Fatal("should've mentioned @user and not @mention")
- }
-
- // mentioning multiple people with multiple words
- message = "this is an @mention for @user"
- keywords = map[string][]string{"@user": {id1}, "@mention": {id2}}
- if mentions, _, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] {
- t.Fatal("should've mentioned two users with @user and @mention")
- }
-
- // mentioning @channel (not a special case, but it's good to double check)
- message = "this is an message for @channel"
- keywords = map[string][]string{"@channel": {id1, id2}}
- if mentions, _, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] {
- t.Fatal("should've mentioned two users with @channel")
- }
-
- // mentioning @all (not a special case, but it's good to double check)
- message = "this is an message for @all"
- keywords = map[string][]string{"@all": {id1, id2}}
- if mentions, _, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] {
- t.Fatal("should've mentioned two users with @all")
- }
-
- // mentioning user.period without mentioning user (PLT-3222)
- message = "user.period doesn't complicate things at all by including periods in their username"
- keywords = map[string][]string{"user.period": {id1}, "user": {id2}}
- if mentions, _, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] {
- t.Fatal("should've mentioned user.period and not user")
- }
-
- // mentioning a potential out of channel user
- message = "this is an message for @potential and @user"
- keywords = map[string][]string{"@user": {id1}}
- if mentions, potential, _, _, _ := getExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || len(potential) != 1 {
- t.Fatal("should've mentioned user and have a potential not in channel")
- }
-}
-
func TestGetFlaggedPosts(t *testing.T) {
th := Setup().InitBasic()
Client := th.BasicClient
@@ -1215,28 +1132,28 @@ func TestGetFlaggedPosts(t *testing.T) {
func TestGetMessageForNotification(t *testing.T) {
Setup().InitBasic()
- testPng := store.Must(Srv.Store.FileInfo().Save(&model.FileInfo{
+ testPng := store.Must(app.Srv.Store.FileInfo().Save(&model.FileInfo{
CreatorId: model.NewId(),
Path: "test1.png",
Name: "test1.png",
MimeType: "image/png",
})).(*model.FileInfo)
- testJpg1 := store.Must(Srv.Store.FileInfo().Save(&model.FileInfo{
+ testJpg1 := store.Must(app.Srv.Store.FileInfo().Save(&model.FileInfo{
CreatorId: model.NewId(),
Path: "test2.jpg",
Name: "test2.jpg",
MimeType: "image/jpeg",
})).(*model.FileInfo)
- testFile := store.Must(Srv.Store.FileInfo().Save(&model.FileInfo{
+ testFile := store.Must(app.Srv.Store.FileInfo().Save(&model.FileInfo{
CreatorId: model.NewId(),
Path: "test1.go",
Name: "test1.go",
MimeType: "text/plain",
})).(*model.FileInfo)
- testJpg2 := store.Must(Srv.Store.FileInfo().Save(&model.FileInfo{
+ testJpg2 := store.Must(app.Srv.Store.FileInfo().Save(&model.FileInfo{
CreatorId: model.NewId(),
Path: "test3.jpg",
Name: "test3.jpg",
@@ -1250,37 +1167,37 @@ func TestGetMessageForNotification(t *testing.T) {
Message: "test",
}
- if getMessageForNotification(post, translateFunc) != "test" {
+ if app.GetMessageForNotification(post, translateFunc) != "test" {
t.Fatal("should've returned message text")
}
post.FileIds = model.StringArray{testPng.Id}
- store.Must(Srv.Store.FileInfo().AttachToPost(testPng.Id, post.Id))
- if getMessageForNotification(post, translateFunc) != "test" {
+ store.Must(app.Srv.Store.FileInfo().AttachToPost(testPng.Id, post.Id))
+ if app.GetMessageForNotification(post, translateFunc) != "test" {
t.Fatal("should've returned message text, even with attachments")
}
post.Message = ""
- if message := getMessageForNotification(post, translateFunc); message != "1 image sent: test1.png" {
+ if message := app.GetMessageForNotification(post, translateFunc); message != "1 image sent: test1.png" {
t.Fatal("should've returned number of images:", message)
}
post.FileIds = model.StringArray{testPng.Id, testJpg1.Id}
- store.Must(Srv.Store.FileInfo().AttachToPost(testJpg1.Id, post.Id))
- if message := getMessageForNotification(post, translateFunc); message != "2 images sent: test1.png, test2.jpg" && message != "2 images sent: test2.jpg, test1.png" {
+ store.Must(app.Srv.Store.FileInfo().AttachToPost(testJpg1.Id, post.Id))
+ if message := app.GetMessageForNotification(post, translateFunc); message != "2 images sent: test1.png, test2.jpg" && message != "2 images sent: test2.jpg, test1.png" {
t.Fatal("should've returned number of images:", message)
}
post.Id = model.NewId()
post.FileIds = model.StringArray{testFile.Id}
- store.Must(Srv.Store.FileInfo().AttachToPost(testFile.Id, post.Id))
- if message := getMessageForNotification(post, translateFunc); message != "1 file sent: test1.go" {
+ store.Must(app.Srv.Store.FileInfo().AttachToPost(testFile.Id, post.Id))
+ if message := app.GetMessageForNotification(post, translateFunc); message != "1 file sent: test1.go" {
t.Fatal("should've returned number of files:", message)
}
- store.Must(Srv.Store.FileInfo().AttachToPost(testJpg2.Id, post.Id))
+ store.Must(app.Srv.Store.FileInfo().AttachToPost(testJpg2.Id, post.Id))
post.FileIds = model.StringArray{testFile.Id, testJpg2.Id}
- if message := getMessageForNotification(post, translateFunc); message != "2 files sent: test1.go, test3.jpg" && message != "2 files sent: test3.jpg, test1.go" {
+ if message := app.GetMessageForNotification(post, translateFunc); message != "2 files sent: test1.go, test3.jpg" && message != "2 files sent: test3.jpg, test1.go" {
t.Fatal("should've returned number of mixed files:", message)
}
}
@@ -1323,43 +1240,6 @@ func TestGetFileInfosForPost(t *testing.T) {
}
}
-func TestSendNotifications(t *testing.T) {
- th := Setup().InitBasic()
- Client := th.BasicClient
-
- AddUserToChannel(th.BasicUser2, th.BasicChannel)
-
- mockSession := model.Session{
- UserId: th.BasicUser.Id,
- TeamMembers: []*model.TeamMember{{TeamId: th.BasicTeam.Id, UserId: th.BasicUser.Id}},
- IsOAuth: false,
- }
-
- newContext := &Context{
- Session: mockSession,
- RequestId: model.NewId(),
- IpAddress: "",
- Path: "fake",
- Err: nil,
- siteURL: *utils.Cfg.ServiceSettings.SiteURL,
- TeamId: th.BasicTeam.Id,
- }
-
- post1 := Client.Must(Client.CreatePost(&model.Post{
- ChannelId: th.BasicChannel.Id,
- Message: "@" + th.BasicUser2.Username,
- })).Data.(*model.Post)
-
- mentions := sendNotifications(newContext, post1, th.BasicTeam, th.BasicChannel)
- if mentions == nil {
- t.Log(mentions)
- t.Fatal("user should have been mentioned")
- } else if mentions[0] != th.BasicUser2.Id {
- t.Log(mentions)
- t.Fatal("user should have been mentioned")
- }
-}
-
func TestGetPostById(t *testing.T) {
th := Setup().InitBasic()
Client := th.BasicClient
@@ -1389,3 +1269,63 @@ func TestGetPostById(t *testing.T) {
t.Fatal(respMetadata.Error)
}
}
+
+func TestGetPermalinkTmp(t *testing.T) {
+ th := Setup().InitBasic()
+ Client := th.BasicClient
+ channel1 := th.BasicChannel
+
+ time.Sleep(10 * time.Millisecond)
+ post1 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
+ post1 = Client.Must(Client.CreatePost(post1)).Data.(*model.Post)
+
+ time.Sleep(10 * time.Millisecond)
+ post2 := &model.Post{ChannelId: channel1.Id, Message: "a" + model.NewId() + "a"}
+ post2 = Client.Must(Client.CreatePost(post2)).Data.(*model.Post)
+
+ etag := Client.Must(Client.GetPost(channel1.Id, post1.Id, "")).Etag
+
+ // test etag caching
+ if cache_result, respMetadata := Client.GetPermalink(channel1.Id, post1.Id, etag); respMetadata.Error != nil {
+ t.Fatal(respMetadata.Error)
+ } else if cache_result != nil {
+ t.Log(cache_result)
+ t.Fatal("cache should be empty")
+ }
+
+ if results, respMetadata := Client.GetPermalink(channel1.Id, post1.Id, ""); respMetadata.Error != nil {
+ t.Fatal(respMetadata.Error)
+ } else if results == nil {
+ t.Fatal("should not be empty")
+ }
+}
+
+func TestGetOpenGraphMetadata(t *testing.T) {
+ th := Setup().InitBasic()
+ Client := th.BasicClient
+
+ ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path == "/og-data/" {
+ fmt.Fprintln(w, `
+ <html><head><meta property="og:type" content="article" />
+ <meta property="og:title" content="Test Title" />
+ <meta property="og:url" content="http://example.com/" />
+ </head><body></body></html>
+ `)
+ } else if r.URL.Path == "/no-og-data/" {
+ fmt.Fprintln(w, `<html><head></head><body></body></html>`)
+ }
+ }))
+
+ for _, data := range [](map[string]string){{"path": "/og-data/", "title": "Test Title"}, {"path": "/no-og-data/", "title": ""}} {
+ res, err := Client.DoApiPost("/get_opengraph_metadata", fmt.Sprintf("{\"url\":\"%s\"}", ts.URL+data["path"]))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ ogData := model.StringInterfaceFromJson(res.Body)
+ if strings.Compare(ogData["title"].(string), data["title"]) != 0 {
+ t.Fatal(fmt.Sprintf("OG data title mismatch for path \"%s\". Expected title: \"%s\". Actual title: \"%s\"", data["path"], data["title"], ogData["title"]))
+ }
+ }
+}
diff --git a/api/preference.go b/api/preference.go
index 240ead571..5384afbb5 100644
--- a/api/preference.go
+++ b/api/preference.go
@@ -6,6 +6,7 @@ package api
import (
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
"net/http"
@@ -22,7 +23,7 @@ func InitPreference() {
}
func getAllPreferences(c *Context, w http.ResponseWriter, r *http.Request) {
- if result := <-Srv.Store.Preference().GetAll(c.Session.UserId); result.Err != nil {
+ if result := <-app.Srv.Store.Preference().GetAll(c.Session.UserId); result.Err != nil {
c.Err = result.Err
} else {
data := result.Data.(model.Preferences)
@@ -49,7 +50,7 @@ func savePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
- if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil {
+ if result := <-app.Srv.Store.Preference().Save(&preferences); result.Err != nil {
c.Err = result.Err
return
}
@@ -61,7 +62,7 @@ func getPreferenceCategory(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
category := params["category"]
- if result := <-Srv.Store.Preference().GetCategory(c.Session.UserId, category); result.Err != nil {
+ if result := <-app.Srv.Store.Preference().GetCategory(c.Session.UserId, category); result.Err != nil {
c.Err = result.Err
} else {
data := result.Data.(model.Preferences)
@@ -75,7 +76,7 @@ func getPreference(c *Context, w http.ResponseWriter, r *http.Request) {
category := params["category"]
name := params["name"]
- if result := <-Srv.Store.Preference().Get(c.Session.UserId, category, name); result.Err != nil {
+ if result := <-app.Srv.Store.Preference().Get(c.Session.UserId, category, name); result.Err != nil {
c.Err = result.Err
} else {
data := result.Data.(model.Preference)
@@ -101,7 +102,7 @@ func deletePreferences(c *Context, w http.ResponseWriter, r *http.Request) {
}
for _, preference := range preferences {
- if result := <-Srv.Store.Preference().Delete(c.Session.UserId, preference.Category, preference.Name); result.Err != nil {
+ if result := <-app.Srv.Store.Preference().Delete(c.Session.UserId, preference.Category, preference.Name); result.Err != nil {
c.Err = result.Err
return
}
diff --git a/api/reaction.go b/api/reaction.go
index 5acf09f9e..fd9a05779 100644
--- a/api/reaction.go
+++ b/api/reaction.go
@@ -6,6 +6,7 @@ package api
import (
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
"net/http"
@@ -40,7 +41,8 @@ func saveReaction(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
+ if !app.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_READ_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
@@ -50,7 +52,7 @@ func saveReaction(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- pchan := Srv.Store.Post().Get(reaction.PostId)
+ pchan := app.Srv.Store.Post().Get(reaction.PostId)
var postHadReactions bool
if result := <-pchan; result.Err != nil {
@@ -65,7 +67,7 @@ func saveReaction(c *Context, w http.ResponseWriter, r *http.Request) {
postHadReactions = post.HasReactions
}
- if result := <-Srv.Store.Reaction().Save(reaction); result.Err != nil {
+ if result := <-app.Srv.Store.Reaction().Save(reaction); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -98,7 +100,8 @@ func deleteReaction(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
+ if !app.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_READ_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
@@ -108,7 +111,7 @@ func deleteReaction(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- pchan := Srv.Store.Post().Get(reaction.PostId)
+ pchan := app.Srv.Store.Post().Get(reaction.PostId)
var postHadReactions bool
if result := <-pchan; result.Err != nil {
@@ -123,7 +126,7 @@ func deleteReaction(c *Context, w http.ResponseWriter, r *http.Request) {
postHadReactions = post.HasReactions
}
- if result := <-Srv.Store.Reaction().Delete(reaction); result.Err != nil {
+ if result := <-app.Srv.Store.Reaction().Delete(reaction); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -139,13 +142,13 @@ func sendReactionEvent(event string, channelId string, reaction *model.Reaction,
message := model.NewWebSocketEvent(event, "", channelId, "", nil)
message.Add("reaction", reaction.ToJson())
- Publish(message)
+ app.Publish(message)
}()
// send out that a post was updated if post.HasReactions has changed
go func() {
var post *model.Post
- if result := <-Srv.Store.Post().Get(reaction.PostId); result.Err != nil {
+ if result := <-app.Srv.Store.Post().Get(reaction.PostId); result.Err != nil {
l4g.Warn(utils.T("api.reaction.send_reaction_event.post.app_error"))
return
} else {
@@ -156,7 +159,7 @@ func sendReactionEvent(event string, channelId string, reaction *model.Reaction,
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", channelId, "", nil)
message.Add("post", post.ToJson())
- Publish(message)
+ app.Publish(message)
}
}()
}
@@ -176,9 +179,10 @@ func listReactions(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- pchan := Srv.Store.Post().Get(postId)
+ pchan := app.Srv.Store.Post().Get(postId)
- if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
+ if !app.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_READ_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
@@ -192,7 +196,7 @@ func listReactions(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.Reaction().GetForPost(postId); result.Err != nil {
+ if result := <-app.Srv.Store.Reaction().GetForPost(postId); result.Err != nil {
c.Err = result.Err
return
} else {
diff --git a/api/status.go b/api/status.go
index 909ab50ec..69f391f47 100644
--- a/api/status.go
+++ b/api/status.go
@@ -8,67 +8,30 @@ import (
l4g "github.com/alecthomas/log4go"
- "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
- "github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
)
-var statusCache *utils.Cache = utils.NewLru(model.STATUS_CACHE_SIZE)
-
-func ClearStatusCache() {
- statusCache.Purge()
-}
-
-func AddStatusCacheSkipClusterSend(status *model.Status) {
- statusCache.Add(status.UserId, status)
-}
-
-func AddStatusCache(status *model.Status) {
- AddStatusCacheSkipClusterSend(status)
-
- if einterfaces.GetClusterInterface() != nil {
- einterfaces.GetClusterInterface().UpdateStatus(status)
- }
-}
-
func InitStatus() {
l4g.Debug(utils.T("api.status.init.debug"))
BaseRoutes.Users.Handle("/status", ApiUserRequired(getStatusesHttp)).Methods("GET")
BaseRoutes.Users.Handle("/status/ids", ApiUserRequired(getStatusesByIdsHttp)).Methods("POST")
- BaseRoutes.WebSocket.Handle("get_statuses", ApiWebSocketHandler(getStatusesWebSocket))
- BaseRoutes.WebSocket.Handle("get_statuses_by_ids", ApiWebSocketHandler(getStatusesByIdsWebSocket))
+ app.Srv.WebSocketRouter.Handle("get_statuses", ApiWebSocketHandler(getStatusesWebSocket))
+ app.Srv.WebSocketRouter.Handle("get_statuses_by_ids", ApiWebSocketHandler(getStatusesByIdsWebSocket))
}
func getStatusesHttp(c *Context, w http.ResponseWriter, r *http.Request) {
- statusMap := model.StatusMapToInterfaceMap(GetAllStatuses())
+ statusMap := model.StatusMapToInterfaceMap(app.GetAllStatuses())
w.Write([]byte(model.StringInterfaceToJson(statusMap)))
}
func getStatusesWebSocket(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) {
- statusMap := GetAllStatuses()
+ statusMap := app.GetAllStatuses()
return model.StatusMapToInterfaceMap(statusMap), nil
}
-func GetAllStatuses() map[string]*model.Status {
- userIds := statusCache.Keys()
- statusMap := map[string]*model.Status{}
-
- for _, userId := range userIds {
- if id, ok := userId.(string); !ok {
- continue
- } else {
- status := GetStatusFromCache(id)
- if status != nil {
- statusMap[id] = status
- }
- }
- }
-
- return statusMap
-}
-
func getStatusesByIdsHttp(c *Context, w http.ResponseWriter, r *http.Request) {
userIds := model.ArrayFromJson(r.Body)
@@ -77,7 +40,7 @@ func getStatusesByIdsHttp(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- statusMap, err := GetStatusesByIds(userIds)
+ statusMap, err := app.GetStatusesByIds(userIds)
if err != nil {
c.Err = err
return
@@ -93,216 +56,10 @@ func getStatusesByIdsWebSocket(req *model.WebSocketRequest) (map[string]interfac
return nil, NewInvalidWebSocketParamError(req.Action, "user_ids")
}
- statusMap, err := GetStatusesByIds(userIds)
+ statusMap, err := app.GetStatusesByIds(userIds)
if err != nil {
return nil, err
}
return statusMap, nil
}
-
-func GetStatusesByIds(userIds []string) (map[string]interface{}, *model.AppError) {
- statusMap := map[string]interface{}{}
- metrics := einterfaces.GetMetricsInterface()
-
- missingUserIds := []string{}
- for _, userId := range userIds {
- if result, ok := statusCache.Get(userId); ok {
- statusMap[userId] = result.(*model.Status).Status
- if metrics != nil {
- metrics.IncrementMemCacheHitCounter("Status")
- }
- } else {
- missingUserIds = append(missingUserIds, userId)
- if metrics != nil {
- metrics.IncrementMemCacheMissCounter("Status")
- }
- }
- }
-
- if len(missingUserIds) > 0 {
- if result := <-Srv.Store.Status().GetByIds(missingUserIds); result.Err != nil {
- return nil, result.Err
- } else {
- statuses := result.Data.([]*model.Status)
-
- for _, s := range statuses {
- AddStatusCache(s)
- statusMap[s.UserId] = s.Status
- }
- }
- }
-
- // For the case where the user does not have a row in the Status table and cache
- for _, userId := range missingUserIds {
- if _, ok := statusMap[userId]; !ok {
- statusMap[userId] = model.STATUS_OFFLINE
- }
- }
-
- return statusMap, nil
-}
-
-func SetStatusOnline(userId string, sessionId string, manual bool) {
- broadcast := false
-
- var oldStatus string = model.STATUS_OFFLINE
- var oldTime int64 = 0
- var oldManual bool = false
- var status *model.Status
- var err *model.AppError
-
- if status, err = GetStatus(userId); err != nil {
- status = &model.Status{userId, model.STATUS_ONLINE, false, model.GetMillis(), ""}
- broadcast = true
- } else {
- if status.Manual && !manual {
- return // manually set status always overrides non-manual one
- }
-
- if status.Status != model.STATUS_ONLINE {
- broadcast = true
- }
-
- oldStatus = status.Status
- oldTime = status.LastActivityAt
- oldManual = status.Manual
-
- status.Status = model.STATUS_ONLINE
- status.Manual = false // for "online" there's no manual setting
- status.LastActivityAt = model.GetMillis()
- }
-
- AddStatusCache(status)
-
- // Only update the database if the status has changed, the status has been manually set,
- // or enough time has passed since the previous action
- if status.Status != oldStatus || status.Manual != oldManual || status.LastActivityAt-oldTime > model.STATUS_MIN_UPDATE_TIME {
- achan := Srv.Store.Session().UpdateLastActivityAt(sessionId, status.LastActivityAt)
-
- var schan store.StoreChannel
- if broadcast {
- schan = Srv.Store.Status().SaveOrUpdate(status)
- } else {
- schan = Srv.Store.Status().UpdateLastActivityAt(status.UserId, status.LastActivityAt)
- }
-
- if result := <-achan; result.Err != nil {
- l4g.Error(utils.T("api.status.last_activity.error"), userId, sessionId, result.Err)
- }
-
- if result := <-schan; result.Err != nil {
- l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
- }
- }
-
- if broadcast {
- event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil)
- event.Add("status", model.STATUS_ONLINE)
- event.Add("user_id", status.UserId)
- go Publish(event)
- }
-}
-
-func SetStatusOffline(userId string, manual bool) {
- status, err := GetStatus(userId)
- if err == nil && status.Manual && !manual {
- return // manually set status always overrides non-manual one
- }
-
- status = &model.Status{userId, model.STATUS_OFFLINE, manual, model.GetMillis(), ""}
-
- AddStatusCache(status)
-
- if result := <-Srv.Store.Status().SaveOrUpdate(status); result.Err != nil {
- l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
- }
-
- event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil)
- event.Add("status", model.STATUS_OFFLINE)
- event.Add("user_id", status.UserId)
- go Publish(event)
-}
-
-func SetStatusAwayIfNeeded(userId string, manual bool) {
- status, err := GetStatus(userId)
-
- if err != nil {
- status = &model.Status{userId, model.STATUS_OFFLINE, manual, 0, ""}
- }
-
- if !manual && status.Manual {
- return // manually set status always overrides non-manual one
- }
-
- if !manual {
- if status.Status == model.STATUS_AWAY {
- return
- }
-
- if !IsUserAway(status.LastActivityAt) {
- return
- }
- }
-
- status.Status = model.STATUS_AWAY
- status.Manual = manual
- status.ActiveChannel = ""
-
- AddStatusCache(status)
-
- if result := <-Srv.Store.Status().SaveOrUpdate(status); result.Err != nil {
- l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
- }
-
- event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil)
- event.Add("status", model.STATUS_AWAY)
- event.Add("user_id", status.UserId)
- go Publish(event)
-}
-
-func GetStatusFromCache(userId string) *model.Status {
- if result, ok := statusCache.Get(userId); ok {
- status := result.(*model.Status)
- statusCopy := &model.Status{}
- *statusCopy = *status
- return statusCopy
- }
-
- return nil
-}
-
-func GetStatus(userId string) (*model.Status, *model.AppError) {
- status := GetStatusFromCache(userId)
- if status != nil {
- return status, nil
- }
-
- if result := <-Srv.Store.Status().Get(userId); result.Err != nil {
- return nil, result.Err
- } else {
- return result.Data.(*model.Status), nil
- }
-}
-
-func IsUserAway(lastActivityAt int64) bool {
- return model.GetMillis()-lastActivityAt >= *utils.Cfg.TeamSettings.UserStatusAwayTimeout*1000
-}
-
-func DoesStatusAllowPushNotification(user *model.User, status *model.Status, channelId string) bool {
- props := user.NotifyProps
-
- if props["push"] == "none" {
- return false
- }
-
- if pushStatus, ok := props["push_status"]; (pushStatus == model.STATUS_ONLINE || !ok) && (status.ActiveChannel != channelId || model.GetMillis()-status.LastActivityAt > model.STATUS_CHANNEL_TIMEOUT) {
- return true
- } else if pushStatus == model.STATUS_AWAY && (status.Status == model.STATUS_AWAY || status.Status == model.STATUS_OFFLINE) {
- return true
- } else if pushStatus == model.STATUS_OFFLINE && status.Status == model.STATUS_OFFLINE {
- return true
- }
-
- return false
-}
diff --git a/api/status_test.go b/api/status_test.go
index 5db5a8d7c..34c3320bd 100644
--- a/api/status_test.go
+++ b/api/status_test.go
@@ -8,6 +8,7 @@ import (
"testing"
"time"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
@@ -34,12 +35,12 @@ func TestStatuses(t *testing.T) {
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User)
LinkUserToTeam(ruser, rteam.Data.(*model.Team))
- store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser.Id))
user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser2 := Client.Must(Client.CreateUser(&user2, "")).Data.(*model.User)
LinkUserToTeam(ruser2, rteam.Data.(*model.Team))
- store.Must(Srv.Store.User().VerifyEmail(ruser2.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser2.Id))
Client.Login(user.Email, user.Password)
Client.SetTeamId(team.Id)
@@ -137,7 +138,7 @@ func TestStatuses(t *testing.T) {
WebSocketClient2.Close()
- SetStatusAwayIfNeeded(th.BasicUser.Id, false)
+ app.SetStatusAwayIfNeeded(th.BasicUser.Id, false)
awayTimeout := *utils.Cfg.TeamSettings.UserStatusAwayTimeout
defer func() {
@@ -147,8 +148,8 @@ func TestStatuses(t *testing.T) {
time.Sleep(1500 * time.Millisecond)
- SetStatusAwayIfNeeded(th.BasicUser.Id, false)
- SetStatusOnline(th.BasicUser.Id, "junk", false)
+ app.SetStatusAwayIfNeeded(th.BasicUser.Id, false)
+ app.SetStatusOnline(th.BasicUser.Id, "junk", false)
time.Sleep(1500 * time.Millisecond)
diff --git a/api/team.go b/api/team.go
index c855a515d..92f9ebaa4 100644
--- a/api/team.go
+++ b/api/team.go
@@ -5,26 +5,23 @@ package api
import (
"bytes"
- "fmt"
- "html/template"
"io"
"net/http"
- "net/url"
"strconv"
"strings"
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
- "github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
)
func InitTeam() {
l4g.Debug(utils.T("api.team.init.debug"))
- BaseRoutes.Teams.Handle("/create", ApiAppHandler(createTeam)).Methods("POST")
+ BaseRoutes.Teams.Handle("/create", ApiUserRequired(createTeam)).Methods("POST")
BaseRoutes.Teams.Handle("/all", ApiAppHandler(getAll)).Methods("GET")
BaseRoutes.Teams.Handle("/all_team_listings", ApiUserRequired(GetAllTeamListings)).Methods("GET")
BaseRoutes.Teams.Handle("/get_invite_info", ApiAppHandler(getInviteInfo)).Methods("POST")
@@ -59,327 +56,86 @@ func createTeam(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- var user *model.User
- if len(c.Session.UserId) > 0 {
- uchan := Srv.Store.User().Get(c.Session.UserId)
-
- if result := <-uchan; result.Err != nil {
- c.Err = result.Err
- return
- } else {
- user = result.Data.(*model.User)
- team.Email = user.Email
- }
- }
-
- if !isTeamCreationAllowed(c, team.Email) {
+ if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_CREATE_TEAM) {
+ c.Err = model.NewLocAppError("createTeam", "api.team.is_team_creation_allowed.disabled.app_error", nil, "")
return
}
- rteam := CreateTeam(c, team)
- if c.Err != nil {
+ rteam, err := app.CreateTeamWithUser(team, c.Session.UserId)
+ if err != nil {
+ c.Err = err
return
}
- if user != nil {
- err := JoinUserToTeam(team, user)
- if err != nil {
- c.Err = err
- return
- }
- }
-
w.Write([]byte(rteam.ToJson()))
}
-func CreateTeam(c *Context, team *model.Team) *model.Team {
- if result := <-Srv.Store.Team().Save(team); result.Err != nil {
- c.Err = result.Err
- return nil
- } else {
- rteam := result.Data.(*model.Team)
-
- if _, err := CreateDefaultChannels(c, rteam.Id); err != nil {
- c.Err = err
- return nil
- }
-
- return rteam
- }
-}
-
-func JoinUserToTeamById(teamId string, user *model.User) *model.AppError {
- if result := <-Srv.Store.Team().Get(teamId); result.Err != nil {
- return result.Err
- } else {
- return JoinUserToTeam(result.Data.(*model.Team), user)
- }
-}
-
-func JoinUserToTeam(team *model.Team, user *model.User) *model.AppError {
-
- tm := &model.TeamMember{
- TeamId: team.Id,
- UserId: user.Id,
- Roles: model.ROLE_TEAM_USER.Id,
- }
-
- channelRole := model.ROLE_CHANNEL_USER.Id
-
- if team.Email == user.Email {
- tm.Roles = model.ROLE_TEAM_USER.Id + " " + model.ROLE_TEAM_ADMIN.Id
- channelRole = model.ROLE_CHANNEL_USER.Id + " " + model.ROLE_CHANNEL_ADMIN.Id
- }
-
- if etmr := <-Srv.Store.Team().GetMember(team.Id, user.Id); etmr.Err == nil {
- // Membership alredy exists. Check if deleted and and update, otherwise do nothing
- rtm := etmr.Data.(model.TeamMember)
-
- // Do nothing if already added
- if rtm.DeleteAt == 0 {
- return nil
- }
-
- if tmr := <-Srv.Store.Team().UpdateMember(tm); tmr.Err != nil {
- return tmr.Err
- }
- } else {
- // Membership appears to be missing. Lets try to add.
- if tmr := <-Srv.Store.Team().SaveMember(tm); tmr.Err != nil {
- return tmr.Err
- }
- }
-
- if uua := <-Srv.Store.User().UpdateUpdateAt(user.Id); uua.Err != nil {
- return uua.Err
- }
-
- // Soft error if there is an issue joining the default channels
- if err := JoinDefaultChannels(team.Id, user, channelRole); err != nil {
- l4g.Error(utils.T("api.user.create_user.joining.error"), user.Id, team.Id, err)
- }
-
- RemoveAllSessionsForUserId(user.Id)
- InvalidateCacheForUser(user.Id)
-
- return nil
-}
-
-func LeaveTeam(team *model.Team, user *model.User) *model.AppError {
-
- var teamMember model.TeamMember
-
- if result := <-Srv.Store.Team().GetMember(team.Id, user.Id); result.Err != nil {
- return model.NewLocAppError("RemoveUserFromTeam", "api.team.remove_user_from_team.missing.app_error", nil, result.Err.Error())
- } else {
- teamMember = result.Data.(model.TeamMember)
- }
-
- var channelList *model.ChannelList
-
- if result := <-Srv.Store.Channel().GetChannels(team.Id, user.Id); result.Err != nil {
- if result.Err.Id == "store.sql_channel.get_channels.not_found.app_error" {
- channelList = &model.ChannelList{}
- } else {
- return result.Err
- }
-
- } else {
- channelList = result.Data.(*model.ChannelList)
- }
-
- for _, channel := range *channelList {
- if channel.Type != model.CHANNEL_DIRECT {
- InvalidateCacheForChannel(channel.Id)
- if result := <-Srv.Store.Channel().RemoveMember(channel.Id, user.Id); result.Err != nil {
- return result.Err
- }
- }
- }
-
- // Send the websocket message before we actually do the remove so the user being removed gets it.
- message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_LEAVE_TEAM, team.Id, "", "", nil)
- message.Add("user_id", user.Id)
- message.Add("team_id", team.Id)
- Publish(message)
-
- teamMember.Roles = ""
- teamMember.DeleteAt = model.GetMillis()
-
- if result := <-Srv.Store.Team().UpdateMember(&teamMember); result.Err != nil {
- return result.Err
- }
-
- if uua := <-Srv.Store.User().UpdateUpdateAt(user.Id); uua.Err != nil {
- return uua.Err
- }
-
- // delete the preferences that set the last channel used in the team and other team specific preferences
- if result := <-Srv.Store.Preference().DeleteCategory(user.Id, team.Id); result.Err != nil {
- return result.Err
- }
-
- RemoveAllSessionsForUserId(user.Id)
- InvalidateCacheForUser(user.Id)
-
- return nil
-}
-
-func isTeamCreationAllowed(c *Context, email string) bool {
-
- email = strings.ToLower(email)
-
- if !utils.Cfg.TeamSettings.EnableTeamCreation && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
- c.Err = model.NewLocAppError("isTeamCreationAllowed", "api.team.is_team_creation_allowed.disabled.app_error", nil, "")
- return false
- }
- c.Err = nil
+func GetAllTeamListings(c *Context, w http.ResponseWriter, r *http.Request) {
+ var teams []*model.Team
+ var err *model.AppError
- if result := <-Srv.Store.User().GetByEmail(email); result.Err == nil {
- user := result.Data.(*model.User)
- if len(user.AuthService) > 0 && len(*user.AuthData) > 0 {
- return true
- }
+ if teams, err = app.GetAllOpenTeams(); err != nil {
+ c.Err = err
+ return
}
- // commas and @ signs are optional
- // can be in the form of "@corp.mattermost.com, mattermost.com mattermost.org" -> corp.mattermost.com mattermost.com mattermost.org
- domains := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(utils.Cfg.TeamSettings.RestrictCreationToDomains, "@", " ", -1), ",", " ", -1))))
-
- matched := false
- for _, d := range domains {
- if strings.HasSuffix(email, "@"+d) {
- matched = true
- break
+ m := make(map[string]*model.Team)
+ for _, v := range teams {
+ m[v.Id] = v
+ if !app.HasPermissionTo(c.Session.UserId, model.PERMISSION_MANAGE_SYSTEM) {
+ m[v.Id].Sanitize()
}
}
- if len(utils.Cfg.TeamSettings.RestrictCreationToDomains) > 0 && !matched {
- c.Err = model.NewLocAppError("isTeamCreationAllowed", "api.team.is_team_creation_allowed.domain.app_error", nil, "")
- return false
- }
-
- return true
-}
-
-func GetAllTeamListings(c *Context, w http.ResponseWriter, r *http.Request) {
- if result := <-Srv.Store.Team().GetAllTeamListing(); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- teams := result.Data.([]*model.Team)
- m := make(map[string]*model.Team)
- for _, v := range teams {
- m[v.Id] = v
- if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
- m[v.Id].Sanitize()
- }
- c.Err = nil
- }
-
- w.Write([]byte(model.TeamMapToJson(m)))
- }
+ w.Write([]byte(model.TeamMapToJson(m)))
}
// Gets all teams which the current user can has access to. If the user is a System Admin, this will be all teams
// on the server. Otherwise, it will only be the teams of which the user is a member.
func getAll(c *Context, w http.ResponseWriter, r *http.Request) {
- var tchan store.StoreChannel
- if HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
- tchan = Srv.Store.Team().GetAll()
- } else {
- c.Err = nil
- tchan = Srv.Store.Team().GetTeamsByUserId(c.Session.UserId)
- }
+ var teams []*model.Team
+ var err *model.AppError
- if result := <-tchan; result.Err != nil {
- c.Err = result.Err
- return
+ if app.HasPermissionTo(c.Session.UserId, model.PERMISSION_MANAGE_SYSTEM) {
+ teams, err = app.GetAllTeams()
} else {
- teams := result.Data.([]*model.Team)
- m := make(map[string]*model.Team)
- for _, v := range teams {
- m[v.Id] = v
- }
-
- w.Write([]byte(model.TeamMapToJson(m)))
+ teams, err = app.GetTeamsForUser(c.Session.UserId)
}
-}
-
-func revokeAllSessions(c *Context, w http.ResponseWriter, r *http.Request) {
- props := model.MapFromJson(r.Body)
- id := props["id"]
- if result := <-Srv.Store.Session().Get(id); result.Err != nil {
- c.Err = result.Err
+ if err != nil {
+ c.Err = err
return
- } else {
- session := result.Data.(*model.Session)
-
- c.LogAudit("revoked_all=" + id)
+ }
- if session.IsOAuth {
- RevokeAccessToken(session.Token)
- } else {
- if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- RemoveAllSessionsForUserId(session.UserId)
- w.Write([]byte(model.MapToJson(props)))
- return
- }
- }
+ m := make(map[string]*model.Team)
+ for _, v := range teams {
+ m[v.Id] = v
}
+
+ w.Write([]byte(model.TeamMapToJson(m)))
}
func inviteMembers(c *Context, w http.ResponseWriter, r *http.Request) {
invites := model.InvitesFromJson(r.Body)
- if len(invites.Invites) == 0 {
- c.Err = model.NewLocAppError("inviteMembers", "api.team.invite_members.no_one.app_error", nil, "")
- c.Err.StatusCode = http.StatusBadRequest
- return
- }
- if utils.IsLicensed {
- if !HasPermissionToCurrentTeamContext(c, model.PERMISSION_INVITE_USER) {
- if *utils.Cfg.TeamSettings.RestrictTeamInvite == model.PERMISSIONS_SYSTEM_ADMIN {
- c.Err = model.NewLocAppError("inviteMembers", "api.team.invite_members.restricted_system_admin.app_error", nil, "")
- }
- if *utils.Cfg.TeamSettings.RestrictTeamInvite == model.PERMISSIONS_TEAM_ADMIN {
- c.Err = model.NewLocAppError("inviteMembers", "api.team.invite_members.restricted_team_admin.app_error", nil, "")
- }
- c.Err.StatusCode = http.StatusForbidden
- return
+ if utils.IsLicensed && !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_INVITE_USER) {
+ errorId := ""
+ if *utils.Cfg.TeamSettings.RestrictTeamInvite == model.PERMISSIONS_SYSTEM_ADMIN {
+ errorId = "api.team.invite_members.restricted_system_admin.app_error"
+ } else if *utils.Cfg.TeamSettings.RestrictTeamInvite == model.PERMISSIONS_TEAM_ADMIN {
+ errorId = "api.team.invite_members.restricted_team_admin.app_error"
}
- }
-
- tchan := Srv.Store.Team().Get(c.TeamId)
- uchan := Srv.Store.User().Get(c.Session.UserId)
- var team *model.Team
- if result := <-tchan; result.Err != nil {
- c.Err = result.Err
+ c.Err = model.NewLocAppError("inviteMembers", errorId, nil, "")
+ c.Err.StatusCode = http.StatusForbidden
return
- } else {
- team = result.Data.(*model.Team)
}
- var user *model.User
- if result := <-uchan; result.Err != nil {
- c.Err = result.Err
+ if err := app.InviteNewUsersToTeam(invites.ToEmailList(), c.TeamId, c.Session.UserId, c.GetSiteURL()); err != nil {
+ c.Err = err
return
- } else {
- user = result.Data.(*model.User)
- }
-
- emailList := make([]string, len(invites.Invites))
- for _, invite := range invites.Invites {
- emailList = append(emailList, invite["email"])
}
- InviteMembers(team, user.GetDisplayName(), emailList, c.GetSiteURL())
-
w.Write([]byte(invites.ToJson()))
}
@@ -392,31 +148,12 @@ func addUserToTeam(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- tchan := Srv.Store.Team().Get(c.TeamId)
- uchan := Srv.Store.User().Get(userId)
-
- var team *model.Team
- if result := <-tchan; result.Err != nil {
- c.Err = result.Err
- return
- } else {
- team = result.Data.(*model.Team)
- }
-
- var user *model.User
- if result := <-uchan; result.Err != nil {
- c.Err = result.Err
- return
- } else {
- user = result.Data.(*model.User)
- }
-
- if !HasPermissionToTeamContext(c, team.Id, model.PERMISSION_ADD_USER_TO_TEAM) {
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_ADD_USER_TO_TEAM) {
+ c.SetPermissionError(model.PERMISSION_ADD_USER_TO_TEAM)
return
}
- err := JoinUserToTeam(team, user)
- if err != nil {
+ if _, err := app.AddUserToTeam(c.TeamId, userId); err != nil {
c.Err = err
return
}
@@ -433,33 +170,14 @@ func removeUserFromTeam(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- tchan := Srv.Store.Team().Get(c.TeamId)
- uchan := Srv.Store.User().Get(userId)
-
- var team *model.Team
- if result := <-tchan; result.Err != nil {
- c.Err = result.Err
- return
- } else {
- team = result.Data.(*model.Team)
- }
-
- var user *model.User
- if result := <-uchan; result.Err != nil {
- c.Err = result.Err
- return
- } else {
- user = result.Data.(*model.User)
- }
-
- if c.Session.UserId != user.Id {
- if !HasPermissionToTeamContext(c, team.Id, model.PERMISSION_REMOVE_USER_FROM_TEAM) {
+ if c.Session.UserId != userId {
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_REMOVE_USER_FROM_TEAM) {
+ c.SetPermissionError(model.PERMISSION_REMOVE_USER_FROM_TEAM)
return
}
}
- err := LeaveTeam(team, user)
- if err != nil {
+ if err := app.RemoveUserFromTeam(c.TeamId, userId); err != nil {
c.Err = err
return
}
@@ -468,73 +186,26 @@ func removeUserFromTeam(c *Context, w http.ResponseWriter, r *http.Request) {
}
func addUserToTeamFromInvite(c *Context, w http.ResponseWriter, r *http.Request) {
-
params := model.MapFromJson(r.Body)
hash := params["hash"]
data := params["data"]
inviteId := params["invite_id"]
- teamId := ""
var team *model.Team
+ var err *model.AppError
if len(hash) > 0 {
- props := model.MapFromJson(strings.NewReader(data))
-
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
- c.Err = model.NewLocAppError("addUserToTeamFromInvite", "api.user.create_user.signup_link_invalid.app_error", nil, "")
- return
- }
-
- t, err := strconv.ParseInt(props["time"], 10, 64)
- if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours
- c.Err = model.NewLocAppError("addUserToTeamFromInvite", "api.user.create_user.signup_link_expired.app_error", nil, "")
- return
- }
-
- teamId = props["id"]
-
- // try to load the team to make sure it exists
- if result := <-Srv.Store.Team().Get(teamId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- team = result.Data.(*model.Team)
- }
- }
-
- if len(inviteId) > 0 {
- if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- team = result.Data.(*model.Team)
- teamId = team.Id
- }
- }
-
- if len(teamId) == 0 {
+ team, err = app.AddUserToTeamByHash(c.Session.UserId, hash, data)
+ } else if len(inviteId) > 0 {
+ team, err = app.AddUserToTeamByInviteId(inviteId, c.Session.UserId)
+ } else {
c.Err = model.NewLocAppError("addUserToTeamFromInvite", "api.user.create_user.signup_link_invalid.app_error", nil, "")
return
}
- uchan := Srv.Store.User().Get(c.Session.UserId)
-
- var user *model.User
- if result := <-uchan; result.Err != nil {
- c.Err = result.Err
+ if err != nil {
+ c.Err = err
return
- } else {
- user = result.Data.(*model.User)
- }
-
- tm := c.Session.GetTeamByTeamId(teamId)
-
- if tm == nil {
- err := JoinUserToTeam(team, user)
- if err != nil {
- c.Err = err
- return
- }
}
team.Sanitize()
@@ -542,20 +213,12 @@ func addUserToTeamFromInvite(c *Context, w http.ResponseWriter, r *http.Request)
w.Write([]byte(team.ToJson()))
}
-func FindTeamByName(name string) bool {
- if result := <-Srv.Store.Team().GetByName(name); result.Err != nil {
- return false
- } else {
- return true
- }
-}
-
func findTeamByName(c *Context, w http.ResponseWriter, r *http.Request) {
m := model.MapFromJson(r.Body)
name := strings.ToLower(strings.TrimSpace(m["name"]))
- found := FindTeamByName(name)
+ found := app.FindTeamByName(name)
if found {
w.Write([]byte("true"))
@@ -568,14 +231,13 @@ func getTeamByName(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
teamname := params["team_name"]
- if result := <-Srv.Store.Team().GetByName(teamname); result.Err != nil {
- c.Err = result.Err
+ if team, err := app.GetTeamByName(teamname); err != nil {
+ c.Err = err
return
} else {
- team := result.Data.(*model.Team)
-
if team.Type != model.TEAM_OPEN && c.Session.GetTeamByTeamId(team.Id) == nil {
- if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
return
}
}
@@ -589,12 +251,11 @@ func getMyTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) {
if len(c.Session.TeamMembers) > 0 {
w.Write([]byte(model.TeamMembersToJson(c.Session.TeamMembers)))
} else {
- if result := <-Srv.Store.Team().GetTeamsForUser(c.Session.UserId); result.Err != nil {
- c.Err = result.Err
+ if members, err := app.GetTeamMembersForUser(c.Session.UserId); err != nil {
+ c.Err = err
return
} else {
- data := result.Data.([]*model.TeamMember)
- w.Write([]byte(model.TeamMembersToJson(data)))
+ w.Write([]byte(model.TeamMembersToJson(members)))
}
}
}
@@ -602,86 +263,17 @@ func getMyTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) {
func getMyTeamsUnread(c *Context, w http.ResponseWriter, r *http.Request) {
teamId := r.URL.Query().Get("id")
- if result := <-Srv.Store.Team().GetTeamsUnreadForUser(teamId, c.Session.UserId); result.Err != nil {
- c.Err = result.Err
+ if unreads, err := app.GetTeamsUnreadForUser(teamId, c.Session.UserId); err != nil {
+ c.Err = err
return
} else {
- data := result.Data.([]*model.ChannelUnread)
- var members []*model.TeamUnread
- membersMap := make(map[string]*model.TeamUnread)
-
- unreads := func(cu *model.ChannelUnread, tu *model.TeamUnread) *model.TeamUnread {
- tu.MentionCount += cu.MentionCount
-
- if cu.NotifyProps["mark_unread"] != model.CHANNEL_MARK_UNREAD_MENTION {
- tu.MsgCount += (cu.TotalMsgCount - cu.MsgCount)
- }
-
- return tu
- }
-
- for i := range data {
- id := data[i].TeamId
- if mu, ok := membersMap[id]; ok {
- membersMap[id] = unreads(data[i], mu)
- } else {
- membersMap[id] = unreads(data[i], &model.TeamUnread{
- MsgCount: 0,
- MentionCount: 0,
- TeamId: id,
- })
- }
- }
-
- for _, val := range membersMap {
- members = append(members, val)
- }
- w.Write([]byte(model.TeamsUnreadToJson(members)))
- }
-}
-
-func InviteMembers(team *model.Team, senderName string, invites []string, siteURL string) {
- for _, invite := range invites {
- if len(invite) > 0 {
- senderRole := utils.T("api.team.invite_members.member")
-
- subject := utils.T("api.templates.invite_subject",
- map[string]interface{}{"SenderName": senderName, "TeamDisplayName": team.DisplayName, "SiteName": utils.ClientCfg["SiteName"]})
-
- bodyPage := utils.NewHTMLTemplate("invite_body", model.DEFAULT_LOCALE)
- bodyPage.Props["SiteURL"] = siteURL
- bodyPage.Props["Title"] = utils.T("api.templates.invite_body.title")
- bodyPage.Html["Info"] = template.HTML(utils.T("api.templates.invite_body.info",
- map[string]interface{}{"SenderStatus": senderRole, "SenderName": senderName, "TeamDisplayName": team.DisplayName}))
- bodyPage.Props["Button"] = utils.T("api.templates.invite_body.button")
- bodyPage.Html["ExtraInfo"] = template.HTML(utils.T("api.templates.invite_body.extra_info",
- map[string]interface{}{"TeamDisplayName": team.DisplayName, "TeamURL": siteURL + "/" + team.Name}))
-
- props := make(map[string]string)
- props["email"] = invite
- props["id"] = team.Id
- props["display_name"] = team.DisplayName
- props["name"] = team.Name
- props["time"] = fmt.Sprintf("%v", model.GetMillis())
- data := model.MapToJson(props)
- hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt))
- bodyPage.Props["Link"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&h=%s", siteURL, url.QueryEscape(data), url.QueryEscape(hash))
-
- if !utils.Cfg.EmailSettings.SendEmailNotifications {
- l4g.Info(utils.T("api.team.invite_members.sending.info"), invite, bodyPage.Props["Link"])
- }
-
- if err := utils.SendMail(invite, subject, bodyPage.Render()); err != nil {
- l4g.Error(utils.T("api.team.invite_members.send.error"), err)
- }
- }
+ w.Write([]byte(model.TeamsUnreadToJson(unreads)))
}
}
func updateTeam(c *Context, w http.ResponseWriter, r *http.Request) {
team := model.TeamFromJson(r.Body)
-
if team == nil {
c.SetInvalidParam("updateTeam", "team")
return
@@ -689,40 +281,21 @@ func updateTeam(c *Context, w http.ResponseWriter, r *http.Request) {
team.Id = c.TeamId
- if !HasPermissionToTeamContext(c, team.Id, model.PERMISSION_MANAGE_TEAM) {
- c.Err = model.NewLocAppError("updateTeam", "api.team.update_team.permissions.app_error", nil, "userId="+c.Session.UserId)
- c.Err.StatusCode = http.StatusForbidden
- return
- }
-
- var oldTeam *model.Team
- if result := <-Srv.Store.Team().Get(team.Id); result.Err != nil {
- c.Err = result.Err
+ if !app.SessionHasPermissionToTeam(c.Session, team.Id, model.PERMISSION_MANAGE_TEAM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_TEAM)
return
- } else {
- oldTeam = result.Data.(*model.Team)
}
- oldTeam.DisplayName = team.DisplayName
- oldTeam.Description = team.Description
- oldTeam.InviteId = team.InviteId
- oldTeam.AllowOpenInvite = team.AllowOpenInvite
- oldTeam.CompanyName = team.CompanyName
- oldTeam.AllowedDomains = team.AllowedDomains
- //oldTeam.Type = team.Type
+ var err *model.AppError
+ var updatedTeam *model.Team
- if result := <-Srv.Store.Team().Update(oldTeam); result.Err != nil {
- c.Err = result.Err
+ updatedTeam, err = app.UpdateTeam(team)
+ if err != nil {
+ c.Err = err
return
}
- oldTeam.Sanitize()
-
- message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_UPDATE_TEAM, "", "", "", nil)
- message.Add("team", oldTeam.ToJson())
- go Publish(message)
-
- w.Write([]byte(oldTeam.ToJson()))
+ w.Write([]byte(updatedTeam.ToJson()))
}
func updateMemberRoles(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -734,8 +307,6 @@ func updateMemberRoles(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- mchan := Srv.Store.Team().GetTeamsForUser(userId)
-
teamId := c.TeamId
newRoles := props["new_roles"]
@@ -744,116 +315,59 @@ func updateMemberRoles(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToTeamContext(c, teamId, model.PERMISSION_MANAGE_ROLES) {
+ if !app.SessionHasPermissionToTeam(c.Session, teamId, model.PERMISSION_MANAGE_TEAM_ROLES) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_TEAM_ROLES)
return
}
- var member *model.TeamMember
- if result := <-mchan; result.Err != nil {
- c.Err = result.Err
- return
- } else {
- members := result.Data.([]*model.TeamMember)
- for _, m := range members {
- if m.TeamId == teamId {
- member = m
- }
- }
- }
-
- if member == nil {
- c.Err = model.NewLocAppError("updateMemberRoles", "api.team.update_member_roles.not_a_member", nil, "userId="+userId+" teamId="+teamId)
- c.Err.StatusCode = http.StatusBadRequest
- return
- }
-
- member.Roles = newRoles
-
- if result := <-Srv.Store.Team().UpdateMember(member); result.Err != nil {
- c.Err = result.Err
+ if _, err := app.UpdateTeamMemberRoles(teamId, userId, newRoles); err != nil {
+ c.Err = err
return
}
- RemoveAllSessionsForUserId(userId)
-
rdata := map[string]string{}
rdata["status"] = "ok"
w.Write([]byte(model.MapToJson(rdata)))
}
-func PermanentDeleteTeam(team *model.Team) *model.AppError {
- team.DeleteAt = model.GetMillis()
- if result := <-Srv.Store.Team().Update(team); result.Err != nil {
- return result.Err
- }
-
- if result := <-Srv.Store.Channel().PermanentDeleteByTeam(team.Id); result.Err != nil {
- return result.Err
- }
-
- if result := <-Srv.Store.Team().RemoveAllMembersByTeam(team.Id); result.Err != nil {
- return result.Err
- }
-
- if result := <-Srv.Store.Team().PermanentDelete(team.Id); result.Err != nil {
- return result.Err
- }
-
- return nil
-}
-
func getMyTeam(c *Context, w http.ResponseWriter, r *http.Request) {
if len(c.TeamId) == 0 {
return
}
- if result := <-Srv.Store.Team().Get(c.TeamId); result.Err != nil {
- c.Err = result.Err
+ if team, err := app.GetTeam(c.TeamId); err != nil {
+ c.Err = err
return
- } else if HandleEtag(result.Data.(*model.Team).Etag(), "Get My Team", w, r) {
+ } else if HandleEtag(team.Etag(), "Get My Team", w, r) {
return
} else {
- w.Header().Set(model.HEADER_ETAG_SERVER, result.Data.(*model.Team).Etag())
- w.Write([]byte(result.Data.(*model.Team).ToJson()))
+ w.Header().Set(model.HEADER_ETAG_SERVER, team.Etag())
+ w.Write([]byte(team.ToJson()))
return
}
}
func getTeamStats(c *Context, w http.ResponseWriter, r *http.Request) {
if c.Session.GetTeamByTeamId(c.TeamId) == nil {
- if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
return
}
}
- tchan := Srv.Store.Team().GetTotalMemberCount(c.TeamId)
- achan := Srv.Store.Team().GetActiveMemberCount(c.TeamId)
-
- stats := &model.TeamStats{}
- stats.TeamId = c.TeamId
-
- if result := <-tchan; result.Err != nil {
- c.Err = result.Err
- return
- } else {
- stats.TotalMemberCount = result.Data.(int64)
- }
-
- if result := <-achan; result.Err != nil {
- c.Err = result.Err
+ stats, err := app.GetTeamStats(c.TeamId)
+ if err != nil {
+ c.Err = err
return
- } else {
- stats.ActiveMemberCount = result.Data.(int64)
}
w.Write([]byte(stats.ToJson()))
}
func importTeam(c *Context, w http.ResponseWriter, r *http.Request) {
- if !HasPermissionToCurrentTeamContext(c, model.PERMISSION_IMPORT_TEAM) {
- c.Err = model.NewLocAppError("importTeam", "api.team.import_team.admin.app_error", nil, "userId="+c.Session.UserId)
- c.Err.StatusCode = http.StatusForbidden
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_IMPORT_TEAM) {
+ c.SetPermissionError(model.PERMISSION_IMPORT_TEAM)
return
}
@@ -906,7 +420,7 @@ func importTeam(c *Context, w http.ResponseWriter, r *http.Request) {
switch importFrom {
case "slack":
var err *model.AppError
- if err, log = SlackImport(fileData, fileSize, c.TeamId); err != nil {
+ if err, log = app.SlackImport(fileData, fileSize, c.TeamId); err != nil {
c.Err = err
c.Err.StatusCode = http.StatusBadRequest
}
@@ -925,11 +439,10 @@ func getInviteInfo(c *Context, w http.ResponseWriter, r *http.Request) {
m := model.MapFromJson(r.Body)
inviteId := m["invite_id"]
- if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil {
- c.Err = result.Err
+ if team, err := app.GetTeamByInviteId(inviteId); err != nil {
+ c.Err = err
return
} else {
- team := result.Data.(*model.Team)
if !(team.Type == model.TEAM_OPEN) {
c.Err = model.NewLocAppError("getInviteInfo", "api.team.get_invite_info.not_open_team", nil, "id="+inviteId)
return
@@ -960,16 +473,16 @@ func getTeamMembers(c *Context, w http.ResponseWriter, r *http.Request) {
}
if c.Session.GetTeamByTeamId(c.TeamId) == nil {
- if !HasPermissionToTeamContext(c, c.TeamId, model.PERMISSION_MANAGE_SYSTEM) {
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
return
}
}
- if result := <-Srv.Store.Team().GetMembers(c.TeamId, offset, limit); result.Err != nil {
- c.Err = result.Err
+ if members, err := app.GetTeamMembers(c.TeamId, offset, limit); err != nil {
+ c.Err = err
return
} else {
- members := result.Data.([]*model.TeamMember)
w.Write([]byte(model.TeamMembersToJson(members)))
return
}
@@ -985,16 +498,16 @@ func getTeamMember(c *Context, w http.ResponseWriter, r *http.Request) {
}
if c.Session.GetTeamByTeamId(c.TeamId) == nil {
- if !HasPermissionToTeamContext(c, c.TeamId, model.PERMISSION_MANAGE_SYSTEM) {
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
return
}
}
- if result := <-Srv.Store.Team().GetMember(c.TeamId, userId); result.Err != nil {
- c.Err = result.Err
+ if member, err := app.GetTeamMember(c.TeamId, userId); err != nil {
+ c.Err = err
return
} else {
- member := result.Data.(model.TeamMember)
w.Write([]byte(member.ToJson()))
return
}
@@ -1008,16 +521,16 @@ func getTeamMembersByIds(c *Context, w http.ResponseWriter, r *http.Request) {
}
if c.Session.GetTeamByTeamId(c.TeamId) == nil {
- if !HasPermissionToTeamContext(c, c.TeamId, model.PERMISSION_MANAGE_SYSTEM) {
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
return
}
}
- if result := <-Srv.Store.Team().GetMembersByIds(c.TeamId, userIds); result.Err != nil {
- c.Err = result.Err
+ if members, err := app.GetTeamMembersByIds(c.TeamId, userIds); err != nil {
+ c.Err = err
return
} else {
- members := result.Data.([]*model.TeamMember)
w.Write([]byte(model.TeamMembersToJson(members)))
return
}
diff --git a/api/team_test.go b/api/team_test.go
index 7403afd8a..5720f71d7 100644
--- a/api/team_test.go
+++ b/api/team_test.go
@@ -6,6 +6,7 @@ package api
import (
"testing"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
@@ -13,7 +14,6 @@ import (
func TestCreateTeam(t *testing.T) {
th := Setup().InitBasic()
- th.BasicClient.Logout()
Client := th.BasicClient
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
@@ -25,7 +25,7 @@ func TestCreateTeam(t *testing.T) {
user := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
LinkUserToTeam(user, rteam.Data.(*model.Team))
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user.Id))
Client.Login(user.Email, "passwd1")
Client.SetTeamId(rteam.Data.(*model.Team).Id)
@@ -57,6 +57,8 @@ func TestCreateTeam(t *testing.T) {
func TestAddUserToTeam(t *testing.T) {
th := Setup().InitSystemAdmin().InitBasic()
+ th.BasicClient.Logout()
+ th.LoginBasic2()
user2 := th.CreateUser(th.BasicClient)
@@ -65,7 +67,7 @@ func TestAddUserToTeam(t *testing.T) {
}
th.SystemAdminClient.SetTeamId(th.BasicTeam.Id)
- if _, err := th.SystemAdminClient.UpdateTeamRoles(th.BasicUser.Id, "team_user team_admin"); err != nil {
+ if _, err := th.SystemAdminClient.UpdateTeamRoles(th.BasicUser2.Id, "team_user team_admin"); err != nil {
t.Fatal(err)
}
@@ -77,6 +79,10 @@ func TestAddUserToTeam(t *testing.T) {
t.Fatal("ids didn't match")
}
}
+
+ if _, err := th.BasicClient.GetTeamMember(th.BasicTeam.Id, user2.Id); err != nil {
+ t.Fatal(err)
+ }
}
func TestRemoveUserFromTeam(t *testing.T) {
@@ -131,16 +137,17 @@ func TestAddUserToTeamFromInvite(t *testing.T) {
func TestGetAllTeams(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
- th.BasicClient.Logout()
Client := th.BasicClient
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+ Client.Logout()
+
user := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
LinkUserToTeam(user, team)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user.Id))
Client.Login(user.Email, "passwd1")
Client.SetTeamId(team.Id)
@@ -164,16 +171,17 @@ func TestGetAllTeams(t *testing.T) {
func TestGetAllTeamListings(t *testing.T) {
th := Setup().InitBasic()
- th.BasicClient.Logout()
Client := th.BasicClient
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN, AllowOpenInvite: true}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+ Client.Logout()
+
user := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
LinkUserToTeam(user, team)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user.Id))
Client.Login(user.Email, "passwd1")
Client.SetTeamId(team.Id)
@@ -190,7 +198,7 @@ func TestGetAllTeamListings(t *testing.T) {
}
}
- UpdateUserRoles(user, model.ROLE_SYSTEM_ADMIN.Id)
+ app.UpdateUserRoles(user.Id, model.ROLE_SYSTEM_ADMIN.Id)
Client.Login(user.Email, "passwd1")
Client.SetTeamId(team.Id)
@@ -210,16 +218,17 @@ func TestGetAllTeamListings(t *testing.T) {
func TestTeamPermDelete(t *testing.T) {
th := Setup().InitBasic()
- th.BasicClient.Logout()
Client := th.BasicClient
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+ Client.Logout()
+
user1 := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
LinkUserToTeam(user1, team)
- store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user1.Id))
Client.Login(user1.Email, "passwd1")
Client.SetTeamId(team.Id)
@@ -243,7 +252,7 @@ func TestTeamPermDelete(t *testing.T) {
c.RequestId = model.NewId()
c.IpAddress = "test"
- err := PermanentDeleteTeam(team)
+ err := app.PermanentDeleteTeam(team)
if err != nil {
t.Fatal(err)
}
@@ -253,17 +262,18 @@ func TestTeamPermDelete(t *testing.T) {
func TestInviteMembers(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
- th.BasicClient.Logout()
Client := th.BasicClient
SystemAdminClient := th.SystemAdminClient
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+ Client.Logout()
+
user := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
LinkUserToTeam(user, team)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user.Id))
Client.Login(user.Email, "passwd1")
Client.SetTeamId(team.Id)
@@ -338,21 +348,17 @@ func TestInviteMembers(t *testing.T) {
func TestUpdateTeamDisplayName(t *testing.T) {
th := Setup().InitBasic()
- th.BasicClient.Logout()
Client := th.BasicClient
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "success+" + model.NewId() + "@simulator.amazonses.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
- user := &model.User{Email: team.Email, Nickname: "Corey Hulen", Password: "passwd1"}
- user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
- LinkUserToTeam(user, team)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ Client.Logout()
user2 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
LinkUserToTeam(user2, team)
- store.Must(Srv.Store.User().VerifyEmail(user2.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user2.Id))
Client.Login(user2.Email, "passwd1")
Client.SetTeamId(team.Id)
@@ -363,7 +369,7 @@ func TestUpdateTeamDisplayName(t *testing.T) {
t.Fatal("Should have errored, not admin")
}
- Client.Login(user.Email, "passwd1")
+ th.LoginBasic()
vteam.DisplayName = ""
if _, err := Client.UpdateTeam(vteam); err == nil {
@@ -378,7 +384,6 @@ func TestUpdateTeamDisplayName(t *testing.T) {
func TestFuzzyTeamCreate(t *testing.T) {
th := Setup().InitBasic()
- th.BasicClient.Logout()
Client := th.BasicClient
for i := 0; i < len(utils.FUZZY_STRINGS_NAMES) || i < len(utils.FUZZY_STRINGS_EMAILS); i++ {
@@ -403,17 +408,18 @@ func TestFuzzyTeamCreate(t *testing.T) {
func TestGetMyTeam(t *testing.T) {
th := Setup().InitBasic()
- th.BasicClient.Logout()
Client := th.BasicClient
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(team)
team = rteam.Data.(*model.Team)
+ Client.Logout()
+
user := model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser, _ := Client.CreateUser(&user, "")
LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team))
- store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
Client.Login(user.Email, user.Password)
Client.SetTeamId(team.Id)
@@ -658,7 +664,7 @@ func TestGetTeamStats(t *testing.T) {
user := model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser, _ := Client.CreateUser(&user, "")
- store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
Client.Login(user.Email, user.Password)
@@ -669,21 +675,17 @@ func TestGetTeamStats(t *testing.T) {
func TestUpdateTeamDescription(t *testing.T) {
th := Setup().InitBasic()
- th.BasicClient.Logout()
Client := th.BasicClient
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "success+" + model.NewId() + "@simulator.amazonses.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
- user := &model.User{Email: team.Email, Nickname: "My Testing", Password: "passwd1"}
- user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
- LinkUserToTeam(user, team)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ Client.Logout()
user2 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Jabba the Hutt", Password: "passwd1"}
user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
LinkUserToTeam(user2, team)
- store.Must(Srv.Store.User().VerifyEmail(user2.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user2.Id))
Client.Login(user2.Email, "passwd1")
Client.SetTeamId(team.Id)
@@ -694,7 +696,7 @@ func TestUpdateTeamDescription(t *testing.T) {
t.Fatal("Should have errored, not admin")
}
- Client.Login(user.Email, "passwd1")
+ th.LoginBasic()
vteam.Description = ""
if _, err := Client.UpdateTeam(vteam); err != nil {
@@ -709,7 +711,6 @@ func TestUpdateTeamDescription(t *testing.T) {
func TestGetTeamByName(t *testing.T) {
th := Setup().InitSystemAdmin().InitBasic()
- th.BasicClient.Logout()
Client := th.BasicClient
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "success+" + model.NewId() + "@simulator.amazonses.com", Type: model.TEAM_INVITE}
@@ -718,12 +719,6 @@ func TestGetTeamByName(t *testing.T) {
team2 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "success+" + model.NewId() + "@simulator.amazonses.com", Type: model.TEAM_OPEN}
team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team)
- user := &model.User{Email: team.Email, Nickname: "My Testing", Password: "passwd1"}
- user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
- LinkUserToTeam(user, team)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
-
- Client.Login(user.Email, "passwd1")
if _, err := Client.GetTeamByName(team.Name); err != nil {
t.Fatal("Failed to get team")
}
@@ -740,13 +735,13 @@ func TestGetTeamByName(t *testing.T) {
user2 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Jabba the Hutt", Password: "passwd1"}
user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
- store.Must(Srv.Store.User().VerifyEmail(user2.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user2.Id))
Client.Login(user2.Email, "passwd1")
// TEAM_INVITE and user is not part of the team
if _, err := Client.GetTeamByName(team.Name); err == nil {
- t.Fatal("Should not fail dont have permissions to get the team")
+ t.Fatal("Should fail dont have permissions to get the team")
}
if _, err := Client.GetTeamByName("InvalidTeamName"); err == nil {
diff --git a/api/user.go b/api/user.go
index 760f4faea..7722e917b 100644
--- a/api/user.go
+++ b/api/user.go
@@ -7,26 +7,15 @@ import (
"bytes"
b64 "encoding/base64"
"fmt"
- "hash/fnv"
- "html/template"
- "image"
- "image/color"
- "image/draw"
- _ "image/gif"
- _ "image/jpeg"
- "image/png"
"io"
- "io/ioutil"
"net/http"
- "net/url"
"strconv"
"strings"
"time"
l4g "github.com/alecthomas/log4go"
- "github.com/disintegration/imaging"
- "github.com/golang/freetype"
"github.com/gorilla/mux"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
@@ -84,7 +73,7 @@ func InitUser() {
BaseRoutes.Root.Handle("/login/sso/saml", AppHandlerIndependent(loginWithSaml)).Methods("GET")
BaseRoutes.Root.Handle("/login/sso/saml", AppHandlerIndependent(completeSaml)).Methods("POST")
- BaseRoutes.WebSocket.Handle("user_typing", ApiWebSocketHandler(userTyping))
+ app.Srv.WebSocketRouter.Handle("user_typing", ApiWebSocketHandler(userTyping))
}
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -101,348 +90,45 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- hash := r.URL.Query().Get("h")
- teamId := ""
- var team *model.Team
- shouldSendWelcomeEmail := true
user.EmailVerified = false
- if len(hash) > 0 {
- data := r.URL.Query().Get("d")
- props := model.MapFromJson(strings.NewReader(data))
-
- if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
- c.Err = model.NewLocAppError("createUser", "api.user.create_user.signup_link_invalid.app_error", nil, "")
- return
- }
-
- t, err := strconv.ParseInt(props["time"], 10, 64)
- if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours
- c.Err = model.NewLocAppError("createUser", "api.user.create_user.signup_link_expired.app_error", nil, "")
- return
- }
-
- teamId = props["id"]
-
- // try to load the team to make sure it exists
- if result := <-Srv.Store.Team().Get(teamId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- team = result.Data.(*model.Team)
- }
-
- user.Email = props["email"]
- user.EmailVerified = true
- shouldSendWelcomeEmail = false
- }
-
+ hash := r.URL.Query().Get("h")
inviteId := r.URL.Query().Get("iid")
- if len(inviteId) > 0 {
- if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- team = result.Data.(*model.Team)
- teamId = team.Id
- }
- }
- firstAccount := false
- if sessionCache.Len() == 0 {
- if cr := <-Srv.Store.User().GetTotalUsersCount(); cr.Err != nil {
- c.Err = cr.Err
+ var ruser *model.User
+ var err *model.AppError
+ if len(hash) > 0 {
+ data := r.URL.Query().Get("d")
+ ruser, err = app.CreateUserWithHash(user, hash, data)
+ if err != nil {
+ c.Err = err
return
- } else {
- count := cr.Data.(int64)
- if count <= 0 {
- firstAccount = true
- }
}
- }
-
- if !firstAccount && !*utils.Cfg.TeamSettings.EnableOpenServer && len(teamId) == 0 {
- c.Err = model.NewLocAppError("createUser", "api.user.create_user.no_open_server", nil, "email="+user.Email)
- return
- }
-
- if !CheckUserDomain(user, utils.Cfg.TeamSettings.RestrictCreationToDomains) {
- c.Err = model.NewLocAppError("createUser", "api.user.create_user.accepted_domain.app_error", nil, "")
- return
- }
-
- ruser, err := CreateUser(user)
- if err != nil {
- c.Err = err
- return
- }
-
- if len(teamId) > 0 {
- err := JoinUserToTeam(team, ruser)
+ } else if len(inviteId) > 0 {
+ ruser, err = app.CreateUserWithInviteId(user, inviteId, c.GetSiteURL())
if err != nil {
c.Err = err
return
}
-
- go addDirectChannels(team.Id, ruser)
- }
-
- if shouldSendWelcomeEmail {
- go sendWelcomeEmail(c, ruser.Id, ruser.Email, c.GetSiteURL(), ruser.EmailVerified)
- }
-
- w.Write([]byte(ruser.ToJson()))
-
-}
-
-// Check that a user's email domain matches a list of space-delimited domains as a string.
-func CheckUserDomain(user *model.User, domains string) bool {
- if len(domains) == 0 {
- return true
- }
-
- domainArray := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(domains, "@", " ", -1), ",", " ", -1))))
-
- matched := false
- for _, d := range domainArray {
- if strings.HasSuffix(strings.ToLower(user.Email), "@"+d) {
- matched = true
- break
- }
- }
-
- return matched
-}
-
-func IsVerifyHashRequired(user *model.User, team *model.Team, hash string) bool {
- shouldVerifyHash := true
-
- if team.Type == model.TEAM_INVITE && len(team.AllowedDomains) > 0 && len(hash) == 0 && user != nil {
- matched := CheckUserDomain(user, team.AllowedDomains)
-
- if matched {
- shouldVerifyHash = false
- } else {
- return true
- }
- }
-
- if team.Type == model.TEAM_OPEN {
- shouldVerifyHash = false
- }
-
- if len(hash) > 0 {
- shouldVerifyHash = true
- }
-
- return shouldVerifyHash
-}
-
-func CreateUser(user *model.User) (*model.User, *model.AppError) {
-
- user.Roles = model.ROLE_SYSTEM_USER.Id
-
- // Below is a special case where the first user in the entire
- // system is granted the system_admin role
- if result := <-Srv.Store.User().GetTotalUsersCount(); result.Err != nil {
- return nil, result.Err
} else {
- count := result.Data.(int64)
- if count <= 0 {
- user.Roles = model.ROLE_SYSTEM_ADMIN.Id + " " + model.ROLE_SYSTEM_USER.Id
- }
- }
-
- user.MakeNonNil()
- user.Locale = *utils.Cfg.LocalizationSettings.DefaultClientLocale
-
- if err := utils.IsPasswordValid(user.Password); user.AuthService == "" && err != nil {
- return nil, err
- }
-
- if result := <-Srv.Store.User().Save(user); result.Err != nil {
- l4g.Error(utils.T("api.user.create_user.save.error"), result.Err)
- return nil, result.Err
- } else {
- ruser := result.Data.(*model.User)
-
- if user.EmailVerified {
- if cresult := <-Srv.Store.User().VerifyEmail(ruser.Id); cresult.Err != nil {
- l4g.Error(utils.T("api.user.create_user.verified.error"), cresult.Err)
- }
- }
-
- pref := model.Preference{UserId: ruser.Id, Category: model.PREFERENCE_CATEGORY_TUTORIAL_STEPS, Name: ruser.Id, Value: "0"}
- if presult := <-Srv.Store.Preference().Save(&model.Preferences{pref}); presult.Err != nil {
- l4g.Error(utils.T("api.user.create_user.tutorial.error"), presult.Err.Message)
- }
-
- ruser.Sanitize(map[string]bool{})
-
- // This message goes to everyone, so the teamId, channelId and userId are irrelevant
- message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_NEW_USER, "", "", "", nil)
- message.Add("user_id", ruser.Id)
- go Publish(message)
-
- return ruser, nil
- }
-}
-
-func CreateOAuthUser(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.Reader, teamId string) *model.User {
- var user *model.User
- provider := einterfaces.GetOauthProvider(service)
- if provider == nil {
- c.Err = model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.not_available.app_error", map[string]interface{}{"Service": strings.Title(service)}, "")
- return nil
- } else {
- user = provider.GetUserFromJson(userData)
- }
-
- if user == nil {
- c.Err = model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.create.app_error", map[string]interface{}{"Service": service}, "")
- return nil
- }
-
- suchan := Srv.Store.User().GetByAuth(user.AuthData, service)
- euchan := Srv.Store.User().GetByEmail(user.Email)
-
- found := true
- count := 0
- for found {
- if found = IsUsernameTaken(user.Username); found {
- user.Username = user.Username + strconv.Itoa(count)
- count += 1
- }
- }
-
- if result := <-suchan; result.Err == nil {
- c.Err = model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_used.app_error",
- map[string]interface{}{"Service": service}, "email="+user.Email)
- return nil
- }
-
- if result := <-euchan; result.Err == nil {
- authService := result.Data.(*model.User).AuthService
- if authService == "" {
- c.Err = model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error",
- map[string]interface{}{"Service": service, "Auth": model.USER_AUTH_SERVICE_EMAIL}, "email="+user.Email)
- } else {
- c.Err = model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error",
- map[string]interface{}{"Service": service, "Auth": authService}, "email="+user.Email)
+ if !app.IsFirstUserAccount() && !*utils.Cfg.TeamSettings.EnableOpenServer {
+ c.Err = model.NewLocAppError("createUser", "api.user.create_user.no_open_server", nil, "email="+user.Email)
+ return
}
- return nil
- }
-
- user.EmailVerified = true
- ruser, err := CreateUser(user)
- if err != nil {
- c.Err = err
- return nil
- }
-
- if len(teamId) > 0 {
- err = JoinUserToTeamById(teamId, user)
+ ruser, err = app.CreateUser(user)
if err != nil {
c.Err = err
- return nil
- }
-
- go addDirectChannels(teamId, user)
- }
-
- doLogin(c, w, r, ruser, "")
- if c.Err != nil {
- return nil
- }
-
- return ruser
-}
-
-func sendWelcomeEmail(c *Context, userId string, email string, siteURL string, verified bool) {
- rawUrl, _ := url.Parse(siteURL)
-
- subject := c.T("api.templates.welcome_subject", map[string]interface{}{"ServerURL": rawUrl.Host})
-
- bodyPage := utils.NewHTMLTemplate("welcome_body", c.Locale)
- bodyPage.Props["SiteURL"] = siteURL
- bodyPage.Props["Title"] = c.T("api.templates.welcome_body.title", map[string]interface{}{"ServerURL": rawUrl.Host})
- bodyPage.Props["Info"] = c.T("api.templates.welcome_body.info")
- bodyPage.Props["Button"] = c.T("api.templates.welcome_body.button")
- bodyPage.Props["Info2"] = c.T("api.templates.welcome_body.info2")
- bodyPage.Props["Info3"] = c.T("api.templates.welcome_body.info3")
- bodyPage.Props["SiteURL"] = siteURL
-
- if *utils.Cfg.NativeAppSettings.AppDownloadLink != "" {
- bodyPage.Props["AppDownloadInfo"] = c.T("api.templates.welcome_body.app_download_info")
- bodyPage.Props["AppDownloadLink"] = *utils.Cfg.NativeAppSettings.AppDownloadLink
- }
-
- if !verified {
- link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashPassword(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(email))
- bodyPage.Props["VerifyUrl"] = link
- }
-
- if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil {
- l4g.Error(utils.T("api.user.send_welcome_email_and_forget.failed.error"), err)
- }
-}
-
-func addDirectChannels(teamId string, user *model.User) {
- var profiles map[string]*model.User
- if result := <-Srv.Store.User().GetProfiles(teamId, 0, 100); result.Err != nil {
- l4g.Error(utils.T("api.user.add_direct_channels_and_forget.failed.error"), user.Id, teamId, result.Err.Error())
- return
- } else {
- profiles = result.Data.(map[string]*model.User)
- }
-
- var preferences model.Preferences
-
- for id := range profiles {
- if id == user.Id {
- continue
+ return
}
- profile := profiles[id]
-
- preference := model.Preference{
- UserId: user.Id,
- Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
- Name: profile.Id,
- Value: "true",
+ if err := app.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, c.GetSiteURL()); err != nil {
+ l4g.Error(err.Error())
}
-
- preferences = append(preferences, preference)
-
- if len(preferences) >= 10 {
- break
- }
- }
-
- if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil {
- l4g.Error(utils.T("api.user.add_direct_channels_and_forget.failed.error"), user.Id, teamId, result.Err.Error())
}
-}
-
-func SendVerifyEmail(c *Context, userId, userEmail, siteURL string) {
- link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashPassword(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(userEmail))
- url, _ := url.Parse(siteURL)
-
- subject := c.T("api.templates.verify_subject",
- map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
-
- bodyPage := utils.NewHTMLTemplate("verify_body", c.Locale)
- bodyPage.Props["SiteURL"] = siteURL
- bodyPage.Props["Title"] = c.T("api.templates.verify_body.title", map[string]interface{}{"ServerURL": url.Host})
- bodyPage.Props["Info"] = c.T("api.templates.verify_body.info")
- bodyPage.Props["VerifyUrl"] = link
- bodyPage.Props["Button"] = c.T("api.templates.verify_body.button")
+ w.Write([]byte(ruser.ToJson()))
- if err := utils.SendMail(userEmail, subject, bodyPage.Render()); err != nil {
- l4g.Error(utils.T("api.user.send_verify_email_and_forget.failed.error"), err)
- }
}
func login(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -467,21 +153,19 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) {
if len(id) != 0 {
c.LogAuditWithUserId(id, "attempt")
- if result := <-Srv.Store.User().Get(id); result.Err != nil {
+ if user, err = app.GetUser(id); err != nil {
c.LogAuditWithUserId(id, "failure")
- c.Err = result.Err
+ c.Err = err
c.Err.StatusCode = http.StatusBadRequest
if einterfaces.GetMetricsInterface() != nil {
einterfaces.GetMetricsInterface().IncrementLoginFail()
}
return
- } else {
- user = result.Data.(*model.User)
}
} else {
c.LogAudit("attempt")
- if user, err = getUserForLogin(loginId, ldapOnly); err != nil {
+ if user, err = app.GetUserForLogin(loginId, ldapOnly); err != nil {
c.LogAudit("failure")
c.Err = err
if einterfaces.GetMetricsInterface() != nil {
@@ -518,37 +202,6 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(user.ToJson()))
}
-func getUserForLogin(loginId string, onlyLdap bool) (*model.User, *model.AppError) {
- ldapAvailable := *utils.Cfg.LdapSettings.Enable && einterfaces.GetLdapInterface() != nil && utils.IsLicensed && *utils.License.Features.LDAP
-
- if result := <-Srv.Store.User().GetForLogin(
- loginId,
- *utils.Cfg.EmailSettings.EnableSignInWithUsername && !onlyLdap,
- *utils.Cfg.EmailSettings.EnableSignInWithEmail && !onlyLdap,
- ldapAvailable,
- ); result.Err != nil && result.Err.Id == "store.sql_user.get_for_login.multiple_users" {
- // don't fall back to LDAP in this case since we already know there's an LDAP user, but that it shouldn't work
- result.Err.StatusCode = http.StatusBadRequest
- return nil, result.Err
- } else if result.Err != nil {
- if !ldapAvailable {
- // failed to find user and no LDAP server to fall back on
- result.Err.StatusCode = http.StatusBadRequest
- return nil, result.Err
- }
-
- // fall back to LDAP server to see if we can find a user
- if ldapUser, ldapErr := einterfaces.GetLdapInterface().GetUser(loginId); ldapErr != nil {
- ldapErr.StatusCode = http.StatusBadRequest
- return nil, ldapErr
- } else {
- return ldapUser, nil
- }
- } else {
- return result.Data.(*model.User), nil
- }
-}
-
func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service string, userData io.Reader) *model.User {
buf := bytes.Buffer{}
buf.ReadFrom(userData)
@@ -570,20 +223,23 @@ func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service st
}
var user *model.User
- if result := <-Srv.Store.User().GetByAuth(&authData, service); result.Err != nil {
- if result.Err.Id == store.MISSING_AUTH_ACCOUNT_ERROR {
- return CreateOAuthUser(c, w, r, service, bytes.NewReader(buf.Bytes()), "")
+ var err *model.AppError
+ if user, err = app.GetUserByAuth(&authData, service); err != nil {
+ if err.Id == store.MISSING_AUTH_ACCOUNT_ERROR {
+ if user, err = app.CreateOAuthUser(service, bytes.NewReader(buf.Bytes()), ""); err != nil {
+ c.Err = err
+ return nil
+ }
}
- c.Err = result.Err
+ c.Err = err
+ return nil
+ }
+
+ doLogin(c, w, r, user, "")
+ if c.Err != nil {
return nil
- } else {
- user = result.Data.(*model.User)
- doLogin(c, w, r, user, "")
- if c.Err != nil {
- return nil
- }
- return user
}
+ return user
}
// User MUST be authenticated completely before calling Login
@@ -598,22 +254,10 @@ func doLogin(c *Context, w http.ResponseWriter, r *http.Request, user *model.Use
maxAge = *utils.Cfg.ServiceSettings.SessionLengthMobileInDays * 60 * 60 * 24
// A special case where we logout of all other sessions with the same Id
- if result := <-Srv.Store.Session().GetSessions(user.Id); result.Err != nil {
- c.Err = result.Err
+ if err := app.RevokeSessionsForDeviceId(user.Id, deviceId, ""); err != nil {
+ c.Err = err
c.Err.StatusCode = http.StatusInternalServerError
return
- } else {
- sessions := result.Data.([]*model.Session)
- for _, session := range sessions {
- if session.DeviceId == deviceId {
- l4g.Debug(utils.T("api.user.login.revoking.app_error"), session.Id, user.Id)
- RevokeSessionById(c, session.Id)
- if c.Err != nil {
- c.LogError(c.Err)
- c.Err = nil
- }
- }
- }
}
} else {
session.SetExpireInDays(*utils.Cfg.ServiceSettings.SessionLengthWebInDays)
@@ -644,13 +288,11 @@ func doLogin(c *Context, w http.ResponseWriter, r *http.Request, user *model.Use
session.AddProp(model.SESSION_PROP_OS, os)
session.AddProp(model.SESSION_PROP_BROWSER, fmt.Sprintf("%v/%v", bname, bversion))
- if result := <-Srv.Store.Session().Save(session); result.Err != nil {
- c.Err = result.Err
+ var err *model.AppError
+ if session, err = app.CreateSession(session); err != nil {
+ c.Err = err
c.Err.StatusCode = http.StatusInternalServerError
return
- } else {
- session = result.Data.(*model.Session)
- AddSessionToCache(session)
}
w.Header().Set(model.HEADER_TOKEN, session.Token)
@@ -679,7 +321,12 @@ func doLogin(c *Context, w http.ResponseWriter, r *http.Request, user *model.Use
func revokeSession(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJson(r.Body)
id := props["id"]
- RevokeSessionById(c, id)
+
+ if err := app.RevokeSessionById(id); err != nil {
+ c.Err = err
+ return
+ }
+
w.Write([]byte(model.MapToJson(props)))
}
@@ -697,26 +344,14 @@ func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- // A special case where we logout of all other sessions with the same Id
- if result := <-Srv.Store.Session().GetSessions(c.Session.UserId); result.Err != nil {
- c.Err = result.Err
+ // A special case where we logout of all other sessions with the same device id
+ if err := app.RevokeSessionsForDeviceId(c.Session.UserId, deviceId, c.Session.Id); err != nil {
+ c.Err = err
c.Err.StatusCode = http.StatusInternalServerError
return
- } else {
- sessions := result.Data.([]*model.Session)
- for _, session := range sessions {
- if session.DeviceId == deviceId && session.Id != c.Session.Id {
- l4g.Debug(utils.T("api.user.login.revoking.app_error"), session.Id, c.Session.UserId)
- RevokeSessionById(c, session.Id)
- if c.Err != nil {
- c.LogError(c.Err)
- c.Err = nil
- }
- }
- }
}
- RemoveAllSessionsForUserId(c.Session.UserId)
+ app.ClearSessionCacheForUser(c.Session.UserId)
c.Session.SetExpireInDays(*utils.Cfg.ServiceSettings.SessionLengthMobileInDays)
maxAge := *utils.Cfg.ServiceSettings.SessionLengthMobileInDays * 60 * 60 * 24
@@ -739,100 +374,28 @@ func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, sessionCookie)
- if result := <-Srv.Store.Session().UpdateDeviceId(c.Session.Id, deviceId, c.Session.ExpiresAt); result.Err != nil {
- c.Err = result.Err
+ if err := app.AttachDeviceId(c.Session.Id, deviceId, c.Session.ExpiresAt); err != nil {
+ c.Err = err
return
}
w.Write([]byte(model.MapToJson(props)))
}
-func RevokeSessionById(c *Context, sessionId string) {
- if result := <-Srv.Store.Session().Get(sessionId); result.Err != nil {
- c.Err = result.Err
- } else {
- session := result.Data.(*model.Session)
- c.LogAudit("session_id=" + session.Id)
-
- if session.IsOAuth {
- RevokeAccessToken(session.Token)
- } else {
- if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
- c.Err = result.Err
- }
- }
-
- RevokeWebrtcToken(session.Id)
- RemoveAllSessionsForUserId(session.UserId)
- }
-}
-
-// IF YOU UPDATE THIS PLEASE UPDATE BELOW
-func RevokeAllSession(c *Context, userId string) {
- if result := <-Srv.Store.Session().GetSessions(userId); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- sessions := result.Data.([]*model.Session)
-
- for _, session := range sessions {
- c.LogAuditWithUserId(userId, "session_id="+session.Id)
- if session.IsOAuth {
- RevokeAccessToken(session.Token)
- } else {
- if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
- c.Err = result.Err
- return
- }
- }
-
- RevokeWebrtcToken(session.Id)
- }
- }
-
- RemoveAllSessionsForUserId(userId)
-}
-
-// UGH...
-// If you update this please update above
-func RevokeAllSessionsNoContext(userId string) *model.AppError {
- if result := <-Srv.Store.Session().GetSessions(userId); result.Err != nil {
- return result.Err
- } else {
- sessions := result.Data.([]*model.Session)
-
- for _, session := range sessions {
- if session.IsOAuth {
- RevokeAccessToken(session.Token)
- } else {
- if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
- return result.Err
- }
- }
-
- RevokeWebrtcToken(session.Id)
- }
- }
-
- RemoveAllSessionsForUserId(userId)
-
- return nil
-}
-
func getSessions(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["user_id"]
- if !HasPermissionToUser(c, id) {
+ if !app.SessionHasPermissionToUser(c.Session, id) {
+ c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS)
return
}
- if result := <-Srv.Store.Session().GetSessions(id); result.Err != nil {
- c.Err = result.Err
+ if sessions, err := app.GetSessions(id); err != nil {
+ c.Err = err
return
} else {
- sessions := result.Data.([]*model.Session)
for _, session := range sessions {
session.Sanitize()
}
@@ -855,23 +418,26 @@ func Logout(c *Context, w http.ResponseWriter, r *http.Request) {
c.LogAudit("")
c.RemoveSessionCookie(w, r)
if c.Session.Id != "" {
- RevokeSessionById(c, c.Session.Id)
+ if err := app.RevokeSessionById(c.Session.Id); err != nil {
+ c.Err = err
+ return
+ }
}
}
func getMe(c *Context, w http.ResponseWriter, r *http.Request) {
- if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
- c.Err = result.Err
+ if user, err := app.GetUser(c.Session.UserId); err != nil {
+ c.Err = err
c.RemoveSessionCookie(w, r)
l4g.Error(utils.T("api.user.get_me.getting.error"), c.Session.UserId)
return
- } else if HandleEtag(result.Data.(*model.User).Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get Me", w, r) {
+ } else if HandleEtag(user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get Me", w, r) {
return
} else {
- result.Data.(*model.User).Sanitize(map[string]bool{})
- w.Header().Set(model.HEADER_ETAG_SERVER, result.Data.(*model.User).Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress))
- w.Write([]byte(result.Data.(*model.User).ToJson()))
+ user.Sanitize(map[string]bool{})
+ w.Header().Set(model.HEADER_ETAG_SERVER, user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress))
+ w.Write([]byte(user.ToJson()))
return
}
}
@@ -880,68 +446,48 @@ func getInitialLoad(c *Context, w http.ResponseWriter, r *http.Request) {
il := model.InitialLoad{}
- var cchan store.StoreChannel
-
- if sessionCache.Len() == 0 {
- // Below is a special case when intializating a new server
- // Lets check to make sure the server is really empty
-
- cchan = Srv.Store.User().GetTotalUsersCount()
- }
-
if len(c.Session.UserId) != 0 {
- uchan := Srv.Store.User().Get(c.Session.UserId)
- pchan := Srv.Store.Preference().GetAll(c.Session.UserId)
- tchan := Srv.Store.Team().GetTeamsByUserId(c.Session.UserId)
+ var err *model.AppError
- il.TeamMembers = c.Session.TeamMembers
-
- if ru := <-uchan; ru.Err != nil {
- c.Err = ru.Err
+ il.User, err = app.GetUser(c.Session.UserId)
+ if err != nil {
+ c.Err = err
return
- } else {
- il.User = ru.Data.(*model.User)
- il.User.Sanitize(map[string]bool{})
}
+ il.User.Sanitize(map[string]bool{})
- if rp := <-pchan; rp.Err != nil {
- c.Err = rp.Err
+ il.Preferences, err = app.GetPreferencesForUser(c.Session.UserId)
+ if err != nil {
+ c.Err = err
return
- } else {
- il.Preferences = rp.Data.(model.Preferences)
}
- if rt := <-tchan; rt.Err != nil {
- c.Err = rt.Err
+ il.Teams, err = app.GetTeamsForUser(c.Session.UserId)
+ if err != nil {
+ c.Err = err
return
- } else {
- il.Teams = rt.Data.([]*model.Team)
+ }
- for _, team := range il.Teams {
- team.Sanitize()
- }
+ for _, team := range il.Teams {
+ team.Sanitize()
}
+
+ il.TeamMembers = c.Session.TeamMembers
}
- if cchan != nil {
- if cr := <-cchan; cr.Err != nil {
- c.Err = cr.Err
- return
- } else {
- count := cr.Data.(int64)
- if count <= 0 {
- il.NoAccounts = true
- }
- }
+ if app.SessionCacheLength() == 0 {
+ // Below is a special case when intializating a new server
+ // Lets check to make sure the server is really empty
+
+ il.NoAccounts = app.IsFirstUserAccount()
}
il.ClientCfg = utils.ClientCfg
- if HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ if app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
il.LicenseCfg = utils.ClientLicense
} else {
il.LicenseCfg = utils.GetSanitizedClientLicense()
}
- c.Err = nil
w.Write([]byte(il.ToJson()))
}
@@ -950,16 +496,19 @@ func getUser(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["user_id"]
- if result := <-Srv.Store.User().Get(id); result.Err != nil {
- c.Err = result.Err
+ var user *model.User
+ var err *model.AppError
+
+ if user, err = app.GetUser(id); err != nil {
+ c.Err = err
return
- } else if HandleEtag(result.Data.(*model.User).Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get User", w, r) {
+ } else if HandleEtag(user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get User", w, r) {
return
} else {
- user := sanitizeProfile(c, result.Data.(*model.User))
+ sanitizeProfile(c, user)
w.Header().Set(model.HEADER_ETAG_SERVER, user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress))
- w.Write([]byte(result.Data.(*model.User).ToJson()))
+ w.Write([]byte(user.ToJson()))
return
}
}
@@ -968,16 +517,19 @@ func getByUsername(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
username := params["username"]
- if result := <-Srv.Store.User().GetByUsername(username); result.Err != nil {
- c.Err = result.Err
+ var user *model.User
+ var err *model.AppError
+
+ if user, err = app.GetUserByUsername(username); err != nil {
+ c.Err = err
return
- } else if HandleEtag(result.Data.(*model.User).Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get By Username", w, r) {
+ } else if HandleEtag(user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get By Username", w, r) {
return
} else {
- user := sanitizeProfile(c, result.Data.(*model.User))
+ sanitizeProfile(c, user)
w.Header().Set(model.HEADER_ETAG_SERVER, user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress))
- w.Write([]byte(result.Data.(*model.User).ToJson()))
+ w.Write([]byte(user.ToJson()))
return
}
}
@@ -986,16 +538,16 @@ func getByEmail(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
email := params["email"]
- if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil {
- c.Err = result.Err
+ if user, err := app.GetUserByEmail(email); err != nil {
+ c.Err = err
return
- } else if HandleEtag(result.Data.(*model.User).Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get By Email", w, r) {
+ } else if HandleEtag(user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get By Email", w, r) {
return
} else {
- user := sanitizeProfile(c, result.Data.(*model.User))
+ sanitizeProfile(c, user)
w.Header().Set(model.HEADER_ETAG_SERVER, user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress))
- w.Write([]byte(result.Data.(*model.User).ToJson()))
+ w.Write([]byte(user.ToJson()))
return
}
}
@@ -1015,17 +567,15 @@ func getProfiles(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- etag := (<-Srv.Store.User().GetEtagForAllProfiles()).Data.(string)
+ etag := app.GetUsersEtag()
if HandleEtag(etag, "Get Profiles", w, r) {
return
}
- if result := <-Srv.Store.User().GetAllProfiles(offset, limit); result.Err != nil {
- c.Err = result.Err
+ if profiles, err := app.GetUsers(offset, limit); err != nil {
+ c.Err = err
return
} else {
- profiles := result.Data.(map[string]*model.User)
-
for k, p := range profiles {
profiles[k] = sanitizeProfile(c, p)
}
@@ -1040,7 +590,7 @@ func getProfilesInTeam(c *Context, w http.ResponseWriter, r *http.Request) {
teamId := params["team_id"]
if c.Session.GetTeamByTeamId(teamId) == nil {
- if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
return
}
}
@@ -1057,17 +607,15 @@ func getProfilesInTeam(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- etag := (<-Srv.Store.User().GetEtagForProfiles(teamId)).Data.(string)
+ etag := app.GetUsersInTeamEtag(teamId)
if HandleEtag(etag, "Get Profiles In Team", w, r) {
return
}
- if result := <-Srv.Store.User().GetProfiles(teamId, offset, limit); result.Err != nil {
- c.Err = result.Err
+ if profiles, err := app.GetUsersInTeam(teamId, offset, limit); err != nil {
+ c.Err = err
return
} else {
- profiles := result.Data.(map[string]*model.User)
-
for k, p := range profiles {
profiles[k] = sanitizeProfile(c, p)
}
@@ -1081,16 +629,6 @@ func getProfilesInChannel(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
channelId := params["channel_id"]
- if c.Session.GetTeamByTeamId(c.TeamId) == nil {
- if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
- return
- }
- }
-
- if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
- return
- }
-
offset, err := strconv.Atoi(params["offset"])
if err != nil {
c.SetInvalidParam("getProfiles", "offset")
@@ -1103,12 +641,25 @@ func getProfilesInChannel(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.User().GetProfilesInChannel(channelId, offset, limit, false); result.Err != nil {
- c.Err = result.Err
+ if c.Session.GetTeamByTeamId(c.TeamId) == nil {
+ if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
+ return
+ }
+ }
+
+ if !app.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_READ_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
- } else {
- profiles := result.Data.(map[string]*model.User)
+ }
+ var profiles map[string]*model.User
+ var profileErr *model.AppError
+
+ if profiles, err = app.GetUsersInChannel(channelId, offset, limit); profileErr != nil {
+ c.Err = profileErr
+ return
+ } else {
for k, p := range profiles {
profiles[k] = sanitizeProfile(c, p)
}
@@ -1122,12 +673,14 @@ func getProfilesNotInChannel(c *Context, w http.ResponseWriter, r *http.Request)
channelId := params["channel_id"]
if c.Session.GetTeamByTeamId(c.TeamId) == nil {
- if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
return
}
}
- if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
+ if !app.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_READ_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
@@ -1143,12 +696,10 @@ func getProfilesNotInChannel(c *Context, w http.ResponseWriter, r *http.Request)
return
}
- if result := <-Srv.Store.User().GetProfilesNotInChannel(c.TeamId, channelId, offset, limit); result.Err != nil {
- c.Err = result.Err
+ if profiles, err := app.GetUsersNotInChannel(c.TeamId, channelId, offset, limit); err != nil {
+ c.Err = err
return
} else {
- profiles := result.Data.(map[string]*model.User)
-
for k, p := range profiles {
profiles[k] = sanitizeProfile(c, p)
}
@@ -1161,22 +712,15 @@ func getAudits(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["user_id"]
- if !HasPermissionToUser(c, id) {
+ if !app.SessionHasPermissionToUser(c.Session, id) {
+ c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS)
return
}
- userChan := Srv.Store.User().Get(id)
- auditChan := Srv.Store.Audit().Get(id, 20)
-
- if c.Err = (<-userChan).Err; c.Err != nil {
- return
- }
-
- if result := <-auditChan; result.Err != nil {
- c.Err = result.Err
+ if audits, err := app.GetAudits(id, 20); err != nil {
+ c.Err = err
return
} else {
- audits := result.Data.(model.Audits)
etag := audits.Etag()
if HandleEtag(etag, "Get Audits", w, r) {
@@ -1192,128 +736,29 @@ func getAudits(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
-func createProfileImage(username string, userId string) ([]byte, *model.AppError) {
- colors := []color.NRGBA{
- {197, 8, 126, 255},
- {227, 207, 18, 255},
- {28, 181, 105, 255},
- {35, 188, 224, 255},
- {116, 49, 196, 255},
- {197, 8, 126, 255},
- {197, 19, 19, 255},
- {250, 134, 6, 255},
- {227, 207, 18, 255},
- {123, 201, 71, 255},
- {28, 181, 105, 255},
- {35, 188, 224, 255},
- {116, 49, 196, 255},
- {197, 8, 126, 255},
- {197, 19, 19, 255},
- {250, 134, 6, 255},
- {227, 207, 18, 255},
- {123, 201, 71, 255},
- {28, 181, 105, 255},
- {35, 188, 224, 255},
- {116, 49, 196, 255},
- {197, 8, 126, 255},
- {197, 19, 19, 255},
- {250, 134, 6, 255},
- {227, 207, 18, 255},
- {123, 201, 71, 255},
- }
-
- h := fnv.New32a()
- h.Write([]byte(userId))
- seed := h.Sum32()
-
- initial := string(strings.ToUpper(username)[0])
-
- fontBytes, err := ioutil.ReadFile(utils.FindDir("fonts") + utils.Cfg.FileSettings.InitialFont)
- if err != nil {
- return nil, model.NewLocAppError("createProfileImage", "api.user.create_profile_image.default_font.app_error", nil, err.Error())
- }
- font, err := freetype.ParseFont(fontBytes)
- if err != nil {
- return nil, model.NewLocAppError("createProfileImage", "api.user.create_profile_image.default_font.app_error", nil, err.Error())
- }
-
- width := int(utils.Cfg.FileSettings.ProfileWidth)
- height := int(utils.Cfg.FileSettings.ProfileHeight)
- color := colors[int64(seed)%int64(len(colors))]
- dstImg := image.NewRGBA(image.Rect(0, 0, width, height))
- srcImg := image.White
- draw.Draw(dstImg, dstImg.Bounds(), &image.Uniform{color}, image.ZP, draw.Src)
- size := float64((width + height) / 4)
-
- c := freetype.NewContext()
- c.SetFont(font)
- c.SetFontSize(size)
- c.SetClip(dstImg.Bounds())
- c.SetDst(dstImg)
- c.SetSrc(srcImg)
-
- pt := freetype.Pt(width/6, height*2/3)
- _, err = c.DrawString(initial, pt)
- if err != nil {
- return nil, model.NewLocAppError("createProfileImage", "api.user.create_profile_image.initial.app_error", nil, err.Error())
- }
-
- buf := new(bytes.Buffer)
-
- if imgErr := png.Encode(buf, dstImg); imgErr != nil {
- return nil, model.NewLocAppError("createProfileImage", "api.user.create_profile_image.encode.app_error", nil, imgErr.Error())
- } else {
- return buf.Bytes(), nil
- }
-}
-
func getProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["user_id"]
- readFailed := false
var etag string
- if result := <-Srv.Store.User().Get(id); result.Err != nil {
- c.Err = result.Err
+ if user, err := app.GetUser(id); err != nil {
+ c.Err = err
return
} else {
- var img []byte
- etag = strconv.FormatInt(result.Data.(*model.User).LastPictureUpdate, 10)
+ etag = strconv.FormatInt(user.LastPictureUpdate, 10)
if HandleEtag(etag, "Profile Image", w, r) {
return
}
- if len(utils.Cfg.FileSettings.DriverName) == 0 {
- var err *model.AppError
- if img, err = createProfileImage(result.Data.(*model.User).Username, id); err != nil {
- c.Err = err
- return
- }
- } else {
- path := "users/" + id + "/profile.png"
-
- if data, err := ReadFile(path); err != nil {
- readFailed = true
-
- if img, err = createProfileImage(result.Data.(*model.User).Username, id); err != nil {
- c.Err = err
- return
- }
-
- if result.Data.(*model.User).LastPictureUpdate == 0 {
- if err := WriteFile(img, path); err != nil {
- c.Err = err
- return
- }
- }
-
- } else {
- img = data
- }
+ var img []byte
+ img, err = app.GetProfileImage(user)
+ if err != nil {
+ c.Err = err
+ return
}
- if c.Session.UserId == id || readFailed {
+ if c.Session.UserId == id {
w.Header().Set("Cache-Control", "max-age=300, public") // 5 mins
} else {
w.Header().Set("Cache-Control", "max-age=86400, public") // 24 hrs
@@ -1360,64 +805,11 @@ func uploadProfileImage(c *Context, w http.ResponseWriter, r *http.Request) {
imageData := imageArray[0]
- file, err := imageData.Open()
- defer file.Close()
- if err != nil {
- c.Err = model.NewLocAppError("uploadProfileImage", "api.user.upload_profile_user.open.app_error", nil, err.Error())
- return
- }
-
- // Decode image config first to check dimensions before loading the whole thing into memory later on
- config, _, err := image.DecodeConfig(file)
- if err != nil {
- c.Err = model.NewLocAppError("uploadProfileFile", "api.user.upload_profile_user.decode_config.app_error", nil, err.Error())
- return
- } else if config.Width*config.Height > MaxImageSize {
- c.Err = model.NewLocAppError("uploadProfileFile", "api.user.upload_profile_user.too_large.app_error", nil, err.Error())
- return
- }
-
- file.Seek(0, 0)
-
- // Decode image into Image object
- img, _, err := image.Decode(file)
- if err != nil {
- c.Err = model.NewLocAppError("uploadProfileImage", "api.user.upload_profile_user.decode.app_error", nil, err.Error())
- return
- }
-
- // Scale profile image
- img = imaging.Resize(img, utils.Cfg.FileSettings.ProfileWidth, utils.Cfg.FileSettings.ProfileHeight, imaging.Lanczos)
-
- buf := new(bytes.Buffer)
- err = png.Encode(buf, img)
- if err != nil {
- c.Err = model.NewLocAppError("uploadProfileImage", "api.user.upload_profile_user.encode.app_error", nil, err.Error())
- return
- }
-
- path := "users/" + c.Session.UserId + "/profile.png"
-
- if err := WriteFile(buf.Bytes(), path); err != nil {
- c.Err = model.NewLocAppError("uploadProfileImage", "api.user.upload_profile_user.upload_profile.app_error", nil, "")
+ if err := app.SetProfileImage(c.Session.UserId, imageData); err != nil {
+ c.Err = err
return
}
- Srv.Store.User().UpdateLastPictureUpdate(c.Session.UserId)
-
- if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
- l4g.Error(utils.T("api.user.get_me.getting.error"), c.Session.UserId)
- } else {
- user := result.Data.(*model.User)
- user = sanitizeProfile(c, user)
- omitUsers := make(map[string]bool, 1)
- omitUsers[user.Id] = true
- message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", omitUsers)
- message.Add("user", user)
-
- go Publish(message)
- }
-
c.LogAudit("")
// write something as the response since jQuery expects a json response
@@ -1432,50 +824,30 @@ func updateUser(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToUser(c, user.Id) {
+ if !app.SessionHasPermissionToUser(c.Session, user.Id) {
+ c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS)
return
}
- if err := utils.IsPasswordValid(user.Password); user.Password != "" && err != nil {
+ if ruser, err := app.UpdateUser(user, c.GetSiteURL()); err != nil {
c.Err = err
return
- }
-
- if result := <-Srv.Store.User().Update(user, false); result.Err != nil {
- c.Err = result.Err
- return
} else {
c.LogAudit("")
- rusers := result.Data.([2]*model.User)
-
- if rusers[0].Email != rusers[1].Email {
- go sendEmailChangeEmail(c, rusers[1].Email, rusers[0].Email, c.GetSiteURL())
-
- if utils.Cfg.EmailSettings.RequireEmailVerification {
- go SendEmailChangeVerifyEmail(c, rusers[0].Id, rusers[0].Email, c.GetSiteURL())
- }
- }
-
- if rusers[0].Username != rusers[1].Username {
- go sendEmailChangeUsername(c, rusers[1].Username, rusers[0].Username, rusers[0].Email, c.GetSiteURL())
- }
-
- InvalidateCacheForUser(user.Id)
-
- updatedUser := rusers[0]
+ updatedUser := ruser
updatedUser = sanitizeProfile(c, updatedUser)
omitUsers := make(map[string]bool, 1)
omitUsers[user.Id] = true
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", omitUsers)
message.Add("user", updatedUser)
- go Publish(message)
+ go app.Publish(message)
- rusers[0].Password = ""
- rusers[0].AuthData = new(string)
- *rusers[0].AuthData = ""
- w.Write([]byte(rusers[0].ToJson()))
+ ruser.Password = ""
+ ruser.AuthData = new(string)
+ *ruser.AuthData = ""
+ w.Write([]byte(ruser.ToJson()))
}
}
@@ -1497,32 +869,26 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) {
newPassword := props["new_password"]
- if err := utils.IsPasswordValid(newPassword); err != nil {
- c.Err = err
- return
- }
-
if userId != c.Session.UserId {
c.Err = model.NewLocAppError("updatePassword", "api.user.update_password.context.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
return
}
- var result store.StoreResult
+ var user *model.User
+ var err *model.AppError
- if result = <-Srv.Store.User().Get(userId); result.Err != nil {
- c.Err = result.Err
+ if user, err = app.GetUser(userId); err != nil {
+ c.Err = err
return
}
- if result.Data == nil {
+ if user == nil {
c.Err = model.NewLocAppError("updatePassword", "api.user.update_password.valid_account.app_error", nil, "")
c.Err.StatusCode = http.StatusBadRequest
return
}
- user := result.Data.(*model.User)
-
if user.AuthData != nil && *user.AuthData != "" {
c.LogAudit("failed - tried to update user password who was logged in through oauth")
c.Err = model.NewLocAppError("updatePassword", "api.user.update_password.oauth.app_error", nil, "auth_service="+user.AuthService)
@@ -1540,16 +906,14 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if uresult := <-Srv.Store.User().UpdatePassword(c.Session.UserId, model.HashPassword(newPassword)); uresult.Err != nil {
- c.Err = model.NewLocAppError("updatePassword", "api.user.update_password.failed.app_error", nil, uresult.Err.Error())
+ if err := app.UpdatePasswordSendEmail(user, newPassword, c.T("api.user.update_password.menu"), c.GetSiteURL()); err != nil {
+ c.Err = err
return
} else {
c.LogAudit("completed")
- go sendPasswordChangeEmail(c, user.Email, c.GetSiteURL(), c.T("api.user.update_password.menu"))
-
data := make(map[string]string)
- data["user_id"] = uresult.Data.(string)
+ data["user_id"] = c.Session.UserId
w.Write([]byte(model.MapToJson(data)))
}
}
@@ -1570,22 +934,15 @@ func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToContext(c, model.PERMISSION_MANAGE_ROLES) {
+ if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_ROLES) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_ROLES)
return
}
- var user *model.User
- if result := <-Srv.Store.User().Get(userId); result.Err != nil {
- c.Err = result.Err
+ if _, err := app.UpdateUserRoles(userId, newRoles); err != nil {
return
} else {
- user = result.Data.(*model.User)
- }
-
- if _, err := UpdateUserRoles(user, newRoles); err != nil {
- return
- } else {
- c.LogAuditWithUserId(user.Id, "roles="+newRoles)
+ c.LogAuditWithUserId(userId, "roles="+newRoles)
}
rdata := map[string]string{}
@@ -1593,177 +950,34 @@ func updateRoles(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(rdata)))
}
-func UpdateUserRoles(user *model.User, newRoles string) (*model.User, *model.AppError) {
-
- user.Roles = newRoles
- uchan := Srv.Store.User().Update(user, true)
- schan := Srv.Store.Session().UpdateRoles(user.Id, newRoles)
-
- var ruser *model.User
- if result := <-uchan; result.Err != nil {
- return nil, result.Err
- } else {
- ruser = result.Data.([2]*model.User)[0]
- }
-
- if result := <-schan; result.Err != nil {
- // soft error since the user roles were still updated
- l4g.Error(result.Err)
- }
-
- RemoveAllSessionsForUserId(user.Id)
-
- return ruser, nil
-}
-
func updateActive(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJson(r.Body)
- user_id := props["user_id"]
- if len(user_id) != 26 {
+ userId := props["user_id"]
+ if len(userId) != 26 {
c.SetInvalidParam("updateActive", "user_id")
return
}
active := props["active"] == "true"
- var user *model.User
- if result := <-Srv.Store.User().Get(user_id); result.Err != nil {
- c.Err = result.Err
- return
- } else {
- user = result.Data.(*model.User)
- }
-
// true when you're trying to de-activate yourself
- isSelfDeactive := !active && user_id == c.Session.UserId
+ isSelfDeactive := !active && userId == c.Session.UserId
- if !isSelfDeactive && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
- c.Err = model.NewLocAppError("updateActive", "api.user.update_active.permissions.app_error", nil, "userId="+user_id)
+ if !isSelfDeactive && !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
+ c.Err = model.NewLocAppError("updateActive", "api.user.update_active.permissions.app_error", nil, "userId="+userId)
c.Err.StatusCode = http.StatusForbidden
return
}
- if user.IsLDAPUser() {
- c.Err = model.NewLocAppError("updateActive", "api.user.update_active.no_deactivate_ldap.app_error", nil, "userId="+user_id)
- c.Err.StatusCode = http.StatusBadRequest
- return
- }
-
- if ruser, err := UpdateActive(user, active); err != nil {
+ if ruser, err := app.UpdateActiveNoLdap(userId, active); err != nil {
c.Err = err
} else {
- if !active {
- SetStatusOffline(ruser.Id, false)
- }
-
c.LogAuditWithUserId(ruser.Id, fmt.Sprintf("active=%v", active))
w.Write([]byte(ruser.ToJson()))
}
}
-func UpdateActive(user *model.User, active bool) (*model.User, *model.AppError) {
- if active {
- user.DeleteAt = 0
- } else {
- user.DeleteAt = model.GetMillis()
- }
-
- if result := <-Srv.Store.User().Update(user, true); result.Err != nil {
- return nil, result.Err
- } else {
- if user.DeleteAt > 0 {
- RevokeAllSessionsNoContext(user.Id)
- }
-
- if extra := <-Srv.Store.Channel().ExtraUpdateByUser(user.Id, model.GetMillis()); extra.Err != nil {
- return nil, extra.Err
- }
-
- ruser := result.Data.([2]*model.User)[0]
- options := utils.Cfg.GetSanitizeOptions()
- options["passwordupdate"] = false
- ruser.Sanitize(options)
- return ruser, nil
- }
-}
-
-func PermanentDeleteUser(user *model.User) *model.AppError {
- l4g.Warn(utils.T("api.user.permanent_delete_user.attempting.warn"), user.Email, user.Id)
- if user.IsInRole(model.ROLE_SYSTEM_ADMIN.Id) {
- l4g.Warn(utils.T("api.user.permanent_delete_user.system_admin.warn"), user.Email)
- }
-
- if _, err := UpdateActive(user, false); err != nil {
- return err
- }
-
- if result := <-Srv.Store.Session().PermanentDeleteSessionsByUser(user.Id); result.Err != nil {
- return result.Err
- }
-
- if result := <-Srv.Store.OAuth().PermanentDeleteAuthDataByUser(user.Id); result.Err != nil {
- return result.Err
- }
-
- if result := <-Srv.Store.Webhook().PermanentDeleteIncomingByUser(user.Id); result.Err != nil {
- return result.Err
- }
-
- if result := <-Srv.Store.Webhook().PermanentDeleteOutgoingByUser(user.Id); result.Err != nil {
- return result.Err
- }
-
- if result := <-Srv.Store.Command().PermanentDeleteByUser(user.Id); result.Err != nil {
- return result.Err
- }
-
- if result := <-Srv.Store.Preference().PermanentDeleteByUser(user.Id); result.Err != nil {
- return result.Err
- }
-
- if result := <-Srv.Store.Channel().PermanentDeleteMembersByUser(user.Id); result.Err != nil {
- return result.Err
- }
-
- if result := <-Srv.Store.Post().PermanentDeleteByUser(user.Id); result.Err != nil {
- return result.Err
- }
-
- if result := <-Srv.Store.User().PermanentDelete(user.Id); result.Err != nil {
- return result.Err
- }
-
- if result := <-Srv.Store.Audit().PermanentDeleteByUser(user.Id); result.Err != nil {
- return result.Err
- }
-
- if result := <-Srv.Store.Team().RemoveAllMembersByUser(user.Id); result.Err != nil {
- return result.Err
- }
-
- if result := <-Srv.Store.PasswordRecovery().Delete(user.Id); result.Err != nil {
- return result.Err
- }
-
- l4g.Warn(utils.T("api.user.permanent_delete_user.deleted.warn"), user.Email, user.Id)
-
- return nil
-}
-
-func PermanentDeleteAllUsers() *model.AppError {
- if result := <-Srv.Store.User().GetAll(); result.Err != nil {
- return result.Err
- } else {
- users := result.Data.([]*model.User)
- for _, user := range users {
- PermanentDeleteUser(user)
- }
- }
-
- return nil
-}
-
func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJson(r.Body)
@@ -1773,45 +987,13 @@ func sendPasswordReset(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- var user *model.User
- if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil {
- w.Write([]byte(model.MapToJson(props)))
- return
- } else {
- user = result.Data.(*model.User)
- }
-
- if user.AuthData != nil && len(*user.AuthData) != 0 {
- c.Err = model.NewLocAppError("sendPasswordReset", "api.user.send_password_reset.sso.app_error", nil, "userId="+user.Id)
- return
- }
-
- recovery := &model.PasswordRecovery{}
- recovery.UserId = user.Id
-
- if result := <-Srv.Store.PasswordRecovery().SaveOrUpdate(recovery); result.Err != nil {
- c.Err = result.Err
- return
- }
-
- link := fmt.Sprintf("%s/reset_password_complete?code=%s", c.GetSiteURL(), url.QueryEscape(recovery.Code))
-
- subject := c.T("api.templates.reset_subject")
-
- bodyPage := utils.NewHTMLTemplate("reset_body", c.Locale)
- bodyPage.Props["SiteURL"] = c.GetSiteURL()
- bodyPage.Props["Title"] = c.T("api.templates.reset_body.title")
- bodyPage.Html["Info"] = template.HTML(c.T("api.templates.reset_body.info"))
- bodyPage.Props["ResetUrl"] = link
- bodyPage.Props["Button"] = c.T("api.templates.reset_body.button")
-
- if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil {
- c.Err = model.NewLocAppError("sendPasswordReset", "api.user.send_password_reset.send.app_error", nil, "err="+err.Message)
+ if sent, err := app.SendPasswordReset(email, c.GetSiteURL()); err != nil {
+ c.Err = err
return
+ } else if sent {
+ c.LogAudit("sent=" + email)
}
- c.LogAuditWithUserId(user.Id, "sent="+email)
-
w.Write([]byte(model.MapToJson(props)))
}
@@ -1825,172 +1007,33 @@ func resetPassword(c *Context, w http.ResponseWriter, r *http.Request) {
}
newPassword := props["new_password"]
- if err := utils.IsPasswordValid(newPassword); err != nil {
- c.Err = err
- return
- }
- c.LogAudit("attempt")
-
- userId := ""
+ c.LogAudit("attempt - code=" + code)
- if result := <-Srv.Store.PasswordRecovery().GetByCode(code); result.Err != nil {
- c.LogAuditWithUserId(userId, "fail - bad code")
- c.Err = model.NewLocAppError("resetPassword", "api.user.reset_password.invalid_link.app_error", nil, result.Err.Error())
- return
- } else {
- recovery := result.Data.(*model.PasswordRecovery)
-
- if model.GetMillis()-recovery.CreateAt < model.PASSWORD_RECOVER_EXPIRY_TIME {
- userId = recovery.UserId
- } else {
- c.LogAuditWithUserId(userId, "fail - link expired")
- c.Err = model.NewLocAppError("resetPassword", "api.user.reset_password.link_expired.app_error", nil, "")
- return
- }
-
- go func() {
- if result := <-Srv.Store.PasswordRecovery().Delete(userId); result.Err != nil {
- l4g.Error("%v", result.Err)
- }
- }()
- }
-
- if err := ResetPassword(c, userId, newPassword); err != nil {
+ if err := app.ResetPasswordFromCode(code, newPassword, c.GetSiteURL()); err != nil {
+ c.LogAudit("fail - code=" + code)
c.Err = err
return
}
- c.LogAuditWithUserId(userId, "success")
+ c.LogAudit("success - code=" + code)
rdata := map[string]string{}
rdata["status"] = "ok"
w.Write([]byte(model.MapToJson(rdata)))
}
-func ResetPassword(c *Context, userId, newPassword string) *model.AppError {
- var user *model.User
- if result := <-Srv.Store.User().Get(userId); result.Err != nil {
- return result.Err
- } else {
- user = result.Data.(*model.User)
- }
-
- if user.AuthData != nil && len(*user.AuthData) != 0 && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
- return model.NewLocAppError("ResetPassword", "api.user.reset_password.sso.app_error", nil, "userId="+user.Id)
-
- }
-
- if result := <-Srv.Store.User().UpdatePassword(userId, model.HashPassword(newPassword)); result.Err != nil {
- return result.Err
- }
-
- go sendPasswordChangeEmail(c, user.Email, c.GetSiteURL(), c.T("api.user.reset_password.method"))
-
- return nil
-}
-
-func sendPasswordChangeEmail(c *Context, email, siteURL, method string) {
- subject := c.T("api.templates.password_change_subject",
- map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "SiteName": utils.Cfg.TeamSettings.SiteName})
-
- bodyPage := utils.NewHTMLTemplate("password_change_body", c.Locale)
- bodyPage.Props["SiteURL"] = siteURL
- bodyPage.Props["Title"] = c.T("api.templates.password_change_body.title")
- bodyPage.Html["Info"] = template.HTML(c.T("api.templates.password_change_body.info",
- map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "TeamURL": siteURL, "Method": method}))
-
- if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil {
- l4g.Error(utils.T("api.user.send_password_change_email_and_forget.error"), err)
- }
-}
-
-func sendMfaChangeEmail(c *Context, email string, siteURL string, activated bool) {
- subject := c.T("api.templates.mfa_change_subject",
- map[string]interface{}{"SiteName": utils.Cfg.TeamSettings.SiteName})
-
- bodyPage := utils.NewHTMLTemplate("mfa_change_body", c.Locale)
- bodyPage.Props["SiteURL"] = siteURL
-
- bodyText := ""
- if activated {
- bodyText = "api.templates.mfa_activated_body.info"
- bodyPage.Props["Title"] = c.T("api.templates.mfa_activated_body.title")
- } else {
- bodyText = "api.templates.mfa_deactivated_body.info"
- bodyPage.Props["Title"] = c.T("api.templates.mfa_deactivated_body.title")
- }
-
- bodyPage.Html["Info"] = template.HTML(c.T(bodyText,
- map[string]interface{}{"SiteURL": siteURL}))
-
- if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil {
- l4g.Error(utils.T("api.user.send_mfa_change_email.error"), err)
- }
-}
-
-func sendEmailChangeEmail(c *Context, oldEmail, newEmail, siteURL string) {
- subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, c.T("api.templates.email_change_subject",
- map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName}))
-
- bodyPage := utils.NewHTMLTemplate("email_change_body", c.Locale)
- bodyPage.Props["SiteURL"] = siteURL
- bodyPage.Props["Title"] = c.T("api.templates.email_change_body.title")
- bodyPage.Html["Info"] = template.HTML(c.T("api.templates.email_change_body.info",
- map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "NewEmail": newEmail}))
-
- if err := utils.SendMail(oldEmail, subject, bodyPage.Render()); err != nil {
- l4g.Error(utils.T("api.user.send_email_change_email_and_forget.error"), err)
- }
-}
-
-func SendEmailChangeVerifyEmail(c *Context, userId, newUserEmail, siteURL string) {
- link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashPassword(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(newUserEmail))
-
- subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, c.T("api.templates.email_change_verify_subject",
- map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName}))
-
- bodyPage := utils.NewHTMLTemplate("email_change_verify_body", c.Locale)
- bodyPage.Props["SiteURL"] = siteURL
- bodyPage.Props["Title"] = c.T("api.templates.email_change_verify_body.title")
- bodyPage.Props["Info"] = c.T("api.templates.email_change_verify_body.info",
- map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName})
- bodyPage.Props["VerifyUrl"] = link
- bodyPage.Props["VerifyButton"] = c.T("api.templates.email_change_verify_body.button")
-
- if err := utils.SendMail(newUserEmail, subject, bodyPage.Render()); err != nil {
- l4g.Error(utils.T("api.user.send_email_change_verify_email_and_forget.error"), err)
- }
-}
-
-func sendEmailChangeUsername(c *Context, oldUsername, newUsername, email, siteURL string) {
- subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, c.T("api.templates.username_change_subject",
- map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName}))
-
- bodyPage := utils.NewHTMLTemplate("email_change_body", c.Locale)
- bodyPage.Props["SiteURL"] = siteURL
- bodyPage.Props["Title"] = c.T("api.templates.username_change_body.title")
- bodyPage.Html["Info"] = template.HTML(c.T("api.templates.username_change_body.info",
- map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "NewUsername": newUsername}))
-
- if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil {
- l4g.Error(utils.T("api.user.send_email_change_username_and_forget.error"), err)
- }
-
-}
-
func updateUserNotify(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJson(r.Body)
- user_id := props["user_id"]
- if len(user_id) != 26 {
+ userId := props["user_id"]
+ if len(userId) != 26 {
c.SetInvalidParam("updateUserNotify", "user_id")
return
}
- uchan := Srv.Store.User().Get(user_id)
-
- if !HasPermissionToUser(c, user_id) {
+ if !app.SessionHasPermissionToUser(c.Session, userId) {
+ c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS)
return
}
@@ -2020,45 +1063,18 @@ func updateUserNotify(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- var user *model.User
- if result := <-uchan; result.Err != nil {
- c.Err = result.Err
- return
- } else {
- user = result.Data.(*model.User)
- }
-
- user.NotifyProps = props
-
- if result := <-Srv.Store.User().Update(user, false); result.Err != nil {
- c.Err = result.Err
+ ruser, err := app.UpdateUserNotifyProps(userId, props, c.GetSiteURL())
+ if err != nil {
+ c.Err = err
return
- } else {
- c.LogAuditWithUserId(user.Id, "")
- InvalidateCacheForUser(user.Id)
-
- ruser := result.Data.([2]*model.User)[0]
- options := utils.Cfg.GetSanitizeOptions()
- options["passwordupdate"] = false
- ruser.Sanitize(options)
- w.Write([]byte(ruser.ToJson()))
- }
-}
-
-// Check if the username is already used by another user. Return false if the username is invalid.
-func IsUsernameTaken(name string) bool {
-
- if !model.IsValidUsername(name) {
- return false
}
- if result := <-Srv.Store.User().GetByUsername(name); result.Err != nil {
- return false
- } else {
- return true
- }
+ c.LogAuditWithUserId(ruser.Id, "")
- return false
+ options := utils.Cfg.GetSanitizeOptions()
+ options["passwordupdate"] = false
+ ruser.Sanitize(options)
+ w.Write([]byte(ruser.ToJson()))
}
func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -2087,12 +1103,11 @@ func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
c.LogAudit("attempt")
var user *model.User
- if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil {
+ var err *model.AppError
+ if user, err = app.GetUserByEmail(email); err != nil {
c.LogAudit("fail - couldn't get user")
- c.Err = result.Err
+ c.Err = err
return
- } else {
- user = result.Data.(*model.User)
}
if err := checkPasswordAndAllCriteria(user, password, mfaToken); err != nil {
@@ -2140,12 +1155,11 @@ func oauthToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
c.LogAudit("attempt")
var user *model.User
- if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil {
+ var err *model.AppError
+ if user, err = app.GetUserByEmail(email); err != nil {
c.LogAudit("fail - couldn't get user")
- c.Err = result.Err
+ c.Err = err
return
- } else {
- user = result.Data.(*model.User)
}
if user.Id != c.Session.UserId {
@@ -2155,15 +1169,24 @@ func oauthToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.User().UpdatePassword(c.Session.UserId, model.HashPassword(password)); result.Err != nil {
+ if err := app.UpdatePassword(user, password); err != nil {
c.LogAudit("fail - database issue")
- c.Err = result.Err
+ c.Err = err
return
}
- go sendSignInChangeEmail(c, user.Email, c.GetSiteURL(), c.T("api.templates.signin_change_email.body.method_email"))
+ go func() {
+ if err := app.SendSignInChangeEmail(user.Email, c.T("api.templates.signin_change_email.body.method_email"), user.Locale, c.GetSiteURL()); err != nil {
+ l4g.Error(err.Error())
+ }
+ }()
+
+ if err := app.RevokeAllSessions(c.Session.UserId); err != nil {
+ c.Err = err
+ return
+ }
+ c.LogAuditWithUserId(c.Session.UserId, "Revoked all sessions for user")
- RevokeAllSession(c, c.Session.UserId)
c.RemoveSessionCookie(w, r)
if c.Err != nil {
return
@@ -2208,12 +1231,11 @@ func emailToLdap(c *Context, w http.ResponseWriter, r *http.Request) {
c.LogAudit("attempt")
var user *model.User
- if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil {
+ var err *model.AppError
+ if user, err = app.GetUserByEmail(email); err != nil {
c.LogAudit("fail - couldn't get user")
- c.Err = result.Err
+ c.Err = err
return
- } else {
- user = result.Data.(*model.User)
}
if err := checkPasswordAndAllCriteria(user, emailPassword, token); err != nil {
@@ -2222,7 +1244,12 @@ func emailToLdap(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- RevokeAllSession(c, user.Id)
+ if err := app.RevokeAllSessions(user.Id); err != nil {
+ c.Err = err
+ return
+ }
+ c.LogAuditWithUserId(user.Id, "Revoked all sessions for user")
+
c.RemoveSessionCookie(w, r)
if c.Err != nil {
return
@@ -2241,7 +1268,11 @@ func emailToLdap(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- go sendSignInChangeEmail(c, user.Email, c.GetSiteURL(), "AD/LDAP")
+ go func() {
+ if err := app.SendSignInChangeEmail(user.Email, "AD/LDAP", user.Locale, c.GetSiteURL()); err != nil {
+ l4g.Error(err.Error())
+ }
+ }()
m := map[string]string{}
m["follow_link"] = "/login?extra=signin_change"
@@ -2276,12 +1307,11 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
c.LogAudit("attempt")
var user *model.User
- if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil {
+ var err *model.AppError
+ if user, err = app.GetUserByEmail(email); err != nil {
c.LogAudit("fail - couldn't get user")
- c.Err = result.Err
+ c.Err = err
return
- } else {
- user = result.Data.(*model.User)
}
if user.AuthService != model.USER_AUTH_SERVICE_LDAP {
@@ -2308,19 +1338,28 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.User().UpdatePassword(user.Id, model.HashPassword(emailPassword)); result.Err != nil {
+ if err := app.UpdatePassword(user, emailPassword); err != nil {
c.LogAudit("fail - database issue")
- c.Err = result.Err
+ c.Err = err
return
}
- RevokeAllSession(c, user.Id)
+ if err := app.RevokeAllSessions(user.Id); err != nil {
+ c.Err = err
+ return
+ }
+ c.LogAuditWithUserId(user.Id, "Revoked all sessions for user")
+
c.RemoveSessionCookie(w, r)
if c.Err != nil {
return
}
- go sendSignInChangeEmail(c, user.Email, c.GetSiteURL(), c.T("api.templates.signin_change_email.body.method_email"))
+ go func() {
+ if err := app.SendSignInChangeEmail(user.Email, c.T("api.templates.signin_change_email.body.method_email"), user.Locale, c.GetSiteURL()); err != nil {
+ l4g.Error(err.Error())
+ }
+ }()
m := map[string]string{}
m["follow_link"] = "/login?extra=signin_change"
@@ -2329,21 +1368,6 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(m)))
}
-func sendSignInChangeEmail(c *Context, email, siteURL, method string) {
- subject := c.T("api.templates.singin_change_email.subject",
- map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
-
- bodyPage := utils.NewHTMLTemplate("signin_change_body", c.Locale)
- bodyPage.Props["SiteURL"] = siteURL
- bodyPage.Props["Title"] = c.T("api.templates.signin_change_email.body.title")
- bodyPage.Html["Info"] = template.HTML(c.T("api.templates.singin_change_email.body.info",
- map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], "Method": method}))
-
- if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil {
- l4g.Error(utils.T("api.user.send_sign_in_change_email_and_forget.error"), err)
- }
-}
-
func verifyEmail(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJson(r.Body)
@@ -2360,7 +1384,7 @@ func verifyEmail(c *Context, w http.ResponseWriter, r *http.Request) {
}
if model.ComparePassword(hashedId, userId+utils.Cfg.EmailSettings.InviteSalt) {
- if c.Err = (<-Srv.Store.User().VerifyEmail(userId)).Err; c.Err != nil {
+ if c.Err = app.VerifyUserEmail(userId); c.Err != nil {
return
} else {
c.LogAudit("Email Verified")
@@ -2381,27 +1405,24 @@ func resendVerification(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if user, error := getUserForLogin(email, false); error != nil {
+ if user, error := app.GetUserForLogin(email, false); error != nil {
c.Err = error
return
} else {
- if _, err := GetStatus(user.Id); err != nil {
- go SendVerifyEmail(c, user.Id, user.Email, c.GetSiteURL())
+ if _, err := app.GetStatus(user.Id); err != nil {
+ go app.SendVerifyEmail(user.Id, user.Email, user.Locale, c.GetSiteURL())
} else {
- go SendEmailChangeVerifyEmail(c, user.Id, user.Email, c.GetSiteURL())
+ go app.SendEmailChangeVerifyEmail(user.Id, user.Email, user.Locale, c.GetSiteURL())
}
}
}
func generateMfaSecret(c *Context, w http.ResponseWriter, r *http.Request) {
- uchan := Srv.Store.User().Get(c.Session.UserId)
-
var user *model.User
- if result := <-uchan; result.Err != nil {
- c.Err = result.Err
+ var err *model.AppError
+ if user, err = app.GetUser(c.Session.UserId); err != nil {
+ c.Err = err
return
- } else {
- user = result.Data.(*model.User)
}
mfaInterface := einterfaces.GetMfaInterface()
@@ -2448,13 +1469,13 @@ func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) {
c.LogAudit("attempt")
if activate {
- if err := ActivateMfa(c.Session.UserId, token); err != nil {
+ if err := app.ActivateMfa(c.Session.UserId, token); err != nil {
c.Err = err
return
}
c.LogAudit("success - activated")
} else {
- if err := DeactivateMfa(c.Session.UserId); err != nil {
+ if err := app.DeactivateMfa(c.Session.UserId); err != nil {
c.Err = err
return
}
@@ -2463,13 +1484,15 @@ func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) {
go func() {
var user *model.User
- if result := <-Srv.Store.User().Get(c.Session.UserId); result.Err != nil {
- l4g.Warn(result.Err)
- } else {
- user = result.Data.(*model.User)
+ var err *model.AppError
+ if user, err = app.GetUser(c.Session.UserId); err != nil {
+ l4g.Warn(err.Error())
+ return
}
- sendMfaChangeEmail(c, user.Email, c.GetSiteURL(), activate)
+ if err := app.SendMfaChangeEmail(user.Email, activate, user.Locale, c.GetSiteURL()); err != nil {
+ l4g.Error(err.Error())
+ }
}()
rdata := map[string]string{}
@@ -2477,47 +1500,6 @@ func updateMfa(c *Context, w http.ResponseWriter, r *http.Request) {
w.Write([]byte(model.MapToJson(rdata)))
}
-func ActivateMfa(userId, token string) *model.AppError {
- mfaInterface := einterfaces.GetMfaInterface()
- if mfaInterface == nil {
- err := model.NewLocAppError("ActivateMfa", "api.user.update_mfa.not_available.app_error", nil, "")
- err.StatusCode = http.StatusNotImplemented
- return err
- }
-
- var user *model.User
- if result := <-Srv.Store.User().Get(userId); result.Err != nil {
- return result.Err
- } else {
- user = result.Data.(*model.User)
- }
-
- if len(user.AuthService) > 0 && user.AuthService != model.USER_AUTH_SERVICE_LDAP {
- return model.NewLocAppError("ActivateMfa", "api.user.activate_mfa.email_and_ldap_only.app_error", nil, "")
- }
-
- if err := mfaInterface.Activate(user, token); err != nil {
- return err
- }
-
- return nil
-}
-
-func DeactivateMfa(userId string) *model.AppError {
- mfaInterface := einterfaces.GetMfaInterface()
- if mfaInterface == nil {
- err := model.NewLocAppError("DeactivateMfa", "api.user.update_mfa.not_available.app_error", nil, "")
- err.StatusCode = http.StatusNotImplemented
- return err
- }
-
- if err := mfaInterface.Deactivate(userId); err != nil {
- return err
- }
-
- return nil
-}
-
func checkMfa(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication {
rdata := map[string]string{}
@@ -2534,20 +1516,11 @@ func checkMfa(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- // we don't need to worry about contacting the ldap server to get this user because
- // only users already in the system could have MFA enabled
- uchan := Srv.Store.User().GetForLogin(
- loginId,
- *utils.Cfg.EmailSettings.EnableSignInWithUsername,
- *utils.Cfg.EmailSettings.EnableSignInWithEmail,
- *utils.Cfg.LdapSettings.Enable,
- )
-
rdata := map[string]string{}
- if result := <-uchan; result.Err != nil {
+ if user, err := app.GetUserForLogin(loginId, false); err != nil {
rdata["mfa_required"] = "false"
} else {
- rdata["mfa_required"] = strconv.FormatBool(result.Data.(*model.User).MfaActive)
+ rdata["mfa_required"] = strconv.FormatBool(user.MfaActive)
}
w.Write([]byte(model.MapToJson(rdata)))
}
@@ -2637,12 +1610,20 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
case model.OAUTH_ACTION_SIGNUP:
teamId := relayProps["team_id"]
if len(teamId) > 0 {
- go addDirectChannels(teamId, user)
+ go app.AddDirectChannels(teamId, user)
}
break
case model.OAUTH_ACTION_EMAIL_TO_SSO:
- RevokeAllSession(c, user.Id)
- go sendSignInChangeEmail(c, user.Email, c.GetSiteURL(), strings.Title(model.USER_AUTH_SERVICE_SAML)+" SSO")
+ if err := app.RevokeAllSessions(user.Id); err != nil {
+ c.Err = err
+ return
+ }
+ c.LogAuditWithUserId(user.Id, "Revoked all sessions for user")
+ go func() {
+ if err := app.SendSignInChangeEmail(user.Email, strings.Title(model.USER_AUTH_SERVICE_SAML)+" SSO", user.Locale, c.GetSiteURL()); err != nil {
+ l4g.Error(err.Error())
+ }
+ }()
break
}
doLogin(c, w, r, user, "")
@@ -2676,7 +1657,7 @@ func userTyping(req *model.WebSocketRequest) (map[string]interface{}, *model.App
event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_TYPING, "", channelId, "", omitUsers)
event.Add("parent_id", parentId)
event.Add("user_id", req.Session.UserId)
- go Publish(event)
+ go app.Publish(event)
return nil, nil
}
@@ -2684,12 +1665,11 @@ func userTyping(req *model.WebSocketRequest) (map[string]interface{}, *model.App
func sanitizeProfile(c *Context, user *model.User) *model.User {
options := utils.Cfg.GetSanitizeOptions()
- if HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ if app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
options["email"] = true
options["fullname"] = true
options["authservice"] = true
}
- c.Err = nil
user.SanitizeProfile(options)
@@ -2708,18 +1688,20 @@ func searchUsers(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if props.InChannelId != "" && !HasPermissionToChannelContext(c, props.InChannelId, model.PERMISSION_READ_CHANNEL) {
+ if props.InChannelId != "" && !app.SessionHasPermissionToChannel(c.Session, props.InChannelId, model.PERMISSION_READ_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
- if props.NotInChannelId != "" && !HasPermissionToChannelContext(c, props.NotInChannelId, model.PERMISSION_READ_CHANNEL) {
+ if props.NotInChannelId != "" && !app.SessionHasPermissionToChannel(c.Session, props.NotInChannelId, model.PERMISSION_READ_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
searchOptions := map[string]bool{}
searchOptions[store.USER_SEARCH_OPTION_ALLOW_INACTIVE] = props.AllowInactive
- if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
hideFullName := !utils.Cfg.PrivacySettings.ShowFullName
hideEmail := !utils.Cfg.PrivacySettings.ShowEmailAddress
@@ -2730,31 +1712,28 @@ func searchUsers(c *Context, w http.ResponseWriter, r *http.Request) {
} else if hideEmail {
searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true
}
-
- c.Err = nil
}
- var uchan store.StoreChannel
+ var profiles []*model.User
+ var err *model.AppError
if props.InChannelId != "" {
- uchan = Srv.Store.User().SearchInChannel(props.InChannelId, props.Term, searchOptions)
+ profiles, err = app.SearchUsersInChannel(props.InChannelId, props.Term, searchOptions)
} else if props.NotInChannelId != "" {
- uchan = Srv.Store.User().SearchNotInChannel(props.TeamId, props.NotInChannelId, props.Term, searchOptions)
+ profiles, err = app.SearchUsersNotInChannel(props.TeamId, props.NotInChannelId, props.Term, searchOptions)
} else {
- uchan = Srv.Store.User().Search(props.TeamId, props.Term, searchOptions)
+ profiles, err = app.SearchUsersInTeam(props.TeamId, props.Term, searchOptions)
}
- if result := <-uchan; result.Err != nil {
- c.Err = result.Err
+ if err != nil {
+ c.Err = err
return
- } else {
- profiles := result.Data.([]*model.User)
-
- for _, p := range profiles {
- sanitizeProfile(c, p)
- }
+ }
- w.Write([]byte(model.UserListToJson(profiles)))
+ for _, p := range profiles {
+ sanitizeProfile(c, p)
}
+
+ w.Write([]byte(model.UserListToJson(profiles)))
}
func getProfilesByIds(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -2765,12 +1744,10 @@ func getProfilesByIds(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.User().GetProfileByIds(userIds, true); result.Err != nil {
- c.Err = result.Err
+ if profiles, err := app.GetUsersByIds(userIds); err != nil {
+ c.Err = err
return
} else {
- profiles := result.Data.(map[string]*model.User)
-
for _, p := range profiles {
sanitizeProfile(c, p)
}
@@ -2787,54 +1764,37 @@ func autocompleteUsersInChannel(c *Context, w http.ResponseWriter, r *http.Reque
term := r.URL.Query().Get("term")
if c.Session.GetTeamByTeamId(teamId) == nil {
- if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
return
}
}
- if !HasPermissionToChannelContext(c, channelId, model.PERMISSION_READ_CHANNEL) {
+ if !app.SessionHasPermissionToChannel(c.Session, channelId, model.PERMISSION_READ_CHANNEL) {
+ c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
searchOptions := map[string]bool{}
hideFullName := !utils.Cfg.PrivacySettings.ShowFullName
- if hideFullName && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ if hideFullName && !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY_NO_FULL_NAME] = true
- c.Err = nil
} else {
searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true
}
- uchan := Srv.Store.User().SearchInChannel(channelId, term, searchOptions)
- nuchan := Srv.Store.User().SearchNotInChannel(teamId, channelId, term, searchOptions)
-
- autocomplete := &model.UserAutocompleteInChannel{}
-
- if result := <-uchan; result.Err != nil {
- c.Err = result.Err
+ autocomplete, err := app.AutocompleteUsersInChannel(teamId, channelId, term, searchOptions)
+ if err != nil {
+ c.Err = err
return
- } else {
- profiles := result.Data.([]*model.User)
-
- for _, p := range profiles {
- sanitizeProfile(c, p)
- }
-
- autocomplete.InChannel = profiles
}
- if result := <-nuchan; result.Err != nil {
- c.Err = result.Err
- return
- } else {
- profiles := result.Data.([]*model.User)
-
- for _, p := range profiles {
- sanitizeProfile(c, p)
- }
+ for _, p := range autocomplete.InChannel {
+ sanitizeProfile(c, p)
+ }
- autocomplete.OutOfChannel = profiles
+ for _, p := range autocomplete.OutOfChannel {
+ sanitizeProfile(c, p)
}
w.Write([]byte(autocomplete.ToJson()))
@@ -2847,7 +1807,7 @@ func autocompleteUsersInTeam(c *Context, w http.ResponseWriter, r *http.Request)
term := r.URL.Query().Get("term")
if c.Session.GetTeamByTeamId(teamId) == nil {
- if !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
return
}
}
@@ -2855,28 +1815,20 @@ func autocompleteUsersInTeam(c *Context, w http.ResponseWriter, r *http.Request)
searchOptions := map[string]bool{}
hideFullName := !utils.Cfg.PrivacySettings.ShowFullName
- if hideFullName && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ if hideFullName && !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY_NO_FULL_NAME] = true
- c.Err = nil
} else {
searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true
}
- uchan := Srv.Store.User().Search(teamId, term, searchOptions)
-
- autocomplete := &model.UserAutocompleteInTeam{}
-
- if result := <-uchan; result.Err != nil {
- c.Err = result.Err
+ autocomplete, err := app.AutocompleteUsersInTeam(teamId, term, searchOptions)
+ if err != nil {
+ c.Err = err
return
- } else {
- profiles := result.Data.([]*model.User)
-
- for _, p := range profiles {
- sanitizeProfile(c, p)
- }
+ }
- autocomplete.InTeam = profiles
+ for _, p := range autocomplete.InTeam {
+ sanitizeProfile(c, p)
}
w.Write([]byte(autocomplete.ToJson()))
@@ -2888,26 +1840,22 @@ func autocompleteUsers(c *Context, w http.ResponseWriter, r *http.Request) {
searchOptions := map[string]bool{}
hideFullName := !utils.Cfg.PrivacySettings.ShowFullName
- if hideFullName && !HasPermissionToContext(c, model.PERMISSION_MANAGE_SYSTEM) {
+ if hideFullName && !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY_NO_FULL_NAME] = true
- c.Err = nil
} else {
searchOptions[store.USER_SEARCH_OPTION_NAMES_ONLY] = true
}
- uchan := Srv.Store.User().Search("", term, searchOptions)
-
var profiles []*model.User
+ var err *model.AppError
- if result := <-uchan; result.Err != nil {
- c.Err = result.Err
+ if profiles, err = app.SearchUsersInTeam("", term, searchOptions); err != nil {
+ c.Err = err
return
- } else {
- profiles = result.Data.([]*model.User)
+ }
- for _, p := range profiles {
- sanitizeProfile(c, p)
- }
+ for _, p := range profiles {
+ sanitizeProfile(c, p)
}
w.Write([]byte(model.UserListToJson(profiles)))
diff --git a/api/user_test.go b/api/user_test.go
index c846beee3..5a398a716 100644
--- a/api/user_test.go
+++ b/api/user_test.go
@@ -16,6 +16,7 @@ import (
"testing"
"time"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
@@ -27,9 +28,6 @@ func TestCreateUser(t *testing.T) {
th := Setup()
Client := th.CreateClient()
- team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
- rteam, _ := Client.CreateTeam(&team)
-
user := model.User{Email: strings.ToLower("success+"+model.NewId()) + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "hello1", Username: "n" + model.NewId()}
ruser, err := Client.CreateUser(&user, "")
@@ -37,6 +35,11 @@ func TestCreateUser(t *testing.T) {
t.Fatal(err)
}
+ Client.Login(user.Email, user.Password)
+
+ team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ rteam, _ := Client.CreateTeam(&team)
+
LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team))
if ruser.Data.(*model.User).Nickname != user.Nickname {
@@ -80,54 +83,9 @@ func TestCreateUser(t *testing.T) {
}
}
-func TestCheckUserDomain(t *testing.T) {
- th := Setup().InitBasic()
- user := th.BasicUser
-
- cases := []struct {
- domains string
- matched bool
- }{
- {"simulator.amazonses.com", true},
- {"gmail.com", false},
- {"", true},
- {"gmail.com simulator.amazonses.com", true},
- }
- for _, c := range cases {
- matched := CheckUserDomain(user, c.domains)
- if matched != c.matched {
- if c.matched {
- t.Logf("'%v' should have matched '%v'", user.Email, c.domains)
- } else {
- t.Logf("'%v' should not have matched '%v'", user.Email, c.domains)
- }
- t.FailNow()
- }
- }
-}
-
-func TestIsUsernameTaken(t *testing.T) {
- th := Setup().InitBasic()
- user := th.BasicUser
- taken := IsUsernameTaken(user.Username)
-
- if !taken {
- t.Logf("the username '%v' should be taken", user.Username)
- t.FailNow()
- }
-
- newUsername := "randomUsername"
- taken = IsUsernameTaken(newUsername)
-
- if taken {
- t.Logf("the username '%v' should not be taken", newUsername)
- t.FailNow()
- }
-}
-
func TestLogin(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
enableSignInWithEmail := *utils.Cfg.EmailSettings.EnableSignInWithEmail
enableSignInWithUsername := *utils.Cfg.EmailSettings.EnableSignInWithUsername
@@ -145,10 +103,15 @@ func TestLogin(t *testing.T) {
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
+ team2 := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_INVITE}
+ rteam2 := Client.Must(Client.CreateTeam(&team2))
+
+ Client.Logout()
+
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Username: "corey" + model.NewId(), Password: "passwd1"}
ruser, _ := Client.CreateUser(&user, "")
LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team))
- store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
if result, err := Client.LoginById(ruser.Data.(*model.User).Id, user.Password); err != nil {
t.Fatal(err)
@@ -209,9 +172,6 @@ func TestLogin(t *testing.T) {
Client.AuthToken = ""
- team2 := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_INVITE}
- rteam2 := Client.Must(Client.CreateTeam(&team2))
-
user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
if _, err := Client.CreateUserFromSignup(&user2, "junk", "1231312"); err == nil {
@@ -226,7 +186,10 @@ func TestLogin(t *testing.T) {
data := model.MapToJson(props)
hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt))
- ruser2, _ := Client.CreateUserFromSignup(&user2, data, hash)
+ ruser2, err := Client.CreateUserFromSignup(&user2, data, hash)
+ if err != nil {
+ t.Fatal(err)
+ }
if _, err := Client.Login(ruser2.Data.(*model.User).Email, user2.Password); err != nil {
t.Fatal("From verfied hash")
@@ -242,7 +205,7 @@ func TestLogin(t *testing.T) {
AuthService: model.USER_AUTH_SERVICE_LDAP,
}
user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User)
- store.Must(Srv.Store.User().VerifyEmail(user3.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user3.Id))
if _, err := Client.Login(user3.Id, user3.Password); err == nil {
t.Fatal("AD/LDAP user should not be able to log in with AD/LDAP disabled")
@@ -250,16 +213,18 @@ func TestLogin(t *testing.T) {
}
func TestLoginByLdap(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
+ Client.Logout()
+
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Username: "corey" + model.NewId(), Password: "passwd1"}
ruser, _ := Client.CreateUser(&user, "")
LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team))
- store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
if _, err := Client.LoginByLdap(ruser.Data.(*model.User).Id, user.Password); err == nil {
t.Fatal("should have failed to log in with non AD/LDAP user")
@@ -286,7 +251,7 @@ func TestLoginWithDeviceId(t *testing.T) {
t.Fatal(err)
}
- if sresult := <-Srv.Store.Session().Get(sessions[0].Id); sresult.Err == nil {
+ if sresult := <-app.Srv.Store.Session().Get(sessions[0].Id); sresult.Err == nil {
t.Fatal("session should have been removed")
}
}
@@ -378,29 +343,31 @@ func TestSessions(t *testing.T) {
}
func TestGetUser(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
+ team2 := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ rteam2, _ := Client.CreateTeam(&team2)
+
+ Client.Logout()
+
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser, _ := Client.CreateUser(&user, "")
LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team))
- store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1", FirstName: "Corey", LastName: "Hulen"}
ruser2, _ := Client.CreateUser(&user2, "")
LinkUserToTeam(ruser2.Data.(*model.User), rteam.Data.(*model.Team))
- store.Must(Srv.Store.User().VerifyEmail(ruser2.Data.(*model.User).Id))
-
- team2 := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
- rteam2, _ := Client.CreateTeam(&team2)
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser2.Data.(*model.User).Id))
user3 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser3, _ := Client.CreateUser(&user3, "")
LinkUserToTeam(ruser3.Data.(*model.User), rteam2.Data.(*model.Team))
- store.Must(Srv.Store.User().VerifyEmail(ruser3.Data.(*model.User).Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser3.Data.(*model.User).Id))
Client.Login(user.Email, user.Password)
@@ -481,8 +448,8 @@ func TestGetUser(t *testing.T) {
if userMap, err := Client.GetProfilesInTeam(rteam.Data.(*model.Team).Id, 0, 100, ""); err != nil {
t.Fatal(err)
- } else if len(userMap.Data.(map[string]*model.User)) != 2 {
- t.Fatal("should have been 2")
+ } else if len(userMap.Data.(map[string]*model.User)) != 3 {
+ t.Fatal("should have been 3")
} else if userMap.Data.(map[string]*model.User)[rId].Id != rId {
t.Fatal("should have been valid")
} else {
@@ -523,7 +490,7 @@ func TestGetUser(t *testing.T) {
t.Fatal("shouldn't have accss")
}
- UpdateUserRoles(ruser.Data.(*model.User), model.ROLE_SYSTEM_ADMIN.Id)
+ app.UpdateUserRoles(ruser.Data.(*model.User).Id, model.ROLE_SYSTEM_ADMIN.Id)
Client.Login(user.Email, "passwd1")
@@ -644,16 +611,18 @@ func TestGetProfilesByIds(t *testing.T) {
}
func TestGetAudits(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
+ Client.Logout()
+
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser, _ := Client.CreateUser(&user, "")
LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team))
- store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
time.Sleep(100 * time.Millisecond)
@@ -682,10 +651,10 @@ func TestGetAudits(t *testing.T) {
}
func TestUserCreateImage(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
- b, err := createProfileImage("Corey Hulen", "eo1zkdr96pdj98pjmq8zy35wba")
+ b, err := app.CreateProfileImage("Corey Hulen", "eo1zkdr96pdj98pjmq8zy35wba")
if err != nil {
t.Fatal(err)
}
@@ -708,7 +677,7 @@ func TestUserCreateImage(t *testing.T) {
user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
LinkUserToTeam(user, team)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user.Id))
Client.Login(user.Email, "passwd1")
@@ -744,16 +713,18 @@ func TestUserCreateImage(t *testing.T) {
}
func TestUserUploadProfileImage(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+ Client.Logout()
+
user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
LinkUserToTeam(user, team)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user.Id))
if utils.Cfg.FileSettings.DriverName != "" {
@@ -853,16 +824,18 @@ func TestUserUploadProfileImage(t *testing.T) {
}
func TestUserUpdate(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+ Client.Logout()
+
user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1", Roles: ""}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
LinkUserToTeam(user, team)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user.Id))
if _, err := Client.UpdateUser(user); err == nil {
t.Fatal("Should have errored")
@@ -892,7 +865,7 @@ func TestUserUpdate(t *testing.T) {
user2 := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
LinkUserToTeam(user2, team)
- store.Must(Srv.Store.User().VerifyEmail(user2.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user2.Id))
Client.Login(user2.Email, "passwd1")
Client.SetTeamId(team.Id)
@@ -905,17 +878,19 @@ func TestUserUpdate(t *testing.T) {
}
func TestUserUpdatePassword(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+
+ Client.Logout()
Client.SetTeamId(team.Id)
user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
LinkUserToTeam(user, team)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user.Id))
if _, err := Client.UpdateUserPassword(user.Id, "passwd1", "newpasswd1"); err == nil {
t.Fatal("Should have errored")
@@ -988,21 +963,23 @@ func TestUserUpdatePassword(t *testing.T) {
}
func TestUserUpdateRoles(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+ Client.Logout()
+
user := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
LinkUserToTeam(user, team)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user.Id))
user2 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
LinkUserToTeam(user2, team)
- store.Must(Srv.Store.User().VerifyEmail(user2.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user2.Id))
if _, err := Client.UpdateUserRoles(user.Id, ""); err == nil {
t.Fatal("Should have errored, not logged in")
@@ -1021,7 +998,7 @@ func TestUserUpdateRoles(t *testing.T) {
user3 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User)
LinkUserToTeam(user3, team2)
- store.Must(Srv.Store.User().VerifyEmail(user3.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user3.Id))
Client.Login(user3.Email, "passwd1")
Client.SetTeamId(team2.Id)
@@ -1107,8 +1084,8 @@ func TestUserUpdateRolesMoreCases(t *testing.T) {
}
func TestUserUpdateDeviceId(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
@@ -1116,7 +1093,7 @@ func TestUserUpdateDeviceId(t *testing.T) {
user := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
LinkUserToTeam(user, team)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user.Id))
Client.Login(user.Email, "passwd1")
Client.SetTeamId(team.Id)
@@ -1126,7 +1103,7 @@ func TestUserUpdateDeviceId(t *testing.T) {
t.Fatal(err)
}
- if result := <-Srv.Store.Session().GetSessions(user.Id); result.Err != nil {
+ if result := <-app.Srv.Store.Session().GetSessions(user.Id); result.Err != nil {
t.Fatal(result.Err)
} else {
sessions := result.Data.([]*model.Session)
@@ -1138,22 +1115,27 @@ func TestUserUpdateDeviceId(t *testing.T) {
}
func TestUserUpdateActive(t *testing.T) {
- th := Setup().InitSystemAdmin()
- Client := th.CreateClient()
+ th := Setup().InitBasic().InitSystemAdmin()
+ Client := th.BasicClient
SystemAdminClient := th.SystemAdminClient
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+ team2 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team)
+
+ Client.Logout()
+
user := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
LinkUserToTeam(user, team)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user.Id))
user2 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
LinkUserToTeam(user2, team)
- store.Must(Srv.Store.User().VerifyEmail(user2.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user2.Id))
if _, err := Client.UpdateActive(user.Id, false); err == nil {
t.Fatal("Should have errored, not logged in")
@@ -1168,13 +1150,10 @@ func TestUserUpdateActive(t *testing.T) {
Client.Must(Client.Logout())
- team2 := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
- team2 = Client.Must(Client.CreateTeam(team2)).Data.(*model.Team)
-
user3 := &model.User{Email: "success+" + model.NewId() + "@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user3 = Client.Must(Client.CreateUser(user3, "")).Data.(*model.User)
LinkUserToTeam(user2, team2)
- store.Must(Srv.Store.User().VerifyEmail(user3.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user3.Id))
Client.Login(user3.Email, "passwd1")
Client.SetTeamId(team2.Id)
@@ -1194,13 +1173,13 @@ func TestUserUpdateActive(t *testing.T) {
t.Fatal("Should have errored, bad id")
}
- SetStatusOnline(user3.Id, "", false)
+ app.SetStatusOnline(user3.Id, "", false)
if _, err := SystemAdminClient.UpdateActive(user3.Id, false); err != nil {
t.Fatal(err)
}
- if status, err := GetStatus(user3.Id); err != nil {
+ if status, err := app.GetStatus(user3.Id); err != nil {
t.Fatal(err)
} else if status.Status != model.STATUS_OFFLINE {
t.Fatal("status should have been set to offline")
@@ -1208,8 +1187,8 @@ func TestUserUpdateActive(t *testing.T) {
}
func TestUserPermDelete(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
@@ -1217,7 +1196,7 @@ func TestUserPermDelete(t *testing.T) {
user1 := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user1 = Client.Must(Client.CreateUser(user1, "")).Data.(*model.User)
LinkUserToTeam(user1, team)
- store.Must(Srv.Store.User().VerifyEmail(user1.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user1.Id))
Client.Login(user1.Email, "passwd1")
Client.SetTeamId(team.Id)
@@ -1241,7 +1220,7 @@ func TestUserPermDelete(t *testing.T) {
c.RequestId = model.NewId()
c.IpAddress = "test"
- err := PermanentDeleteUser(user1)
+ err := app.PermanentDeleteUser(user1)
if err != nil {
t.Fatal(err)
}
@@ -1250,8 +1229,8 @@ func TestUserPermDelete(t *testing.T) {
}
func TestSendPasswordReset(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
@@ -1259,7 +1238,9 @@ func TestSendPasswordReset(t *testing.T) {
user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
LinkUserToTeam(user, team)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user.Id))
+
+ Client.Logout()
if result, err := Client.SendPasswordReset(user.Email); err != nil {
t.Fatal(err)
@@ -1282,7 +1263,7 @@ func TestSendPasswordReset(t *testing.T) {
user2 := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", AuthData: &authData, AuthService: "random"}
user2 = Client.Must(Client.CreateUser(user2, "")).Data.(*model.User)
LinkUserToTeam(user2, team)
- store.Must(Srv.Store.User().VerifyEmail(user2.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user2.Id))
if _, err := Client.SendPasswordReset(user2.Email); err == nil {
t.Fatal("should have errored - SSO user can't send reset password link")
@@ -1297,17 +1278,33 @@ func TestResetPassword(t *testing.T) {
user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
LinkUserToTeam(user, team)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user.Id))
+
+ //Delete all the messages before check the reset password
+ utils.DeleteMailBox(user.Email)
Client.Must(Client.SendPasswordReset(user.Email))
var recovery *model.PasswordRecovery
- if result := <-Srv.Store.PasswordRecovery().Get(user.Id); result.Err != nil {
+ if result := <-app.Srv.Store.PasswordRecovery().Get(user.Id); result.Err != nil {
t.Fatal(result.Err)
} else {
recovery = result.Data.(*model.PasswordRecovery)
}
+ //Check if the email was send to the rigth email address and the recovery key match
+ if resultsMailbox, err := utils.GetMailBox(user.Email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], user.Email) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(user.Email, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Body.Text, recovery.Code) {
+ t.Log(resultsEmail.Body.Text)
+ t.Log(recovery.Code)
+ t.Fatal("Received wrong recovery code")
+ }
+ }
+ }
+
if _, err := Client.ResetPassword(recovery.Code, ""); err == nil {
t.Fatal("Should have errored - no password")
}
@@ -1333,6 +1330,7 @@ func TestResetPassword(t *testing.T) {
}
if _, err := Client.ResetPassword(recovery.Code, "newpwd1"); err != nil {
+ t.Log(recovery.Code)
t.Fatal(err)
}
@@ -1342,14 +1340,14 @@ func TestResetPassword(t *testing.T) {
Client.Must(Client.SendPasswordReset(user.Email))
- if result := <-Srv.Store.PasswordRecovery().Get(user.Id); result.Err != nil {
+ if result := <-app.Srv.Store.PasswordRecovery().Get(user.Id); result.Err != nil {
t.Fatal(result.Err)
} else {
recovery = result.Data.(*model.PasswordRecovery)
}
authData := model.NewId()
- if result := <-Srv.Store.User().UpdateAuthData(user.Id, "random", &authData, "", true); result.Err != nil {
+ if result := <-app.Srv.Store.User().UpdateAuthData(user.Id, "random", &authData, "", true); result.Err != nil {
t.Fatal(result.Err)
}
@@ -1359,16 +1357,18 @@ func TestResetPassword(t *testing.T) {
}
func TestUserUpdateNotify(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
team = Client.Must(Client.CreateTeam(team)).Data.(*model.Team)
+ Client.Logout()
+
user := &model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1", Roles: ""}
user = Client.Must(Client.CreateUser(user, "")).Data.(*model.User)
LinkUserToTeam(user, team)
- store.Must(Srv.Store.User().VerifyEmail(user.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(user.Id))
data := make(map[string]string)
data["user_id"] = user.Id
@@ -1441,12 +1441,14 @@ func TestUserUpdateNotify(t *testing.T) {
}
func TestFuzzyUserCreate(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
+ Client.Logout()
+
for i := 0; i < len(utils.FUZZY_STRINGS_NAMES) || i < len(utils.FUZZY_STRINGS_EMAILS); i++ {
testName := "Name"
testEmail := "test@nowhere.com"
@@ -1470,16 +1472,18 @@ func TestFuzzyUserCreate(t *testing.T) {
}
func TestEmailToOAuth(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
+ Client.Logout()
+
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User)
LinkUserToTeam(ruser, rteam.Data.(*model.Team))
- store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser.Id))
m := map[string]string{}
if _, err := Client.EmailToOAuth(m); err == nil {
@@ -1521,21 +1525,23 @@ func TestEmailToOAuth(t *testing.T) {
}
func TestOAuthToEmail(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
+ Client.Logout()
+
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User)
LinkUserToTeam(ruser, rteam.Data.(*model.Team))
- store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser.Id))
user2 := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser2 := Client.Must(Client.CreateUser(&user2, "")).Data.(*model.User)
LinkUserToTeam(ruser2, rteam.Data.(*model.Team))
- store.Must(Srv.Store.User().VerifyEmail(ruser2.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser2.Id))
m := map[string]string{}
if _, err := Client.OAuthToEmail(m); err == nil {
@@ -1572,8 +1578,8 @@ func TestOAuthToEmail(t *testing.T) {
}
func TestLDAPToEmail(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
@@ -1581,7 +1587,7 @@ func TestLDAPToEmail(t *testing.T) {
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User)
LinkUserToTeam(ruser, rteam.Data.(*model.Team))
- store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser.Id))
Client.Login(user.Email, user.Password)
@@ -1625,8 +1631,8 @@ func TestLDAPToEmail(t *testing.T) {
}
func TestEmailToLDAP(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
@@ -1634,7 +1640,7 @@ func TestEmailToLDAP(t *testing.T) {
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User)
LinkUserToTeam(ruser, rteam.Data.(*model.Team))
- store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser.Id))
Client.Login(user.Email, user.Password)
@@ -1756,8 +1762,8 @@ func TestMeInitialLoad(t *testing.T) {
}
func TestGenerateMfaSecret(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
@@ -1765,7 +1771,7 @@ func TestGenerateMfaSecret(t *testing.T) {
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser, _ := Client.CreateUser(&user, "")
LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team))
- store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
Client.Logout()
@@ -1783,8 +1789,8 @@ func TestGenerateMfaSecret(t *testing.T) {
}
func TestUpdateMfa(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
if utils.License.Features.MFA == nil {
utils.License.Features.MFA = new(bool)
@@ -1803,7 +1809,7 @@ func TestUpdateMfa(t *testing.T) {
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser, _ := Client.CreateUser(&user, "")
LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team))
- store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
Client.Logout()
@@ -1833,16 +1839,18 @@ func TestUpdateMfa(t *testing.T) {
}
func TestCheckMfa(t *testing.T) {
- th := Setup()
- Client := th.CreateClient()
+ th := Setup().InitBasic()
+ Client := th.BasicClient
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
+ Client.Logout()
+
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
ruser, _ := Client.CreateUser(&user, "")
LinkUserToTeam(ruser.Data.(*model.User), rteam.Data.(*model.Team))
- store.Must(Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser.Data.(*model.User).Id))
if result, err := Client.CheckMfa(user.Email); err != nil {
t.Fatal(err)
diff --git a/api/webhook.go b/api/webhook.go
index 8a4263533..5d36409eb 100644
--- a/api/webhook.go
+++ b/api/webhook.go
@@ -11,6 +11,7 @@ import (
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
@@ -31,7 +32,7 @@ func InitWebhook() {
BaseRoutes.Hooks.Handle("/{id:[A-Za-z0-9]+}", ApiAppHandler(incomingWebhook)).Methods("POST")
// Old route. Remove eventually.
- mr := Srv.Router
+ mr := app.Srv.Router
mr.Handle("/hooks/{id:[A-Za-z0-9]+}", ApiAppHandler(incomingWebhook)).Methods("POST")
}
@@ -42,7 +43,8 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_WEBHOOKS) {
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_WEBHOOKS) {
+ c.SetPermissionError(model.PERMISSION_MANAGE_WEBHOOKS)
return
}
@@ -55,7 +57,7 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- cchan := Srv.Store.Channel().Get(hook.ChannelId, true)
+ cchan := app.Srv.Store.Channel().Get(hook.ChannelId, true)
hook.UserId = c.Session.UserId
hook.TeamId = c.TeamId
@@ -68,12 +70,13 @@ func createIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
channel = result.Data.(*model.Channel)
}
- if channel.Type != model.CHANNEL_OPEN && !HasPermissionToChannelContext(c, channel.Id, model.PERMISSION_READ_CHANNEL) {
+ if channel.Type != model.CHANNEL_OPEN && !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_READ_CHANNEL) {
c.LogAudit("fail - bad channel permissions")
+ c.SetPermissionError(model.PERMISSION_READ_CHANNEL)
return
}
- if result := <-Srv.Store.Webhook().SaveIncoming(hook); result.Err != nil {
+ if result := <-app.Srv.Store.Webhook().SaveIncoming(hook); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -90,7 +93,7 @@ func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_WEBHOOKS) {
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_WEBHOOKS) {
c.Err = model.NewLocAppError("deleteIncomingHook", "api.command.admin_only.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
return
@@ -106,18 +109,18 @@ func deleteIncomingHook(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.Webhook().GetIncoming(id); result.Err != nil {
+ if result := <-app.Srv.Store.Webhook().GetIncoming(id); result.Err != nil {
c.Err = result.Err
return
} else {
- if c.Session.UserId != result.Data.(*model.IncomingWebhook).UserId && !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_OTHERS_WEBHOOKS) {
+ if c.Session.UserId != result.Data.(*model.IncomingWebhook).UserId && !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_OTHERS_WEBHOOKS) {
c.LogAudit("fail - inappropriate permissions")
c.Err = model.NewLocAppError("deleteIncomingHook", "api.webhook.delete_incoming.permissions.app_errror", nil, "user_id="+c.Session.UserId)
return
}
}
- if err := (<-Srv.Store.Webhook().DeleteIncoming(id, model.GetMillis())).Err; err != nil {
+ if err := (<-app.Srv.Store.Webhook().DeleteIncoming(id, model.GetMillis())).Err; err != nil {
c.Err = err
return
}
@@ -133,13 +136,13 @@ func getIncomingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_WEBHOOKS) {
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_WEBHOOKS) {
c.Err = model.NewLocAppError("getIncomingHooks", "api.command.admin_only.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
return
}
- if result := <-Srv.Store.Webhook().GetIncomingByTeam(c.TeamId); result.Err != nil {
+ if result := <-app.Srv.Store.Webhook().GetIncomingByTeam(c.TeamId); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -155,7 +158,7 @@ func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_WEBHOOKS) {
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_WEBHOOKS) {
c.Err = model.NewLocAppError("createOutgoingHook", "api.command.admin_only.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
return
@@ -174,7 +177,7 @@ func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
hook.TeamId = c.TeamId
if len(hook.ChannelId) != 0 {
- cchan := Srv.Store.Channel().Get(hook.ChannelId, true)
+ cchan := app.Srv.Store.Channel().Get(hook.ChannelId, true)
var channel *model.Channel
if result := <-cchan; result.Err != nil {
@@ -200,7 +203,7 @@ func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.Webhook().GetOutgoingByTeam(c.TeamId); result.Err != nil {
+ if result := <-app.Srv.Store.Webhook().GetOutgoingByTeam(c.TeamId); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -217,7 +220,7 @@ func createOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
}
}
- if result := <-Srv.Store.Webhook().SaveOutgoing(hook); result.Err != nil {
+ if result := <-app.Srv.Store.Webhook().SaveOutgoing(hook); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -234,13 +237,13 @@ func getOutgoingHooks(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_WEBHOOKS) {
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_WEBHOOKS) {
c.Err = model.NewLocAppError("getOutgoingHooks", "api.command.admin_only.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
return
}
- if result := <-Srv.Store.Webhook().GetOutgoingByTeam(c.TeamId); result.Err != nil {
+ if result := <-app.Srv.Store.Webhook().GetOutgoingByTeam(c.TeamId); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -256,7 +259,7 @@ func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_WEBHOOKS) {
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_WEBHOOKS) {
c.Err = model.NewLocAppError("deleteOutgoingHook", "api.command.admin_only.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
return
@@ -272,18 +275,18 @@ func deleteOutgoingHook(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- if result := <-Srv.Store.Webhook().GetOutgoing(id); result.Err != nil {
+ if result := <-app.Srv.Store.Webhook().GetOutgoing(id); result.Err != nil {
c.Err = result.Err
return
} else {
- if c.Session.UserId != result.Data.(*model.OutgoingWebhook).CreatorId && !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_OTHERS_WEBHOOKS) {
+ if c.Session.UserId != result.Data.(*model.OutgoingWebhook).CreatorId && !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_OTHERS_WEBHOOKS) {
c.LogAudit("fail - inappropriate permissions")
c.Err = model.NewLocAppError("deleteOutgoingHook", "api.webhook.delete_outgoing.permissions.app_error", nil, "user_id="+c.Session.UserId)
return
}
}
- if err := (<-Srv.Store.Webhook().DeleteOutgoing(id, model.GetMillis())).Err; err != nil {
+ if err := (<-app.Srv.Store.Webhook().DeleteOutgoing(id, model.GetMillis())).Err; err != nil {
c.Err = err
return
}
@@ -299,7 +302,7 @@ func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request)
return
}
- if !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_WEBHOOKS) {
+ if !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_WEBHOOKS) {
c.Err = model.NewLocAppError("regenOutgoingHookToken", "api.command.admin_only.app_error", nil, "")
c.Err.StatusCode = http.StatusForbidden
return
@@ -316,13 +319,13 @@ func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request)
}
var hook *model.OutgoingWebhook
- if result := <-Srv.Store.Webhook().GetOutgoing(id); result.Err != nil {
+ if result := <-app.Srv.Store.Webhook().GetOutgoing(id); result.Err != nil {
c.Err = result.Err
return
} else {
hook = result.Data.(*model.OutgoingWebhook)
- if c.TeamId != hook.TeamId && c.Session.UserId != hook.CreatorId && !HasPermissionToCurrentTeamContext(c, model.PERMISSION_MANAGE_OTHERS_WEBHOOKS) {
+ if c.TeamId != hook.TeamId && c.Session.UserId != hook.CreatorId && !app.SessionHasPermissionToTeam(c.Session, c.TeamId, model.PERMISSION_MANAGE_OTHERS_WEBHOOKS) {
c.LogAudit("fail - inappropriate permissions")
c.Err = model.NewLocAppError("regenOutgoingHookToken", "api.webhook.regen_outgoing_token.permissions.app_error", nil, "user_id="+c.Session.UserId)
return
@@ -331,7 +334,7 @@ func regenOutgoingHookToken(c *Context, w http.ResponseWriter, r *http.Request)
hook.Token = model.NewId()
- if result := <-Srv.Store.Webhook().UpdateOutgoing(hook); result.Err != nil {
+ if result := <-app.Srv.Store.Webhook().UpdateOutgoing(hook); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -349,7 +352,7 @@ func incomingWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
id := params["id"]
- hchan := Srv.Store.Webhook().GetIncoming(id)
+ hchan := app.Srv.Store.Webhook().GetIncoming(id)
r.ParseForm()
@@ -434,7 +437,7 @@ func incomingWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
if len(channelName) != 0 {
if channelName[0] == '@' {
- if result := <-Srv.Store.User().GetByUsername(channelName[1:]); result.Err != nil {
+ if result := <-app.Srv.Store.User().GetByUsername(channelName[1:]); result.Err != nil {
c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.user.app_error", nil, "err="+result.Err.Message)
return
} else {
@@ -445,9 +448,9 @@ func incomingWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
channelName = channelName[1:]
}
- cchan = Srv.Store.Channel().GetByName(hook.TeamId, channelName)
+ cchan = app.Srv.Store.Channel().GetByName(hook.TeamId, channelName)
} else {
- cchan = Srv.Store.Channel().Get(hook.ChannelId, true)
+ cchan = app.Srv.Store.Channel().Get(hook.ChannelId, true)
}
overrideUsername := parsedRequest.Username
@@ -455,14 +458,14 @@ func incomingWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
result := <-cchan
if result.Err != nil && result.Err.Id == store.MISSING_CHANNEL_ERROR && directUserId != "" {
- newChanResult := <-Srv.Store.Channel().CreateDirectChannel(directUserId, hook.UserId)
+ newChanResult := <-app.Srv.Store.Channel().CreateDirectChannel(directUserId, hook.UserId)
if newChanResult.Err != nil {
c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.channel.app_error", nil, "err="+newChanResult.Err.Message)
return
} else {
channel = newChanResult.Data.(*model.Channel)
- InvalidateCacheForUser(directUserId)
- InvalidateCacheForUser(hook.UserId)
+ app.InvalidateCacheForUser(directUserId)
+ app.InvalidateCacheForUser(hook.UserId)
}
} else if result.Err != nil {
c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.channel.app_error", nil, "err="+result.Err.Message)
@@ -484,13 +487,13 @@ func incomingWebhook(c *Context, w http.ResponseWriter, r *http.Request) {
c.TeamId = hook.TeamId
- if channel.Type != model.CHANNEL_OPEN && !HasPermissionToChannelContext(c, channel.Id, model.PERMISSION_READ_CHANNEL) {
+ if channel.Type != model.CHANNEL_OPEN && !app.SessionHasPermissionToChannel(c.Session, channel.Id, model.PERMISSION_READ_CHANNEL) {
c.Err = model.NewLocAppError("incomingWebhook", "web.incoming_webhook.permissions.app_error", nil, "")
return
}
c.Err = nil
- if _, err := CreateWebhookPost(c, channel.Id, text, overrideUsername, overrideIconUrl, parsedRequest.Props, webhookType); err != nil {
+ if _, err := app.CreateWebhookPost(hook.UserId, hook.TeamId, channel.Id, text, overrideUsername, overrideIconUrl, parsedRequest.Props, webhookType); err != nil {
c.Err = err
return
}
diff --git a/api/webrtc.go b/api/webrtc.go
index 0ccbd8be1..8b00e724d 100644
--- a/api/webrtc.go
+++ b/api/webrtc.go
@@ -8,14 +8,15 @@ import (
"crypto/sha1"
"crypto/tls"
"encoding/base64"
- l4g "github.com/alecthomas/log4go"
- "github.com/mattermost/platform/model"
- "github.com/mattermost/platform/utils"
- "io/ioutil"
"net/http"
"strconv"
"strings"
"time"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/app"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
)
func InitWebrtc() {
@@ -23,7 +24,7 @@ func InitWebrtc() {
BaseRoutes.Webrtc.Handle("/token", ApiUserRequired(webrtcToken)).Methods("POST")
- BaseRoutes.WebSocket.Handle("webrtc", ApiWebSocketHandler(webrtcMessage))
+ app.Srv.WebSocketRouter.Handle("webrtc", ApiWebSocketHandler(webrtcMessage))
}
func webrtcToken(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -60,7 +61,7 @@ func webrtcMessage(req *model.WebSocketRequest) (map[string]interface{}, *model.
event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_WEBRTC, "", "", toUserId, nil)
event.Data = req.Data
- go Publish(event)
+ go app.Publish(event)
return nil, nil
}
@@ -88,7 +89,7 @@ func getWebrtcToken(sessionId string) (string, *model.AppError) {
if rp, err := httpClient.Do(rq); err != nil {
return "", model.NewLocAppError("WebRTC.Token", "model.client.connecting.app_error", nil, err.Error())
} else if rp.StatusCode >= 300 {
- defer closeBody(rp)
+ defer app.CloseBody(rp)
return "", model.AppErrorFromJson(rp.Body)
} else {
janusResponse := model.GatewayResponseFromJson(rp.Body)
@@ -106,29 +107,3 @@ func generateTurnPassword(username string, secret string) string {
h.Write([]byte(username))
return base64.StdEncoding.EncodeToString(h.Sum(nil))
}
-
-func closeBody(r *http.Response) {
- if r.Body != nil {
- ioutil.ReadAll(r.Body)
- r.Body.Close()
- }
-}
-
-func RevokeWebrtcToken(sessionId string) {
- token := base64.StdEncoding.EncodeToString([]byte(sessionId))
- data := make(map[string]string)
- data["janus"] = "remove_token"
- data["token"] = token
- data["transaction"] = model.NewId()
- data["admin_secret"] = *utils.Cfg.WebrtcSettings.GatewayAdminSecret
-
- rq, _ := http.NewRequest("POST", *utils.Cfg.WebrtcSettings.GatewayAdminUrl, strings.NewReader(model.MapToJson(data)))
- rq.Header.Set("Content-Type", "application/json")
-
- // we do not care about the response
- tr := &http.Transport{
- TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections},
- }
- httpClient := &http.Client{Transport: tr}
- httpClient.Do(rq)
-}
diff --git a/api/webrtc_test.go b/api/webrtc_test.go
index 953333b09..21049d95d 100644
--- a/api/webrtc_test.go
+++ b/api/webrtc_test.go
@@ -5,6 +5,7 @@ package api
import (
"fmt"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
"testing"
@@ -39,5 +40,5 @@ func TestWebrtcToken(t *testing.T) {
fmt.Println("Turn Password", result["turn_password"])
}
- RevokeWebrtcToken(sessionId)
+ app.RevokeWebrtcToken(sessionId)
}
diff --git a/api/websocket.go b/api/websocket.go
index 1c3277497..11ae09036 100644
--- a/api/websocket.go
+++ b/api/websocket.go
@@ -4,27 +4,25 @@
package api
import (
+ "net/http"
+
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/websocket"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
- "net/http"
-)
-
-const (
- SOCKET_MAX_MESSAGE_SIZE_KB = 8 * 1024 // 8KB
)
func InitWebSocket() {
l4g.Debug(utils.T("api.web_socket.init.debug"))
BaseRoutes.Users.Handle("/websocket", ApiAppHandlerTrustRequester(connect)).Methods("GET")
- HubStart()
+ app.HubStart()
}
func connect(c *Context, w http.ResponseWriter, r *http.Request) {
upgrader := websocket.Upgrader{
- ReadBufferSize: SOCKET_MAX_MESSAGE_SIZE_KB,
- WriteBufferSize: SOCKET_MAX_MESSAGE_SIZE_KB,
+ ReadBufferSize: model.SOCKET_MAX_MESSAGE_SIZE_KB,
+ WriteBufferSize: model.SOCKET_MAX_MESSAGE_SIZE_KB,
CheckOrigin: func(r *http.Request) bool {
return true
},
@@ -37,8 +35,8 @@ func connect(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
- wc := NewWebConn(c, ws)
- HubRegister(wc)
- go wc.writePump()
- wc.readPump()
+ wc := app.NewWebConn(ws, c.Session, c.T, c.Locale)
+ app.HubRegister(wc)
+ go wc.WritePump()
+ wc.ReadPump()
}
diff --git a/api/websocket_handler.go b/api/websocket_handler.go
index 95aad8fee..25cdf6458 100644
--- a/api/websocket_handler.go
+++ b/api/websocket_handler.go
@@ -6,22 +6,34 @@ package api
import (
l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
-func ApiWebSocketHandler(wh func(*model.WebSocketRequest) (map[string]interface{}, *model.AppError)) *webSocketHandler {
- return &webSocketHandler{wh}
+func ApiWebSocketHandler(wh func(*model.WebSocketRequest) (map[string]interface{}, *model.AppError)) webSocketHandler {
+ return webSocketHandler{wh}
}
type webSocketHandler struct {
handlerFunc func(*model.WebSocketRequest) (map[string]interface{}, *model.AppError)
}
-func (wh *webSocketHandler) ServeWebSocket(conn *WebConn, r *model.WebSocketRequest) {
+func (wh webSocketHandler) ServeWebSocket(conn *app.WebConn, r *model.WebSocketRequest) {
l4g.Debug("/api/v3/users/websocket:%s", r.Action)
- r.Session = *GetSession(conn.SessionToken)
+ session, sessionErr := app.GetSession(conn.SessionToken)
+ if sessionErr != nil {
+ l4g.Error(utils.T("api.web_socket_handler.log.error"), "/api/v3/users/websocket", r.Action, r.Seq, conn.UserId, sessionErr.SystemMessage(utils.T), sessionErr.Error())
+ sessionErr.DetailedError = ""
+ errResp := model.NewWebSocketError(r.Seq, sessionErr)
+ errResp.DoPreComputeJson()
+
+ conn.Send <- errResp
+ return
+ }
+
+ r.Session = *session
r.T = conn.T
r.Locale = conn.Locale
diff --git a/api/websocket_test.go b/api/websocket_test.go
index d7dbf1561..39a55f8f4 100644
--- a/api/websocket_test.go
+++ b/api/websocket_test.go
@@ -10,6 +10,7 @@ import (
"time"
//"github.com/gorilla/websocket"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
)
@@ -192,7 +193,7 @@ func TestWebSocketEvent(t *testing.T) {
omitUser["somerandomid"] = true
evt1 := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_TYPING, "", th.BasicChannel.Id, "", omitUser)
evt1.Add("user_id", "somerandomid")
- Publish(evt1)
+ app.Publish(evt1)
time.Sleep(300 * time.Millisecond)
@@ -221,7 +222,7 @@ func TestWebSocketEvent(t *testing.T) {
}
evt2 := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_TYPING, "", "somerandomid", "", nil)
- go Publish(evt2)
+ go app.Publish(evt2)
time.Sleep(300 * time.Millisecond)
eventHit = false
diff --git a/app/admin.go b/app/admin.go
new file mode 100644
index 000000000..51e69da57
--- /dev/null
+++ b/app/admin.go
@@ -0,0 +1,191 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "bufio"
+ "os"
+ "strings"
+ "time"
+
+ "runtime/debug"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+ "github.com/mattermost/platform/utils"
+)
+
+func GetLogs() ([]string, *model.AppError) {
+ lines, err := GetLogsSkipSend()
+ if err != nil {
+ return nil, err
+ }
+
+ if einterfaces.GetClusterInterface() != nil {
+ clines, err := einterfaces.GetClusterInterface().GetLogs()
+ if err != nil {
+ return nil, err
+ }
+
+ lines = append(lines, clines...)
+ }
+
+ return lines, nil
+}
+
+func GetLogsSkipSend() ([]string, *model.AppError) {
+ var lines []string
+
+ if utils.Cfg.LogSettings.EnableFile {
+ file, err := os.Open(utils.GetLogFileLocation(utils.Cfg.LogSettings.FileLocation))
+ if err != nil {
+ return nil, model.NewLocAppError("getLogs", "api.admin.file_read_error", nil, err.Error())
+ }
+
+ defer file.Close()
+
+ scanner := bufio.NewScanner(file)
+ for scanner.Scan() {
+ lines = append(lines, scanner.Text())
+ }
+ } else {
+ lines = append(lines, "")
+ }
+
+ return lines, nil
+}
+
+func GetClusterStatus() []*model.ClusterInfo {
+ infos := make([]*model.ClusterInfo, 0)
+
+ if einterfaces.GetClusterInterface() != nil {
+ infos = einterfaces.GetClusterInterface().GetClusterInfos()
+ }
+
+ return infos
+}
+
+func InvalidateAllCaches() *model.AppError {
+ debug.FreeOSMemory()
+ InvalidateAllCachesSkipSend()
+
+ if einterfaces.GetClusterInterface() != nil {
+ err := einterfaces.GetClusterInterface().InvalidateAllCaches()
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func InvalidateAllCachesSkipSend() {
+ l4g.Info(utils.T("api.context.invalidate_all_caches"))
+ sessionCache.Purge()
+ ClearStatusCache()
+ store.ClearChannelCaches()
+ store.ClearUserCaches()
+ store.ClearPostCaches()
+}
+
+func GetConfig() *model.Config {
+ json := utils.Cfg.ToJson()
+ cfg := model.ConfigFromJson(strings.NewReader(json))
+ cfg.Sanitize()
+
+ return cfg
+}
+
+func ReloadConfig() {
+ debug.FreeOSMemory()
+ utils.LoadConfig(utils.CfgFileName)
+
+ // start/restart email batching job if necessary
+ InitEmailBatching()
+}
+
+func SaveConfig(cfg *model.Config) *model.AppError {
+ cfg.SetDefaults()
+ utils.Desanitize(cfg)
+
+ if err := cfg.IsValid(); err != nil {
+ return err
+ }
+
+ if err := utils.ValidateLdapFilter(cfg); err != nil {
+ return err
+ }
+
+ if *utils.Cfg.ClusterSettings.Enable {
+ return model.NewLocAppError("saveConfig", "ent.cluster.save_config.error", nil, "")
+ }
+
+ //oldCfg := utils.Cfg
+ utils.SaveConfig(utils.CfgFileName, cfg)
+ utils.LoadConfig(utils.CfgFileName)
+
+ if einterfaces.GetMetricsInterface() != nil {
+ if *utils.Cfg.MetricsSettings.Enable {
+ einterfaces.GetMetricsInterface().StartServer()
+ } else {
+ einterfaces.GetMetricsInterface().StopServer()
+ }
+ }
+
+ // Future feature is to sync the configuration files
+ // if einterfaces.GetClusterInterface() != nil {
+ // err := einterfaces.GetClusterInterface().ConfigChanged(cfg, oldCfg, true)
+ // if err != nil {
+ // return err
+ // }
+ // }
+
+ // start/restart email batching job if necessary
+ InitEmailBatching()
+
+ return nil
+}
+
+func RecycleDatabaseConnection() {
+ oldStore := Srv.Store
+
+ l4g.Warn(utils.T("api.admin.recycle_db_start.warn"))
+ Srv.Store = store.NewSqlStore()
+
+ time.Sleep(20 * time.Second)
+ oldStore.Close()
+
+ l4g.Warn(utils.T("api.admin.recycle_db_end.warn"))
+}
+
+func TestEmail(userId string, cfg *model.Config) *model.AppError {
+ if len(cfg.EmailSettings.SMTPServer) == 0 {
+ return model.NewLocAppError("testEmail", "api.admin.test_email.missing_server", nil, utils.T("api.context.invalid_param.app_error", map[string]interface{}{"Name": "SMTPServer"}))
+ }
+
+ // if the user hasn't changed their email settings, fill in the actual SMTP password so that
+ // the user can verify an existing SMTP connection
+ if cfg.EmailSettings.SMTPPassword == model.FAKE_SETTING {
+ if cfg.EmailSettings.SMTPServer == utils.Cfg.EmailSettings.SMTPServer &&
+ cfg.EmailSettings.SMTPPort == utils.Cfg.EmailSettings.SMTPPort &&
+ cfg.EmailSettings.SMTPUsername == utils.Cfg.EmailSettings.SMTPUsername {
+ cfg.EmailSettings.SMTPPassword = utils.Cfg.EmailSettings.SMTPPassword
+ } else {
+ return model.NewLocAppError("testEmail", "api.admin.test_email.reenter_password", nil, "")
+ }
+ }
+
+ if user, err := GetUser(userId); err != nil {
+ return err
+ } else {
+ T := utils.GetUserTranslations(user.Locale)
+ if err := utils.SendMailUsingConfig(user.Email, T("api.admin.test_email.subject"), T("api.admin.test_email.body"), cfg); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/app/analytics.go b/app/analytics.go
new file mode 100644
index 000000000..891c0dfae
--- /dev/null
+++ b/app/analytics.go
@@ -0,0 +1,239 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+ "github.com/mattermost/platform/utils"
+)
+
+const (
+ DAY_MILLISECONDS = 24 * 60 * 60 * 1000
+ MONTH_MILLISECONDS = 31 * DAY_MILLISECONDS
+)
+
+func GetAnalytics(name string, teamId string) (model.AnalyticsRows, *model.AppError) {
+ skipIntensiveQueries := false
+ var systemUserCount int64
+ if r := <-Srv.Store.User().AnalyticsUniqueUserCount(""); r.Err != nil {
+ return nil, r.Err
+ } else {
+ systemUserCount = r.Data.(int64)
+ if systemUserCount > int64(*utils.Cfg.AnalyticsSettings.MaxUsersForStatistics) {
+ l4g.Debug("More than %v users on the system, intensive queries skipped", *utils.Cfg.AnalyticsSettings.MaxUsersForStatistics)
+ skipIntensiveQueries = true
+ }
+ }
+
+ if name == "standard" {
+ var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 10)
+ rows[0] = &model.AnalyticsRow{"channel_open_count", 0}
+ rows[1] = &model.AnalyticsRow{"channel_private_count", 0}
+ rows[2] = &model.AnalyticsRow{"post_count", 0}
+ rows[3] = &model.AnalyticsRow{"unique_user_count", 0}
+ rows[4] = &model.AnalyticsRow{"team_count", 0}
+ rows[5] = &model.AnalyticsRow{"total_websocket_connections", 0}
+ rows[6] = &model.AnalyticsRow{"total_master_db_connections", 0}
+ rows[7] = &model.AnalyticsRow{"total_read_db_connections", 0}
+ rows[8] = &model.AnalyticsRow{"daily_active_users", 0}
+ rows[9] = &model.AnalyticsRow{"monthly_active_users", 0}
+
+ openChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_OPEN)
+ privateChan := Srv.Store.Channel().AnalyticsTypeCount(teamId, model.CHANNEL_PRIVATE)
+ teamChan := Srv.Store.Team().AnalyticsTeamCount()
+
+ var userChan store.StoreChannel
+ if teamId != "" {
+ userChan = Srv.Store.User().AnalyticsUniqueUserCount(teamId)
+ }
+
+ var postChan store.StoreChannel
+ if !skipIntensiveQueries {
+ postChan = Srv.Store.Post().AnalyticsPostCount(teamId, false, false)
+ }
+
+ dailyActiveChan := Srv.Store.User().AnalyticsActiveCount(DAY_MILLISECONDS)
+ monthlyActiveChan := Srv.Store.User().AnalyticsActiveCount(MONTH_MILLISECONDS)
+
+ if r := <-openChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[0].Value = float64(r.Data.(int64))
+ }
+
+ if r := <-privateChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[1].Value = float64(r.Data.(int64))
+ }
+
+ if postChan == nil {
+ rows[2].Value = -1
+ } else {
+ if r := <-postChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[2].Value = float64(r.Data.(int64))
+ }
+ }
+
+ if userChan == nil {
+ rows[3].Value = float64(systemUserCount)
+ } else {
+ if r := <-userChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[3].Value = float64(r.Data.(int64))
+ }
+ }
+
+ if r := <-teamChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[4].Value = float64(r.Data.(int64))
+ }
+
+ // If in HA mode then aggregrate all the stats
+ if einterfaces.GetClusterInterface() != nil && *utils.Cfg.ClusterSettings.Enable {
+ stats, err := einterfaces.GetClusterInterface().GetClusterStats()
+ if err != nil {
+ return nil, err
+ }
+
+ totalSockets := TotalWebsocketConnections()
+ totalMasterDb := Srv.Store.TotalMasterDbConnections()
+ totalReadDb := Srv.Store.TotalReadDbConnections()
+
+ for _, stat := range stats {
+ totalSockets = totalSockets + stat.TotalWebsocketConnections
+ totalMasterDb = totalMasterDb + stat.TotalMasterDbConnections
+ totalReadDb = totalReadDb + stat.TotalReadDbConnections
+ }
+
+ rows[5].Value = float64(totalSockets)
+ rows[6].Value = float64(totalMasterDb)
+ rows[7].Value = float64(totalReadDb)
+
+ } else {
+ rows[5].Value = float64(TotalWebsocketConnections())
+ rows[6].Value = float64(Srv.Store.TotalMasterDbConnections())
+ rows[7].Value = float64(Srv.Store.TotalReadDbConnections())
+ }
+
+ if r := <-dailyActiveChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[8].Value = float64(r.Data.(int64))
+ }
+
+ if r := <-monthlyActiveChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[9].Value = float64(r.Data.(int64))
+ }
+
+ return rows, nil
+ } else if name == "post_counts_day" {
+ if skipIntensiveQueries {
+ rows := model.AnalyticsRows{&model.AnalyticsRow{"", -1}}
+ return rows, nil
+ }
+
+ if r := <-Srv.Store.Post().AnalyticsPostCountsByDay(teamId); r.Err != nil {
+ return nil, r.Err
+ } else {
+ return r.Data.(model.AnalyticsRows), nil
+ }
+ } else if name == "user_counts_with_posts_day" {
+ if skipIntensiveQueries {
+ rows := model.AnalyticsRows{&model.AnalyticsRow{"", -1}}
+ return rows, nil
+ }
+
+ if r := <-Srv.Store.Post().AnalyticsUserCountsWithPostsByDay(teamId); r.Err != nil {
+ return nil, r.Err
+ } else {
+ return r.Data.(model.AnalyticsRows), nil
+ }
+ } else if name == "extra_counts" {
+ var rows model.AnalyticsRows = make([]*model.AnalyticsRow, 6)
+ rows[0] = &model.AnalyticsRow{"file_post_count", 0}
+ rows[1] = &model.AnalyticsRow{"hashtag_post_count", 0}
+ rows[2] = &model.AnalyticsRow{"incoming_webhook_count", 0}
+ rows[3] = &model.AnalyticsRow{"outgoing_webhook_count", 0}
+ rows[4] = &model.AnalyticsRow{"command_count", 0}
+ rows[5] = &model.AnalyticsRow{"session_count", 0}
+
+ iHookChan := Srv.Store.Webhook().AnalyticsIncomingCount(teamId)
+ oHookChan := Srv.Store.Webhook().AnalyticsOutgoingCount(teamId)
+ commandChan := Srv.Store.Command().AnalyticsCommandCount(teamId)
+ sessionChan := Srv.Store.Session().AnalyticsSessionCount()
+
+ var fileChan store.StoreChannel
+ var hashtagChan store.StoreChannel
+ if !skipIntensiveQueries {
+ fileChan = Srv.Store.Post().AnalyticsPostCount(teamId, true, false)
+ hashtagChan = Srv.Store.Post().AnalyticsPostCount(teamId, false, true)
+ }
+
+ if fileChan == nil {
+ rows[0].Value = -1
+ } else {
+ if r := <-fileChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[0].Value = float64(r.Data.(int64))
+ }
+ }
+
+ if hashtagChan == nil {
+ rows[1].Value = -1
+ } else {
+ if r := <-hashtagChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[1].Value = float64(r.Data.(int64))
+ }
+ }
+
+ if r := <-iHookChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[2].Value = float64(r.Data.(int64))
+ }
+
+ if r := <-oHookChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[3].Value = float64(r.Data.(int64))
+ }
+
+ if r := <-commandChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[4].Value = float64(r.Data.(int64))
+ }
+
+ if r := <-sessionChan; r.Err != nil {
+ return nil, r.Err
+ } else {
+ rows[5].Value = float64(r.Data.(int64))
+ }
+
+ return rows, nil
+ }
+
+ return nil, nil
+}
+
+func GetRecentlyActiveUsersForTeam(teamId string) (map[string]*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().GetRecentlyActiveUsersForTeam(teamId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(map[string]*model.User), nil
+ }
+}
diff --git a/app/app.go b/app/app.go
new file mode 100644
index 000000000..8568c7bba
--- /dev/null
+++ b/app/app.go
@@ -0,0 +1,16 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "io/ioutil"
+ "net/http"
+)
+
+func CloseBody(r *http.Response) {
+ if r.Body != nil {
+ ioutil.ReadAll(r.Body)
+ r.Body.Close()
+ }
+}
diff --git a/app/apptestlib.go b/app/apptestlib.go
new file mode 100644
index 000000000..41e234130
--- /dev/null
+++ b/app/apptestlib.go
@@ -0,0 +1,192 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "time"
+
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+
+ l4g "github.com/alecthomas/log4go"
+)
+
+type TestHelper struct {
+ BasicTeam *model.Team
+ BasicUser *model.User
+ BasicUser2 *model.User
+ BasicChannel *model.Channel
+ BasicPost *model.Post
+}
+
+func SetupEnterprise() *TestHelper {
+ if Srv == nil {
+ utils.TranslationsPreInit()
+ utils.LoadConfig("config.json")
+ utils.InitTranslations(utils.Cfg.LocalizationSettings)
+ utils.Cfg.TeamSettings.MaxUsersPerTeam = 50
+ *utils.Cfg.RateLimitSettings.Enable = false
+ utils.DisableDebugLogForTest()
+ utils.License.Features.SetDefaults()
+ NewServer()
+ InitStores()
+ StartServer()
+ utils.InitHTML()
+ utils.EnableDebugLogForTest()
+ Srv.Store.MarkSystemRanUnitTests()
+
+ *utils.Cfg.TeamSettings.EnableOpenServer = true
+ }
+
+ return &TestHelper{}
+}
+
+func Setup() *TestHelper {
+ if Srv == nil {
+ utils.TranslationsPreInit()
+ utils.LoadConfig("config.json")
+ utils.InitTranslations(utils.Cfg.LocalizationSettings)
+ utils.Cfg.TeamSettings.MaxUsersPerTeam = 50
+ *utils.Cfg.RateLimitSettings.Enable = false
+ utils.DisableDebugLogForTest()
+ NewServer()
+ InitStores()
+ StartServer()
+ utils.InitHTML()
+ utils.EnableDebugLogForTest()
+ Srv.Store.MarkSystemRanUnitTests()
+
+ *utils.Cfg.TeamSettings.EnableOpenServer = true
+ }
+
+ return &TestHelper{}
+}
+
+func (me *TestHelper) InitBasic() *TestHelper {
+ me.BasicTeam = me.CreateTeam()
+ me.BasicUser = me.CreateUser()
+ LinkUserToTeam(me.BasicUser, me.BasicTeam)
+ me.BasicUser2 = me.CreateUser()
+ LinkUserToTeam(me.BasicUser2, me.BasicTeam)
+ me.BasicChannel = me.CreateChannel(me.BasicTeam)
+ me.BasicPost = me.CreatePost(me.BasicChannel)
+
+ return me
+}
+
+func (me *TestHelper) CreateTeam() *model.Team {
+ id := model.NewId()
+ team := &model.Team{
+ DisplayName: "dn_" + id,
+ Name: "name" + id,
+ Email: "success+" + id + "@simulator.amazonses.com",
+ Type: model.TEAM_OPEN,
+ }
+
+ utils.DisableDebugLogForTest()
+ var err *model.AppError
+ if team, err = CreateTeam(team); err != nil {
+ l4g.Error(err.Error())
+ l4g.Close()
+ time.Sleep(time.Second)
+ panic(err)
+ }
+ utils.EnableDebugLogForTest()
+ return team
+}
+
+func (me *TestHelper) CreateUser() *model.User {
+ id := model.NewId()
+
+ user := &model.User{
+ Email: "success+" + id + "@simulator.amazonses.com",
+ Username: "un_" + id,
+ Nickname: "nn_" + id,
+ Password: "Password1",
+ EmailVerified: true,
+ }
+
+ utils.DisableDebugLogForTest()
+ var err *model.AppError
+ if user, err = CreateUser(user); err != nil {
+ l4g.Error(err.Error())
+ l4g.Close()
+ time.Sleep(time.Second)
+ panic(err)
+ }
+ utils.EnableDebugLogForTest()
+ return user
+}
+
+func (me *TestHelper) CreateChannel(team *model.Team) *model.Channel {
+ return me.createChannel(team, model.CHANNEL_OPEN)
+}
+
+func (me *TestHelper) CreatePrivateChannel(team *model.Team) *model.Channel {
+ return me.createChannel(team, model.CHANNEL_PRIVATE)
+}
+
+func (me *TestHelper) createChannel(team *model.Team, channelType string) *model.Channel {
+ id := model.NewId()
+
+ channel := &model.Channel{
+ DisplayName: "dn_" + id,
+ Name: "name_" + id,
+ Type: channelType,
+ TeamId: team.Id,
+ CreatorId: me.BasicUser.Id,
+ }
+
+ utils.DisableDebugLogForTest()
+ var err *model.AppError
+ if channel, err = CreateChannel(channel, true); err != nil {
+ l4g.Error(err.Error())
+ l4g.Close()
+ time.Sleep(time.Second)
+ panic(err)
+ }
+ utils.EnableDebugLogForTest()
+ return channel
+}
+
+func (me *TestHelper) CreatePost(channel *model.Channel) *model.Post {
+ id := model.NewId()
+
+ post := &model.Post{
+ UserId: me.BasicUser.Id,
+ ChannelId: channel.Id,
+ Message: "message_" + id,
+ }
+
+ utils.DisableDebugLogForTest()
+ var err *model.AppError
+ if post, err = CreatePost(post, channel.TeamId, false); err != nil {
+ l4g.Error(err.Error())
+ l4g.Close()
+ time.Sleep(time.Second)
+ panic(err)
+ }
+ utils.EnableDebugLogForTest()
+ return post
+}
+
+func LinkUserToTeam(user *model.User, team *model.Team) {
+ utils.DisableDebugLogForTest()
+
+ err := JoinUserToTeam(team, user)
+ if err != nil {
+ l4g.Error(err.Error())
+ l4g.Close()
+ time.Sleep(time.Second)
+ panic(err)
+ }
+
+ utils.EnableDebugLogForTest()
+}
+
+func TearDown() {
+ if Srv != nil {
+ StopServer()
+ }
+}
diff --git a/app/audit.go b/app/audit.go
new file mode 100644
index 000000000..6978e9bc2
--- /dev/null
+++ b/app/audit.go
@@ -0,0 +1,16 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "github.com/mattermost/platform/model"
+)
+
+func GetAudits(userId string, limit int) (model.Audits, *model.AppError) {
+ if result := <-Srv.Store.Audit().Get(userId, limit); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(model.Audits), nil
+ }
+}
diff --git a/app/authorization.go b/app/authorization.go
new file mode 100644
index 000000000..b43d64341
--- /dev/null
+++ b/app/authorization.go
@@ -0,0 +1,197 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/model"
+)
+
+func SessionHasPermissionTo(session model.Session, permission *model.Permission) bool {
+ return CheckIfRolesGrantPermission(session.GetUserRoles(), permission.Id)
+}
+
+func SessionHasPermissionToTeam(session model.Session, teamId string, permission *model.Permission) bool {
+ if teamId == "" {
+ return false
+ }
+
+ teamMember := session.GetTeamByTeamId(teamId)
+ if teamMember != nil {
+ if CheckIfRolesGrantPermission(teamMember.GetRoles(), permission.Id) {
+ return true
+ }
+ }
+
+ return SessionHasPermissionTo(session, permission)
+}
+
+func SessionHasPermissionToChannel(session model.Session, channelId string, permission *model.Permission) bool {
+ if channelId == "" {
+ return false
+ }
+
+ channelMember, err := GetChannelMember(channelId, session.UserId)
+ if err == nil {
+ roles := channelMember.GetRoles()
+ if CheckIfRolesGrantPermission(roles, permission.Id) {
+ return true
+ }
+ }
+
+ var channel *model.Channel
+ channel, err = GetChannel(channelId)
+ if err == nil {
+ return SessionHasPermissionToTeam(session, channel.TeamId, permission)
+ }
+
+ return SessionHasPermissionTo(session, permission)
+}
+
+func SessionHasPermissionToChannelByPost(session model.Session, postId string, permission *model.Permission) bool {
+ var channelMember *model.ChannelMember
+ if result := <-Srv.Store.Channel().GetMemberForPost(postId, session.UserId); result.Err == nil {
+ channelMember = result.Data.(*model.ChannelMember)
+
+ if CheckIfRolesGrantPermission(channelMember.GetRoles(), permission.Id) {
+ return true
+ }
+ }
+
+ if result := <-Srv.Store.Channel().GetForPost(postId); result.Err == nil {
+ channel := result.Data.(*model.Channel)
+ return SessionHasPermissionToTeam(session, channel.TeamId, permission)
+ }
+
+ return SessionHasPermissionTo(session, permission)
+}
+
+func SessionHasPermissionToUser(session model.Session, userId string) bool {
+ if userId == "" {
+ return false
+ }
+
+ if session.UserId == userId {
+ return true
+ }
+
+ if SessionHasPermissionTo(session, model.PERMISSION_EDIT_OTHER_USERS) {
+ return true
+ }
+
+ return false
+}
+
+func SessionHasPermissionToPost(session model.Session, postId string, permission *model.Permission) bool {
+ post, err := GetSinglePost(postId)
+ if err != nil {
+ return false
+ }
+
+ if post.UserId == session.UserId {
+ return true
+ }
+
+ return SessionHasPermissionToChannel(session, post.ChannelId, permission)
+}
+
+func HasPermissionTo(askingUserId string, permission *model.Permission) bool {
+ user, err := GetUser(askingUserId)
+ if err != nil {
+ return false
+ }
+
+ roles := user.GetRoles()
+
+ return CheckIfRolesGrantPermission(roles, permission.Id)
+}
+
+func HasPermissionToTeam(askingUserId string, teamId string, permission *model.Permission) bool {
+ if teamId == "" || askingUserId == "" {
+ return false
+ }
+
+ teamMember, err := GetTeamMember(teamId, askingUserId)
+ if err != nil {
+ return false
+ }
+
+ roles := teamMember.GetRoles()
+
+ if CheckIfRolesGrantPermission(roles, permission.Id) {
+ return true
+ }
+
+ return HasPermissionTo(askingUserId, permission)
+}
+
+func HasPermissionToChannel(askingUserId string, channelId string, permission *model.Permission) bool {
+ if channelId == "" || askingUserId == "" {
+ return false
+ }
+
+ channelMember, err := GetChannelMember(channelId, askingUserId)
+ if err == nil {
+ roles := channelMember.GetRoles()
+ if CheckIfRolesGrantPermission(roles, permission.Id) {
+ return true
+ }
+ }
+
+ var channel *model.Channel
+ channel, err = GetChannel(channelId)
+ if err == nil {
+ return HasPermissionToTeam(askingUserId, channel.TeamId, permission)
+ }
+
+ return HasPermissionTo(askingUserId, permission)
+}
+
+func HasPermissionToChannelByPost(askingUserId string, postId string, permission *model.Permission) bool {
+ var channelMember *model.ChannelMember
+ if result := <-Srv.Store.Channel().GetMemberForPost(postId, askingUserId); result.Err == nil {
+ channelMember = result.Data.(*model.ChannelMember)
+
+ if CheckIfRolesGrantPermission(channelMember.GetRoles(), permission.Id) {
+ return true
+ }
+ }
+
+ if result := <-Srv.Store.Channel().GetForPost(postId); result.Err == nil {
+ channel := result.Data.(*model.Channel)
+ return HasPermissionToTeam(askingUserId, channel.TeamId, permission)
+ }
+
+ return HasPermissionTo(askingUserId, permission)
+}
+
+func HasPermissionToUser(askingUserId string, userId string) bool {
+ if askingUserId == userId {
+ return true
+ }
+
+ if HasPermissionTo(askingUserId, model.PERMISSION_EDIT_OTHER_USERS) {
+ return true
+ }
+
+ return false
+}
+
+func CheckIfRolesGrantPermission(roles []string, permissionId string) bool {
+ for _, roleId := range roles {
+ if role, ok := model.BuiltInRoles[roleId]; !ok {
+ l4g.Debug("Bad role in system " + roleId)
+ return false
+ } else {
+ permissions := role.Permissions
+ for _, permission := range permissions {
+ if permission == permissionId {
+ return true
+ }
+ }
+ }
+ }
+
+ return false
+}
diff --git a/api/authorization_test.go b/app/authorization_test.go
index 5613751c2..049567483 100644
--- a/api/authorization_test.go
+++ b/app/authorization_test.go
@@ -1,7 +1,7 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package api
+package app
import (
"testing"
diff --git a/app/brand.go b/app/brand.go
new file mode 100644
index 000000000..aeecc6972
--- /dev/null
+++ b/app/brand.go
@@ -0,0 +1,49 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "mime/multipart"
+ "net/http"
+
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func SaveBrandImage(imageData *multipart.FileHeader) *model.AppError {
+ brandInterface := einterfaces.GetBrandInterface()
+ if brandInterface == nil {
+ err := model.NewLocAppError("SaveBrandImage", "api.admin.upload_brand_image.not_available.app_error", nil, "")
+ err.StatusCode = http.StatusNotImplemented
+ return err
+ }
+
+ if err := brandInterface.SaveBrandImage(imageData); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func GetBrandImage() ([]byte, *model.AppError) {
+ if len(utils.Cfg.FileSettings.DriverName) == 0 {
+ err := model.NewLocAppError("GetBrandImage", "api.admin.get_brand_image.storage.app_error", nil, "")
+ err.StatusCode = http.StatusNotImplemented
+ return nil, err
+ }
+
+ brandInterface := einterfaces.GetBrandInterface()
+ if brandInterface == nil {
+ err := model.NewLocAppError("GetBrandImage", "api.admin.get_brand_image.not_available.app_error", nil, "")
+ err.StatusCode = http.StatusNotImplemented
+ return nil, err
+ }
+
+ if img, err := brandInterface.GetBrandImage(); err != nil {
+ return nil, err
+ } else {
+ return img, nil
+ }
+}
diff --git a/app/channel.go b/app/channel.go
new file mode 100644
index 000000000..1844e3177
--- /dev/null
+++ b/app/channel.go
@@ -0,0 +1,766 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "fmt"
+ "net/http"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+ "github.com/mattermost/platform/utils"
+)
+
+func MakeDirectChannelVisible(channelId string) *model.AppError {
+ var members []model.ChannelMember
+ if result := <-Srv.Store.Channel().GetMembers(channelId); result.Err != nil {
+ return result.Err
+ } else {
+ members = result.Data.([]model.ChannelMember)
+ }
+
+ if len(members) != 2 {
+ return model.NewLocAppError("MakeDirectChannelVisible", "api.post.make_direct_channel_visible.get_2_members.error", map[string]interface{}{"ChannelId": channelId}, "")
+ }
+
+ // make sure the channel is visible to both members
+ for i, member := range members {
+ otherUserId := members[1-i].UserId
+
+ if result := <-Srv.Store.Preference().Get(member.UserId, model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW, otherUserId); result.Err != nil {
+ // create a new preference since one doesn't exist yet
+ preference := &model.Preference{
+ UserId: member.UserId,
+ Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
+ Name: otherUserId,
+ Value: "true",
+ }
+
+ if saveResult := <-Srv.Store.Preference().Save(&model.Preferences{*preference}); saveResult.Err != nil {
+ return saveResult.Err
+ } else {
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCE_CHANGED, "", "", member.UserId, nil)
+ message.Add("preference", preference.ToJson())
+
+ go Publish(message)
+ }
+ } else {
+ preference := result.Data.(model.Preference)
+
+ if preference.Value != "true" {
+ // update the existing preference to make the channel visible
+ preference.Value = "true"
+
+ if updateResult := <-Srv.Store.Preference().Save(&model.Preferences{preference}); updateResult.Err != nil {
+ return updateResult.Err
+ } else {
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_PREFERENCE_CHANGED, "", "", member.UserId, nil)
+ message.Add("preference", preference.ToJson())
+
+ go Publish(message)
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+func CreateDefaultChannels(teamId string) ([]*model.Channel, *model.AppError) {
+ townSquare := &model.Channel{DisplayName: utils.T("api.channel.create_default_channels.town_square"), Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: teamId}
+
+ if _, err := CreateChannel(townSquare, false); err != nil {
+ return nil, err
+ }
+
+ offTopic := &model.Channel{DisplayName: utils.T("api.channel.create_default_channels.off_topic"), Name: "off-topic", Type: model.CHANNEL_OPEN, TeamId: teamId}
+
+ if _, err := CreateChannel(offTopic, false); err != nil {
+ return nil, err
+ }
+
+ channels := []*model.Channel{townSquare, offTopic}
+ return channels, nil
+}
+
+func JoinDefaultChannels(teamId string, user *model.User, channelRole string) *model.AppError {
+ var err *model.AppError = nil
+
+ if result := <-Srv.Store.Channel().GetByName(teamId, "town-square"); result.Err != nil {
+ err = result.Err
+ } else {
+ cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id,
+ Roles: channelRole, NotifyProps: model.GetDefaultChannelNotifyProps()}
+
+ if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil {
+ err = cmResult.Err
+ }
+
+ post := &model.Post{
+ ChannelId: result.Data.(*model.Channel).Id,
+ Message: fmt.Sprintf(utils.T("api.channel.join_channel.post_and_forget"), user.Username),
+ Type: model.POST_JOIN_LEAVE,
+ UserId: user.Id,
+ }
+
+ InvalidateCacheForChannel(result.Data.(*model.Channel).Id)
+
+ if _, err := CreatePost(post, teamId, false); err != nil {
+ l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err)
+ }
+ }
+
+ if result := <-Srv.Store.Channel().GetByName(teamId, "off-topic"); result.Err != nil {
+ err = result.Err
+ } else {
+ cm := &model.ChannelMember{ChannelId: result.Data.(*model.Channel).Id, UserId: user.Id,
+ Roles: channelRole, NotifyProps: model.GetDefaultChannelNotifyProps()}
+
+ if cmResult := <-Srv.Store.Channel().SaveMember(cm); cmResult.Err != nil {
+ err = cmResult.Err
+ }
+
+ post := &model.Post{
+ ChannelId: result.Data.(*model.Channel).Id,
+ Message: fmt.Sprintf(utils.T("api.channel.join_channel.post_and_forget"), user.Username),
+ Type: model.POST_JOIN_LEAVE,
+ UserId: user.Id,
+ }
+
+ InvalidateCacheForChannel(result.Data.(*model.Channel).Id)
+
+ if _, err := CreatePost(post, teamId, false); err != nil {
+ l4g.Error(utils.T("api.channel.post_user_add_remove_message_and_forget.error"), err)
+ }
+ }
+
+ return err
+}
+
+func CreateChannel(channel *model.Channel, addMember bool) (*model.Channel, *model.AppError) {
+ if result := <-Srv.Store.Channel().Save(channel); result.Err != nil {
+ return nil, result.Err
+ } else {
+ sc := result.Data.(*model.Channel)
+
+ if addMember {
+ cm := &model.ChannelMember{
+ ChannelId: sc.Id,
+ UserId: channel.CreatorId,
+ Roles: model.ROLE_CHANNEL_USER.Id + " " + model.ROLE_CHANNEL_ADMIN.Id,
+ NotifyProps: model.GetDefaultChannelNotifyProps(),
+ }
+
+ if cmresult := <-Srv.Store.Channel().SaveMember(cm); cmresult.Err != nil {
+ return nil, cmresult.Err
+ }
+
+ InvalidateCacheForUser(channel.CreatorId)
+ }
+
+ return sc, nil
+ }
+}
+
+func CreateDirectChannel(userId string, otherUserId string) (*model.Channel, *model.AppError) {
+ uc := Srv.Store.User().Get(otherUserId)
+
+ if uresult := <-uc; uresult.Err != nil {
+ return nil, model.NewLocAppError("CreateDirectChannel", "api.channel.create_direct_channel.invalid_user.app_error", nil, otherUserId)
+ }
+
+ if result := <-Srv.Store.Channel().CreateDirectChannel(userId, otherUserId); result.Err != nil {
+ if result.Err.Id == store.CHANNEL_EXISTS_ERROR {
+ return result.Data.(*model.Channel), nil
+ } else {
+ return nil, result.Err
+ }
+ } else {
+ channel := result.Data.(*model.Channel)
+
+ InvalidateCacheForUser(userId)
+ InvalidateCacheForUser(otherUserId)
+
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_DIRECT_ADDED, "", channel.Id, "", nil)
+ message.Add("teammate_id", otherUserId)
+ Publish(message)
+
+ return channel, nil
+ }
+}
+
+func UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppError) {
+ if result := <-Srv.Store.Channel().Update(channel); result.Err != nil {
+ return nil, result.Err
+ } else {
+ InvalidateCacheForChannel(channel.Id)
+ return channel, nil
+ }
+}
+
+func UpdateChannelMemberRoles(channelId string, userId string, newRoles string) (*model.ChannelMember, *model.AppError) {
+ var member *model.ChannelMember
+ var err *model.AppError
+ if member, err = GetChannelMember(channelId, userId); err != nil {
+ return nil, err
+ }
+
+ member.Roles = newRoles
+
+ if result := <-Srv.Store.Channel().UpdateMember(member); result.Err != nil {
+ return nil, result.Err
+ }
+
+ InvalidateCacheForUser(userId)
+ return member, nil
+}
+
+func UpdateChannelMemberNotifyProps(data map[string]string, channelId string, userId string) (*model.ChannelMember, *model.AppError) {
+ var member *model.ChannelMember
+ var err *model.AppError
+ if member, err = GetChannelMember(channelId, userId); err != nil {
+ return nil, err
+ }
+
+ // update whichever notify properties have been provided, but don't change the others
+ if markUnread, exists := data["mark_unread"]; exists {
+ member.NotifyProps["mark_unread"] = markUnread
+ }
+
+ if desktop, exists := data["desktop"]; exists {
+ member.NotifyProps["desktop"] = desktop
+ }
+
+ if result := <-Srv.Store.Channel().UpdateMember(member); result.Err != nil {
+ return nil, result.Err
+ } else {
+ InvalidateCacheForUser(userId)
+ return member, nil
+ }
+}
+
+func DeleteChannel(channel *model.Channel, userId string) *model.AppError {
+ uc := Srv.Store.User().Get(userId)
+ ihc := Srv.Store.Webhook().GetIncomingByChannel(channel.Id)
+ ohc := Srv.Store.Webhook().GetOutgoingByChannel(channel.Id)
+
+ if uresult := <-uc; uresult.Err != nil {
+ return uresult.Err
+ } else if ihcresult := <-ihc; ihcresult.Err != nil {
+ return ihcresult.Err
+ } else if ohcresult := <-ohc; ohcresult.Err != nil {
+ return ohcresult.Err
+ } else {
+ user := uresult.Data.(*model.User)
+ incomingHooks := ihcresult.Data.([]*model.IncomingWebhook)
+ outgoingHooks := ohcresult.Data.([]*model.OutgoingWebhook)
+
+ if channel.DeleteAt > 0 {
+ err := model.NewLocAppError("deleteChannel", "api.channel.delete_channel.deleted.app_error", nil, "")
+ err.StatusCode = http.StatusBadRequest
+ return err
+ }
+
+ if channel.Name == model.DEFAULT_CHANNEL {
+ err := model.NewLocAppError("deleteChannel", "api.channel.delete_channel.cannot.app_error", map[string]interface{}{"Channel": model.DEFAULT_CHANNEL}, "")
+ err.StatusCode = http.StatusBadRequest
+ return err
+ }
+
+ T := utils.GetUserTranslations(user.Locale)
+
+ post := &model.Post{
+ ChannelId: channel.Id,
+ Message: fmt.Sprintf(T("api.channel.delete_channel.archived"), user.Username),
+ Type: model.POST_CHANNEL_DELETED,
+ UserId: userId,
+ }
+
+ if _, err := CreatePost(post, channel.TeamId, false); err != nil {
+ l4g.Error(utils.T("api.channel.delete_channel.failed_post.error"), err)
+ }
+
+ now := model.GetMillis()
+ for _, hook := range incomingHooks {
+ if result := <-Srv.Store.Webhook().DeleteIncoming(hook.Id, now); result.Err != nil {
+ l4g.Error(utils.T("api.channel.delete_channel.incoming_webhook.error"), hook.Id)
+ }
+ }
+
+ for _, hook := range outgoingHooks {
+ if result := <-Srv.Store.Webhook().DeleteOutgoing(hook.Id, now); result.Err != nil {
+ l4g.Error(utils.T("api.channel.delete_channel.outgoing_webhook.error"), hook.Id)
+ }
+ }
+
+ if dresult := <-Srv.Store.Channel().Delete(channel.Id, model.GetMillis()); dresult.Err != nil {
+ return dresult.Err
+ }
+ InvalidateCacheForChannel(channel.Id)
+
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_DELETED, channel.TeamId, "", "", nil)
+ message.Add("channel_id", channel.Id)
+
+ Publish(message)
+ }
+
+ return nil
+}
+
+func AddUserToChannel(user *model.User, channel *model.Channel) (*model.ChannelMember, *model.AppError) {
+ if channel.DeleteAt > 0 {
+ return nil, model.NewLocAppError("AddUserToChannel", "api.channel.add_user_to_channel.deleted.app_error", nil, "")
+ }
+
+ if channel.Type != model.CHANNEL_OPEN && channel.Type != model.CHANNEL_PRIVATE {
+ return nil, model.NewLocAppError("AddUserToChannel", "api.channel.add_user_to_channel.type.app_error", nil, "")
+ }
+
+ tmchan := Srv.Store.Team().GetMember(channel.TeamId, user.Id)
+ cmchan := Srv.Store.Channel().GetMember(channel.Id, user.Id)
+
+ if result := <-tmchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ teamMember := result.Data.(*model.TeamMember)
+ if teamMember.DeleteAt > 0 {
+ return nil, model.NewLocAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.deleted.app_error", nil, "")
+ }
+ }
+
+ if result := <-cmchan; result.Err != nil {
+ if result.Err.Id != store.MISSING_CHANNEL_MEMBER_ERROR {
+ return nil, result.Err
+ }
+ } else {
+ channelMember := result.Data.(*model.ChannelMember)
+ return channelMember, nil
+ }
+
+ newMember := &model.ChannelMember{
+ ChannelId: channel.Id,
+ UserId: user.Id,
+ NotifyProps: model.GetDefaultChannelNotifyProps(),
+ Roles: model.ROLE_CHANNEL_USER.Id,
+ }
+ if result := <-Srv.Store.Channel().SaveMember(newMember); result.Err != nil {
+ l4g.Error("Failed to add member user_id=%v channel_id=%v err=%v", user.Id, channel.Id, result.Err)
+ return nil, model.NewLocAppError("AddUserToChannel", "api.channel.add_user.to.channel.failed.app_error", nil, "")
+ }
+
+ InvalidateCacheForUser(user.Id)
+ InvalidateCacheForChannel(channel.Id)
+
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_ADDED, "", channel.Id, "", nil)
+ message.Add("user_id", user.Id)
+ message.Add("team_id", channel.TeamId)
+ Publish(message)
+
+ return newMember, nil
+}
+
+func AddDirectChannels(teamId string, user *model.User) *model.AppError {
+ var profiles map[string]*model.User
+ if result := <-Srv.Store.User().GetProfiles(teamId, 0, 100); result.Err != nil {
+ return model.NewLocAppError("AddDirectChannels", "api.user.add_direct_channels_and_forget.failed.error", map[string]interface{}{"UserId": user.Id, "TeamId": teamId, "Error": result.Err.Error()}, "")
+ } else {
+ profiles = result.Data.(map[string]*model.User)
+ }
+
+ var preferences model.Preferences
+
+ for id := range profiles {
+ if id == user.Id {
+ continue
+ }
+
+ profile := profiles[id]
+
+ preference := model.Preference{
+ UserId: user.Id,
+ Category: model.PREFERENCE_CATEGORY_DIRECT_CHANNEL_SHOW,
+ Name: profile.Id,
+ Value: "true",
+ }
+
+ preferences = append(preferences, preference)
+
+ if len(preferences) >= 10 {
+ break
+ }
+ }
+
+ if result := <-Srv.Store.Preference().Save(&preferences); result.Err != nil {
+ return model.NewLocAppError("AddDirectChannels", "api.user.add_direct_channels_and_forget.failed.error", map[string]interface{}{"UserId": user.Id, "TeamId": teamId, "Error": result.Err.Error()}, "")
+ }
+
+ return nil
+}
+
+func PostUpdateChannelHeaderMessage(userId string, channelId string, teamId string, oldChannelHeader, newChannelHeader string) *model.AppError {
+ uc := Srv.Store.User().Get(userId)
+
+ if uresult := <-uc; uresult.Err != nil {
+ return model.NewLocAppError("PostUpdateChannelHeaderMessage", "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error", nil, uresult.Err.Error())
+ } else {
+ user := uresult.Data.(*model.User)
+
+ var message string
+ if oldChannelHeader == "" {
+ message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.updated_to"), user.Username, newChannelHeader)
+ } else if newChannelHeader == "" {
+ message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.removed"), user.Username, oldChannelHeader)
+ } else {
+ message = fmt.Sprintf(utils.T("api.channel.post_update_channel_header_message_and_forget.updated_from"), user.Username, oldChannelHeader, newChannelHeader)
+ }
+
+ post := &model.Post{
+ ChannelId: channelId,
+ Message: message,
+ Type: model.POST_HEADER_CHANGE,
+ UserId: userId,
+ Props: model.StringInterface{
+ "old_header": oldChannelHeader,
+ "new_header": newChannelHeader,
+ },
+ }
+
+ if _, err := CreatePost(post, teamId, false); err != nil {
+ return model.NewLocAppError("", "api.channel.post_update_channel_header_message_and_forget.post.error", nil, err.Error())
+ }
+ }
+
+ return nil
+}
+
+func PostUpdateChannelPurposeMessage(userId string, channelId string, teamId string, oldChannelPurpose string, newChannelPurpose string) *model.AppError {
+ uc := Srv.Store.User().Get(userId)
+
+ if uresult := <-uc; uresult.Err != nil {
+ return model.NewLocAppError("PostUpdateChannelPurposeMessage", "app.channel.post_update_channel_purpose_message.retrieve_user.error", nil, uresult.Err.Error())
+ } else {
+ user := uresult.Data.(*model.User)
+
+ var message string
+ if oldChannelPurpose == "" {
+ message = fmt.Sprintf(utils.T("app.channel.post_update_channel_purpose_message.updated_to"), user.Username, newChannelPurpose)
+ } else if newChannelPurpose == "" {
+ message = fmt.Sprintf(utils.T("app.channel.post_update_channel_purpose_message.removed"), user.Username, oldChannelPurpose)
+ } else {
+ message = fmt.Sprintf(utils.T("app.channel.post_update_channel_purpose_message.updated_from"), user.Username, oldChannelPurpose, newChannelPurpose)
+ }
+
+ post := &model.Post{
+ ChannelId: channelId,
+ Message: message,
+ Type: model.POST_PURPOSE_CHANGE,
+ UserId: userId,
+ Props: model.StringInterface{
+ "old_purpose": oldChannelPurpose,
+ "new_purpose": newChannelPurpose,
+ },
+ }
+ if _, err := CreatePost(post, teamId, false); err != nil {
+ return model.NewLocAppError("", "app.channel.post_update_channel_purpose_message.post.error", nil, err.Error())
+ }
+ }
+
+ return nil
+}
+
+func PostUpdateChannelDisplayNameMessage(userId string, channelId string, teamId string, oldChannelDisplayName, newChannelDisplayName string) *model.AppError {
+ uc := Srv.Store.User().Get(userId)
+
+ if uresult := <-uc; uresult.Err != nil {
+ return model.NewLocAppError("PostUpdateChannelDisplayNameMessage", "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error", nil, uresult.Err.Error())
+ } else {
+ user := uresult.Data.(*model.User)
+
+ message := fmt.Sprintf(utils.T("api.channel.post_update_channel_displayname_message_and_forget.updated_from"), user.Username, oldChannelDisplayName, newChannelDisplayName)
+
+ post := &model.Post{
+ ChannelId: channelId,
+ Message: message,
+ Type: model.POST_DISPLAYNAME_CHANGE,
+ UserId: userId,
+ Props: model.StringInterface{
+ "old_displayname": oldChannelDisplayName,
+ "new_displayname": newChannelDisplayName,
+ },
+ }
+
+ if _, err := CreatePost(post, teamId, false); err != nil {
+ return model.NewLocAppError("PostUpdateChannelDisplayNameMessage", "api.channel.post_update_channel_displayname_message_and_forget.create_post.error", nil, err.Error())
+ }
+ }
+
+ return nil
+}
+
+func GetChannel(channelId string) (*model.Channel, *model.AppError) {
+ if result := <-Srv.Store.Channel().Get(channelId, true); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.Channel), nil
+ }
+}
+
+func GetChannelByName(channelName, teamId string) (*model.Channel, *model.AppError) {
+ if result := <-Srv.Store.Channel().GetByName(teamId, channelName); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.Channel), nil
+ }
+}
+
+func GetChannelsForUser(teamId string, userId string) (*model.ChannelList, *model.AppError) {
+ if result := <-Srv.Store.Channel().GetChannels(teamId, userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.ChannelList), nil
+ }
+}
+
+func GetChannelsUserNotIn(teamId string, userId string, offset int, limit int) (*model.ChannelList, *model.AppError) {
+ if result := <-Srv.Store.Channel().GetMoreChannels(teamId, userId, offset, limit); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.ChannelList), nil
+ }
+}
+
+func GetChannelMember(channelId string, userId string) (*model.ChannelMember, *model.AppError) {
+ if result := <-Srv.Store.Channel().GetMember(channelId, userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.ChannelMember), nil
+ }
+}
+
+func GetChannelMembersByIds(channelId string, userIds []string) (*model.ChannelMembers, *model.AppError) {
+ if result := <-Srv.Store.Channel().GetMembersByIds(channelId, userIds); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.ChannelMembers), nil
+ }
+}
+
+func GetChannelMembersForUser(teamId string, userId string) (*model.ChannelMembers, *model.AppError) {
+ if result := <-Srv.Store.Channel().GetMembersForUser(teamId, userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.ChannelMembers), nil
+ }
+}
+
+func GetChannelMemberCount(channelId string) (int64, *model.AppError) {
+ if result := <-Srv.Store.Channel().GetMemberCount(channelId, true); result.Err != nil {
+ return 0, result.Err
+ } else {
+ return result.Data.(int64), nil
+ }
+}
+
+func GetChannelCounts(teamId string, userId string) (*model.ChannelCounts, *model.AppError) {
+ if result := <-Srv.Store.Channel().GetChannelCounts(teamId, userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.ChannelCounts), nil
+ }
+}
+
+func JoinChannel(channel *model.Channel, userId string) *model.AppError {
+ userChan := Srv.Store.User().Get(userId)
+ memberChan := Srv.Store.Channel().GetMember(channel.Id, userId)
+
+ if uresult := <-userChan; uresult.Err != nil {
+ return uresult.Err
+ } else if mresult := <-memberChan; mresult.Err == nil && mresult.Data != nil {
+ // user is already in the channel
+ return nil
+ } else {
+ user := uresult.Data.(*model.User)
+
+ if channel.Type == model.CHANNEL_OPEN {
+ if _, err := AddUserToChannel(user, channel); err != nil {
+ return err
+ }
+ PostUserAddRemoveMessage(userId, channel.Id, channel.TeamId, fmt.Sprintf(utils.T("api.channel.join_channel.post_and_forget"), user.Username), model.POST_JOIN_LEAVE)
+ } else {
+ return model.NewLocAppError("JoinChannel", "api.channel.join_channel.permissions.app_error", nil, "")
+ }
+ }
+
+ return nil
+}
+
+func LeaveChannel(channelId string, userId string) *model.AppError {
+ sc := Srv.Store.Channel().Get(channelId, true)
+ uc := Srv.Store.User().Get(userId)
+ ccm := Srv.Store.Channel().GetMemberCount(channelId, false)
+
+ if cresult := <-sc; cresult.Err != nil {
+ return cresult.Err
+ } else if uresult := <-uc; uresult.Err != nil {
+ return cresult.Err
+ } else if ccmresult := <-ccm; ccmresult.Err != nil {
+ return ccmresult.Err
+ } else {
+ channel := cresult.Data.(*model.Channel)
+ user := uresult.Data.(*model.User)
+ membersCount := ccmresult.Data.(int64)
+
+ if channel.Type == model.CHANNEL_DIRECT {
+ err := model.NewLocAppError("LeaveChannel", "api.channel.leave.direct.app_error", nil, "")
+ err.StatusCode = http.StatusBadRequest
+ return err
+ }
+
+ if channel.Type == model.CHANNEL_PRIVATE && membersCount == 1 {
+ err := model.NewLocAppError("LeaveChannel", "api.channel.leave.last_member.app_error", nil, "userId="+user.Id)
+ err.StatusCode = http.StatusBadRequest
+ return err
+ }
+
+ if err := RemoveUserFromChannel(userId, userId, channel); err != nil {
+ return err
+ }
+
+ go PostUserAddRemoveMessage(userId, channel.Id, channel.TeamId, fmt.Sprintf(utils.T("api.channel.leave.left"), user.Username), model.POST_JOIN_LEAVE)
+ }
+
+ return nil
+}
+
+func RemoveUserFromChannel(userIdToRemove string, removerUserId string, channel *model.Channel) *model.AppError {
+ if channel.DeleteAt > 0 {
+ err := model.NewLocAppError("RemoveUserFromChannel", "api.channel.remove_user_from_channel.deleted.app_error", nil, "")
+ err.StatusCode = http.StatusBadRequest
+ return err
+ }
+
+ if channel.Name == model.DEFAULT_CHANNEL {
+ return model.NewLocAppError("RemoveUserFromChannel", "api.channel.remove.default.app_error", map[string]interface{}{"Channel": model.DEFAULT_CHANNEL}, "")
+ }
+
+ if cmresult := <-Srv.Store.Channel().RemoveMember(channel.Id, userIdToRemove); cmresult.Err != nil {
+ return cmresult.Err
+ }
+
+ InvalidateCacheForUser(userIdToRemove)
+ InvalidateCacheForChannel(channel.Id)
+
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_REMOVED, "", channel.Id, "", nil)
+ message.Add("user_id", userIdToRemove)
+ message.Add("remover_id", removerUserId)
+ go Publish(message)
+
+ // because the removed user no longer belongs to the channel we need to send a separate websocket event
+ userMsg := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_REMOVED, "", "", userIdToRemove, nil)
+ userMsg.Add("channel_id", channel.Id)
+ userMsg.Add("remover_id", removerUserId)
+ go Publish(userMsg)
+
+ return nil
+}
+
+func PostUserAddRemoveMessage(userId, channelId, teamId, message, postType string) *model.AppError {
+ post := &model.Post{
+ ChannelId: channelId,
+ Message: message,
+ Type: postType,
+ UserId: userId,
+ }
+ if _, err := CreatePost(post, teamId, false); err != nil {
+ return model.NewLocAppError("PostUserAddRemoveMessage", "api.channel.post_user_add_remove_message_and_forget.error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func GetNumberOfChannelsOnTeam(teamId string) (int, *model.AppError) {
+ // Get total number of channels on current team
+ if result := <-Srv.Store.Channel().GetTeamChannels(teamId); result.Err != nil {
+ return 0, result.Err
+ } else {
+ return len(*result.Data.(*model.ChannelList)), nil
+ }
+}
+
+func SetActiveChannel(userId string, channelId string) *model.AppError {
+ status, err := GetStatus(userId)
+ if err != nil {
+ status = &model.Status{userId, model.STATUS_ONLINE, false, model.GetMillis(), channelId}
+ } else {
+ status.ActiveChannel = channelId
+ if !status.Manual {
+ status.Status = model.STATUS_ONLINE
+ }
+ status.LastActivityAt = model.GetMillis()
+ }
+
+ AddStatusCache(status)
+
+ return nil
+}
+
+func UpdateChannelLastViewedAt(channelIds []string, userId string) *model.AppError {
+ if result := <-Srv.Store.Channel().UpdateLastViewedAt(channelIds, userId); result.Err != nil {
+ return result.Err
+ }
+
+ return nil
+}
+
+func SearchChannels(teamId string, term string) (*model.ChannelList, *model.AppError) {
+ if result := <-Srv.Store.Channel().SearchInTeam(teamId, term); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.ChannelList), nil
+ }
+}
+
+func SearchChannelsUserNotIn(teamId string, userId string, term string) (*model.ChannelList, *model.AppError) {
+ if result := <-Srv.Store.Channel().SearchMore(userId, teamId, term); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.ChannelList), nil
+ }
+}
+
+func ViewChannel(view *model.ChannelView, teamId string, userId string, clearPushNotifications bool) *model.AppError {
+ channelIds := []string{view.ChannelId}
+
+ var pchan store.StoreChannel
+ if len(view.PrevChannelId) > 0 {
+ channelIds = append(channelIds, view.PrevChannelId)
+
+ if *utils.Cfg.EmailSettings.SendPushNotifications && clearPushNotifications {
+ pchan = Srv.Store.User().GetUnreadCountForChannel(userId, view.ChannelId)
+ }
+ }
+
+ uchan := Srv.Store.Channel().UpdateLastViewedAt(channelIds, userId)
+
+ if pchan != nil {
+ if result := <-pchan; result.Err != nil {
+ return result.Err
+ } else {
+ if result.Data.(int64) > 0 {
+ ClearPushNotification(userId, view.ChannelId)
+ }
+ }
+ }
+
+ if result := <-uchan; result.Err != nil {
+ return result.Err
+ }
+
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_CHANNEL_VIEWED, teamId, "", userId, nil)
+ message.Add("channel_id", view.ChannelId)
+ go Publish(message)
+
+ return nil
+}
diff --git a/app/command.go b/app/command.go
new file mode 100644
index 000000000..2d5861206
--- /dev/null
+++ b/app/command.go
@@ -0,0 +1,31 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "github.com/mattermost/platform/model"
+)
+
+func CreateCommandPost(post *model.Post, teamId string, response *model.CommandResponse) (*model.Post, *model.AppError) {
+ post.Message = parseSlackLinksToMarkdown(response.Text)
+ post.CreateAt = model.GetMillis()
+
+ if response.Attachments != nil {
+ parseSlackAttachment(post, response.Attachments)
+ }
+
+ switch response.ResponseType {
+ case model.COMMAND_RESPONSE_TYPE_IN_CHANNEL:
+ return CreatePost(post, teamId, true)
+ case model.COMMAND_RESPONSE_TYPE_EPHEMERAL:
+ if response.Text == "" {
+ return post, nil
+ }
+
+ post.ParentId = ""
+ SendEphemeralPost(teamId, post.UserId, post)
+ }
+
+ return post, nil
+}
diff --git a/app/compliance.go b/app/compliance.go
new file mode 100644
index 000000000..ffef69b44
--- /dev/null
+++ b/app/compliance.go
@@ -0,0 +1,62 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "io/ioutil"
+
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func GetComplianceReports() (model.Compliances, *model.AppError) {
+ if !*utils.Cfg.ComplianceSettings.Enable || !utils.IsLicensed || !*utils.License.Features.Compliance {
+ return nil, model.NewLocAppError("GetComplianceReports", "ent.compliance.licence_disable.app_error", nil, "")
+ }
+
+ if result := <-Srv.Store.Compliance().GetAll(); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(model.Compliances), nil
+ }
+}
+
+func SaveComplianceReport(job *model.Compliance) (*model.Compliance, *model.AppError) {
+ if !*utils.Cfg.ComplianceSettings.Enable || !utils.IsLicensed || !*utils.License.Features.Compliance || einterfaces.GetComplianceInterface() == nil {
+ return nil, model.NewLocAppError("saveComplianceReport", "ent.compliance.licence_disable.app_error", nil, "")
+ }
+
+ job.Type = model.COMPLIANCE_TYPE_ADHOC
+
+ if result := <-Srv.Store.Compliance().Save(job); result.Err != nil {
+ return nil, result.Err
+ } else {
+ job = result.Data.(*model.Compliance)
+ go einterfaces.GetComplianceInterface().RunComplianceJob(job)
+ }
+
+ return job, nil
+}
+
+func GetComplianceReport(reportId string) (*model.Compliance, *model.AppError) {
+ if !*utils.Cfg.ComplianceSettings.Enable || !utils.IsLicensed || !*utils.License.Features.Compliance || einterfaces.GetComplianceInterface() == nil {
+ return nil, model.NewLocAppError("downloadComplianceReport", "ent.compliance.licence_disable.app_error", nil, "")
+ }
+
+ if result := <-Srv.Store.Compliance().Get(reportId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.Compliance), nil
+ }
+}
+
+func GetComplianceFile(job *model.Compliance) ([]byte, *model.AppError) {
+ if f, err := ioutil.ReadFile(*utils.Cfg.ComplianceSettings.Directory + "compliance/" + job.JobName() + ".zip"); err != nil {
+ return nil, model.NewLocAppError("readFile", "api.file.read_file.reading_local.app_error", nil, err.Error())
+
+ } else {
+ return f, nil
+ }
+}
diff --git a/app/email.go b/app/email.go
new file mode 100644
index 000000000..007a24505
--- /dev/null
+++ b/app/email.go
@@ -0,0 +1,236 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "fmt"
+ "html/template"
+ "net/url"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func SendChangeUsernameEmail(oldUsername, newUsername, email, locale, siteURL string) *model.AppError {
+ T := utils.GetUserTranslations(locale)
+
+ subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, T("api.templates.username_change_subject",
+ map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName}))
+
+ bodyPage := utils.NewHTMLTemplate("email_change_body", locale)
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = T("api.templates.username_change_body.title")
+ bodyPage.Html["Info"] = template.HTML(T("api.templates.username_change_body.info",
+ map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "NewUsername": newUsername}))
+
+ if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil {
+ return model.NewLocAppError("SendChangeUsernameEmail", "api.user.send_email_change_username_and_forget.error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func SendEmailChangeVerifyEmail(userId, newUserEmail, locale, siteURL string) *model.AppError {
+ T := utils.GetUserTranslations(locale)
+
+ link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashPassword(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(newUserEmail))
+
+ subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, T("api.templates.email_change_verify_subject",
+ map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName}))
+
+ bodyPage := utils.NewHTMLTemplate("email_change_verify_body", locale)
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = T("api.templates.email_change_verify_body.title")
+ bodyPage.Props["Info"] = T("api.templates.email_change_verify_body.info",
+ map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName})
+ bodyPage.Props["VerifyUrl"] = link
+ bodyPage.Props["VerifyButton"] = T("api.templates.email_change_verify_body.button")
+
+ if err := utils.SendMail(newUserEmail, subject, bodyPage.Render()); err != nil {
+ return model.NewLocAppError("SendEmailChangeVerifyEmail", "api.user.send_email_change_verify_email_and_forget.error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func SendEmailChangeEmail(oldEmail, newEmail, locale, siteURL string) *model.AppError {
+ T := utils.GetUserTranslations(locale)
+
+ subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, T("api.templates.email_change_subject",
+ map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName}))
+
+ bodyPage := utils.NewHTMLTemplate("email_change_body", locale)
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = T("api.templates.email_change_body.title")
+ bodyPage.Html["Info"] = template.HTML(T("api.templates.email_change_body.info",
+ map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "NewEmail": newEmail}))
+
+ if err := utils.SendMail(oldEmail, subject, bodyPage.Render()); err != nil {
+ return model.NewLocAppError("SendEmailChangeEmail", "api.user.send_email_change_email_and_forget.error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func SendVerifyEmail(userId, userEmail, locale, siteURL string) *model.AppError {
+ T := utils.GetUserTranslations(locale)
+
+ link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashPassword(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(userEmail))
+
+ url, _ := url.Parse(siteURL)
+
+ subject := T("api.templates.verify_subject",
+ map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
+
+ bodyPage := utils.NewHTMLTemplate("verify_body", locale)
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = T("api.templates.verify_body.title", map[string]interface{}{"ServerURL": url.Host})
+ bodyPage.Props["Info"] = T("api.templates.verify_body.info")
+ bodyPage.Props["VerifyUrl"] = link
+ bodyPage.Props["Button"] = T("api.templates.verify_body.button")
+
+ if err := utils.SendMail(userEmail, subject, bodyPage.Render()); err != nil {
+ return model.NewLocAppError("SendVerifyEmail", "api.user.send_verify_email_and_forget.failed.error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func SendSignInChangeEmail(email, method, locale, siteURL string) *model.AppError {
+ T := utils.GetUserTranslations(locale)
+
+ subject := T("api.templates.singin_change_email.subject",
+ map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"]})
+
+ bodyPage := utils.NewHTMLTemplate("signin_change_body", locale)
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = T("api.templates.signin_change_email.body.title")
+ bodyPage.Html["Info"] = template.HTML(T("api.templates.singin_change_email.body.info",
+ map[string]interface{}{"SiteName": utils.ClientCfg["SiteName"], "Method": method}))
+
+ if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil {
+ return model.NewLocAppError("SendSignInChangeEmail", "api.user.send_sign_in_change_email_and_forget.error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func SendWelcomeEmail(userId string, email string, verified bool, locale, siteURL string) *model.AppError {
+ T := utils.GetUserTranslations(locale)
+
+ rawUrl, _ := url.Parse(siteURL)
+
+ subject := T("api.templates.welcome_subject", map[string]interface{}{"ServerURL": rawUrl.Host})
+
+ bodyPage := utils.NewHTMLTemplate("welcome_body", locale)
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = T("api.templates.welcome_body.title", map[string]interface{}{"ServerURL": rawUrl.Host})
+ bodyPage.Props["Info"] = T("api.templates.welcome_body.info")
+ bodyPage.Props["Button"] = T("api.templates.welcome_body.button")
+ bodyPage.Props["Info2"] = T("api.templates.welcome_body.info2")
+ bodyPage.Props["Info3"] = T("api.templates.welcome_body.info3")
+ bodyPage.Props["SiteURL"] = siteURL
+
+ if *utils.Cfg.NativeAppSettings.AppDownloadLink != "" {
+ bodyPage.Props["AppDownloadInfo"] = T("api.templates.welcome_body.app_download_info")
+ bodyPage.Props["AppDownloadLink"] = *utils.Cfg.NativeAppSettings.AppDownloadLink
+ }
+
+ if !verified {
+ link := fmt.Sprintf("%s/do_verify_email?uid=%s&hid=%s&email=%s", siteURL, userId, model.HashPassword(userId+utils.Cfg.EmailSettings.InviteSalt), url.QueryEscape(email))
+ bodyPage.Props["VerifyUrl"] = link
+ }
+
+ if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil {
+ return model.NewLocAppError("SendWelcomeEmail", "api.user.send_welcome_email_and_forget.failed.error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func SendPasswordChangeEmail(email, method, locale, siteURL string) *model.AppError {
+ T := utils.GetUserTranslations(locale)
+
+ subject := T("api.templates.password_change_subject",
+ map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "SiteName": utils.Cfg.TeamSettings.SiteName})
+
+ bodyPage := utils.NewHTMLTemplate("password_change_body", locale)
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = T("api.templates.password_change_body.title")
+ bodyPage.Html["Info"] = template.HTML(T("api.templates.password_change_body.info",
+ map[string]interface{}{"TeamDisplayName": utils.Cfg.TeamSettings.SiteName, "TeamURL": siteURL, "Method": method}))
+
+ if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil {
+ return model.NewLocAppError("SendPasswordChangeEmail", "api.user.send_password_change_email_and_forget.error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func SendMfaChangeEmail(email string, activated bool, locale, siteURL string) *model.AppError {
+ T := utils.GetUserTranslations(locale)
+
+ subject := T("api.templates.mfa_change_subject",
+ map[string]interface{}{"SiteName": utils.Cfg.TeamSettings.SiteName})
+
+ bodyPage := utils.NewHTMLTemplate("mfa_change_body", locale)
+ bodyPage.Props["SiteURL"] = siteURL
+
+ bodyText := ""
+ if activated {
+ bodyText = "api.templates.mfa_activated_body.info"
+ bodyPage.Props["Title"] = T("api.templates.mfa_activated_body.title")
+ } else {
+ bodyText = "api.templates.mfa_deactivated_body.info"
+ bodyPage.Props["Title"] = T("api.templates.mfa_deactivated_body.title")
+ }
+
+ bodyPage.Html["Info"] = template.HTML(T(bodyText,
+ map[string]interface{}{"SiteURL": siteURL}))
+
+ if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil {
+ return model.NewLocAppError("SendMfaChangeEmail", "api.user.send_mfa_change_email.error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func SendInviteEmails(team *model.Team, senderName string, invites []string, siteURL string) {
+ for _, invite := range invites {
+ if len(invite) > 0 {
+ senderRole := utils.T("api.team.invite_members.member")
+
+ subject := utils.T("api.templates.invite_subject",
+ map[string]interface{}{"SenderName": senderName, "TeamDisplayName": team.DisplayName, "SiteName": utils.ClientCfg["SiteName"]})
+
+ bodyPage := utils.NewHTMLTemplate("invite_body", model.DEFAULT_LOCALE)
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = utils.T("api.templates.invite_body.title")
+ bodyPage.Html["Info"] = template.HTML(utils.T("api.templates.invite_body.info",
+ map[string]interface{}{"SenderStatus": senderRole, "SenderName": senderName, "TeamDisplayName": team.DisplayName}))
+ bodyPage.Props["Button"] = utils.T("api.templates.invite_body.button")
+ bodyPage.Html["ExtraInfo"] = template.HTML(utils.T("api.templates.invite_body.extra_info",
+ map[string]interface{}{"TeamDisplayName": team.DisplayName, "TeamURL": siteURL + "/" + team.Name}))
+
+ props := make(map[string]string)
+ props["email"] = invite
+ props["id"] = team.Id
+ props["display_name"] = team.DisplayName
+ props["name"] = team.Name
+ props["time"] = fmt.Sprintf("%v", model.GetMillis())
+ data := model.MapToJson(props)
+ hash := model.HashPassword(fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt))
+ bodyPage.Props["Link"] = fmt.Sprintf("%s/signup_user_complete/?d=%s&h=%s", siteURL, url.QueryEscape(data), url.QueryEscape(hash))
+
+ if !utils.Cfg.EmailSettings.SendEmailNotifications {
+ l4g.Info(utils.T("api.team.invite_members.sending.info"), invite, bodyPage.Props["Link"])
+ }
+
+ if err := utils.SendMail(invite, subject, bodyPage.Render()); err != nil {
+ l4g.Error(utils.T("api.team.invite_members.send.error"), err)
+ }
+ }
+ }
+}
diff --git a/api/email_batching.go b/app/email_batching.go
index 608d839da..fc2fb1cea 100644
--- a/api/email_batching.go
+++ b/app/email_batching.go
@@ -1,7 +1,7 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package api
+package app
import (
"database/sql"
@@ -217,7 +217,7 @@ func renderBatchedPost(template *utils.HTMLTemplate, post *model.Post, teamName
cchan := Srv.Store.Channel().Get(post.ChannelId, true)
template.Props["Button"] = translateFunc("api.email_batching.render_batched_post.go_to_post")
- template.Props["PostMessage"] = getMessageForNotification(post, translateFunc)
+ template.Props["PostMessage"] = GetMessageForNotification(post, translateFunc)
template.Props["PostLink"] = *utils.Cfg.ServiceSettings.SiteURL + "/" + teamName + "/pl/" + post.Id
tm := time.Unix(post.CreateAt/1000, 0)
diff --git a/api/email_batching_test.go b/app/email_batching_test.go
index d1619f912..23722facd 100644
--- a/api/email_batching_test.go
+++ b/app/email_batching_test.go
@@ -1,7 +1,7 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package api
+package app
import (
"testing"
diff --git a/app/email_test.go b/app/email_test.go
new file mode 100644
index 000000000..ecaa389bf
--- /dev/null
+++ b/app/email_test.go
@@ -0,0 +1,419 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/mattermost/platform/utils"
+)
+
+func TestSendChangeUsernameEmail(t *testing.T) {
+ Setup()
+
+ var emailTo string = "test@example.com"
+ var oldUsername string = "myoldusername"
+ var newUsername string = "fancyusername"
+ var locale string = "en"
+ var siteURL string = ""
+ var expectedPartialMessage string = "Your username for Mattermost has been changed to " + newUsername + "."
+ var expectedSubject string = "[" + utils.Cfg.TeamSettings.SiteName + "] Your username has changed for Mattermost"
+
+ //Delete all the messages before check the sample email
+ utils.DeleteMailBox(emailTo)
+
+ if err := SendChangeUsernameEmail(oldUsername, newUsername, emailTo, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(emailTo); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], emailTo) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(emailTo, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ }
+ }
+ }
+}
+
+func TestSendEmailChangeVerifyEmail(t *testing.T) {
+ Setup()
+ utils.LoadConfig("config.json")
+
+ var userId string = "5349853498543jdfvndf9834"
+ var newUserEmail string = "newtest@example.com"
+ var locale string = "en"
+ var siteURL string = ""
+ var expectedPartialMessage string = "You updated your email"
+ var expectedSubject string = "[" + utils.Cfg.TeamSettings.SiteName + "] Verify new email address for Mattermost"
+
+ //Delete all the messages before check the sample email
+ utils.DeleteMailBox(newUserEmail)
+
+ if err := SendEmailChangeVerifyEmail(userId, newUserEmail, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(newUserEmail); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], newUserEmail) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(newUserEmail, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, utils.UrlEncode(newUserEmail)) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong new email in the message")
+ }
+ }
+ }
+ }
+}
+
+func TestSendEmailChangeEmail(t *testing.T) {
+ Setup()
+ utils.LoadConfig("config.json")
+
+ var oldEmail string = "test@example.com"
+ var newUserEmail string = "newtest@example.com"
+ var locale string = "en"
+ var siteURL string = ""
+ var expectedPartialMessage string = "Your email address for Mattermost has been changed to " + newUserEmail
+ var expectedSubject string = "[" + utils.Cfg.TeamSettings.SiteName + "] Your email address has changed for Mattermost"
+
+ //Delete all the messages before check the sample email
+ utils.DeleteMailBox(oldEmail)
+
+ if err := SendEmailChangeEmail(oldEmail, newUserEmail, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(oldEmail); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], oldEmail) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(oldEmail, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ }
+ }
+ }
+}
+
+func TestSendVerifyEmail(t *testing.T) {
+ Setup()
+ utils.LoadConfig("config.json")
+
+ var userId string = "5349853498543jdfvndf9834"
+ var userEmail string = "test@example.com"
+ var locale string = "en"
+ var siteURL string = ""
+ var expectedPartialMessage string = "Please verify your email address by clicking below"
+ var expectedSubject string = "[" + utils.Cfg.TeamSettings.SiteName + "] Email Verification"
+
+ //Delete all the messages before check the sample email
+ utils.DeleteMailBox(userEmail)
+
+ if err := SendVerifyEmail(userId, userEmail, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(userEmail); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], userEmail) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(userEmail, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, utils.UrlEncode(userEmail)) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong new email in the message")
+ }
+ }
+ }
+ }
+}
+
+func TestSendSignInChangeEmail(t *testing.T) {
+ Setup()
+ utils.LoadConfig("config.json")
+
+ var email string = "test@example.com"
+ var locale string = "en"
+ var siteURL string = ""
+ var method string = "AD/LDAP"
+ var expectedPartialMessage string = "You updated your sign-in method on Mattermost to " + method + "."
+ var expectedSubject string = "You updated your sign-in method on Mattermost"
+
+ //Delete all the messages before check the sample email
+ utils.DeleteMailBox(email)
+
+ if err := SendSignInChangeEmail(email, method, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ }
+ }
+ }
+}
+
+func TestSendWelcomeEmail(t *testing.T) {
+ Setup()
+ utils.LoadConfig("config.json")
+
+ var userId string = "32432nkjnijn432uj32"
+ var email string = "test@example.com"
+ var locale string = "en"
+ var siteURL string = "http://test.mattermost.io"
+ var verified bool = true
+ var expectedPartialMessage string = "Mattermost lets you share messages and files from your PC or phone, with instant search and archiving"
+ var expectedSubject string = "You joined test.mattermost.io"
+
+ //Delete all the messages before check the sample email
+ utils.DeleteMailBox(email)
+
+ if err := SendWelcomeEmail(userId, email, verified, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ }
+ }
+ }
+
+ utils.DeleteMailBox(email)
+ verified = false
+ var expectedVerifyEmail string = "Please verify your email address by clicking below."
+
+ if err := SendWelcomeEmail(userId, email, verified, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedVerifyEmail) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, utils.UrlEncode(email)) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong email in the message")
+ }
+ }
+ }
+ }
+}
+
+func TestSendPasswordChangeEmail(t *testing.T) {
+ Setup()
+ utils.LoadConfig("config.json")
+
+ var email string = "test@example.com"
+ var locale string = "en"
+ var siteURL string = "http://test.mattermost.io"
+ var method string = "using a reset password link"
+ var expectedPartialMessage string = "Your password has been updated for " + utils.Cfg.TeamSettings.SiteName + " on " + siteURL + " by " + method
+ var expectedSubject string = "Your password has been updated for " + utils.Cfg.TeamSettings.SiteName + " on " + utils.Cfg.TeamSettings.SiteName
+
+ //Delete all the messages before check the sample email
+ utils.DeleteMailBox(email)
+
+ if err := SendPasswordChangeEmail(email, method, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ }
+ }
+ }
+}
+
+func TestSendMfaChangeEmail(t *testing.T) {
+ Setup()
+ utils.LoadConfig("config.json")
+
+ var email string = "test@example.com"
+ var locale string = "en"
+ var siteURL string = "http://test.mattermost.io"
+ var activated bool = true
+ var expectedPartialMessage string = "Multi-factor authentication has been added to your account on " + siteURL + "."
+ var expectedSubject string = "Your MFA has been updated on " + utils.Cfg.TeamSettings.SiteName
+
+ //Delete all the messages before check the sample email
+ utils.DeleteMailBox(email)
+
+ if err := SendMfaChangeEmail(email, activated, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ }
+ }
+ }
+
+ activated = false
+ expectedPartialMessage = "Multi-factor authentication has been removed from your account on " + siteURL + "."
+ utils.DeleteMailBox(email)
+
+ if err := SendMfaChangeEmail(email, activated, locale, siteURL); err != nil {
+ t.Log(err)
+ t.Fatal("Should send change username email")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := utils.GetMailBox(email); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(email, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ }
+ }
+ }
+}
+
+func TestSendInviteEmails(t *testing.T) {
+ th := Setup().InitBasic()
+ utils.LoadConfig("config.json")
+
+ var email1 string = "test1@example.com"
+ var email2 string = "test2@example.com"
+ var senderName string = "TheBoss"
+ var siteURL string = "http://test.mattermost.io"
+ invites := []string{email1, email2}
+ var expectedPartialMessage string = "The team member *" + senderName + "* , has invited you to join *" + th.BasicTeam.DisplayName + "*"
+ var expectedSubject string = senderName + " invited you to join " + th.BasicTeam.DisplayName + " Team on " + utils.Cfg.TeamSettings.SiteName
+
+ //Delete all the messages before check the sample email
+ utils.DeleteMailBox(email1)
+ utils.DeleteMailBox(email2)
+
+ SendInviteEmails(th.BasicTeam, senderName, invites, siteURL)
+
+ //Check if the email was send to the rigth email address to email1
+ if resultsMailbox, err := utils.GetMailBox(email1); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email1) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(email1, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Log(expectedSubject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ }
+ }
+
+ //Check if the email was send to the rigth email address to email2
+ if resultsMailbox, err := utils.GetMailBox(email2); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], email2) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := utils.GetMessageFromMailbox(email2, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Subject, expectedSubject) {
+ t.Log(resultsEmail.Subject)
+ t.Log(expectedSubject)
+ t.Fatal("Wrong Subject")
+ }
+ if !strings.Contains(resultsEmail.Body.Text, expectedPartialMessage) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Wrong Body message")
+ }
+ }
+ }
+}
diff --git a/app/file.go b/app/file.go
new file mode 100644
index 000000000..a4419bde8
--- /dev/null
+++ b/app/file.go
@@ -0,0 +1,529 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "bytes"
+ _ "image/gif"
+ "crypto/sha256"
+ "encoding/base64"
+ "fmt"
+ "image"
+ "image/color"
+ "image/draw"
+ "image/jpeg"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "os"
+ "path"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+ "github.com/disintegration/imaging"
+ s3 "github.com/minio/minio-go"
+ "github.com/rwcarlsen/goexif/exif"
+ _ "golang.org/x/image/bmp"
+)
+
+const (
+ /*
+ EXIF Image Orientations
+ 1 2 3 4 5 6 7 8
+
+ 888888 888888 88 88 8888888888 88 88 8888888888
+ 88 88 88 88 88 88 88 88 88 88 88 88
+ 8888 8888 8888 8888 88 8888888888 8888888888 88
+ 88 88 88 88
+ 88 88 888888 888888
+ */
+ Upright = 1
+ UprightMirrored = 2
+ UpsideDown = 3
+ UpsideDownMirrored = 4
+ RotatedCWMirrored = 5
+ RotatedCCW = 6
+ RotatedCCWMirrored = 7
+ RotatedCW = 8
+
+ MaxImageSize = 6048 * 4032 // 24 megapixels, roughly 36MB as a raw image
+)
+
+func ReadFile(path string) ([]byte, *model.AppError) {
+ if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
+ endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint
+ accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId
+ secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey
+ secure := *utils.Cfg.FileSettings.AmazonS3SSL
+ s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure)
+ if err != nil {
+ return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
+ }
+ bucket := utils.Cfg.FileSettings.AmazonS3Bucket
+ minioObject, err := s3Clnt.GetObject(bucket, path)
+ defer minioObject.Close()
+ if err != nil {
+ return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
+ }
+ if f, err := ioutil.ReadAll(minioObject); err != nil {
+ return nil, model.NewLocAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error())
+ } else {
+ return f, nil
+ }
+ } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
+ if f, err := ioutil.ReadFile(utils.Cfg.FileSettings.Directory + path); err != nil {
+ return nil, model.NewLocAppError("ReadFile", "api.file.read_file.reading_local.app_error", nil, err.Error())
+ } else {
+ return f, nil
+ }
+ } else {
+ return nil, model.NewLocAppError("ReadFile", "api.file.read_file.configured.app_error", nil, "")
+ }
+}
+
+func MoveFile(oldPath, newPath string) *model.AppError {
+ if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
+ endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint
+ accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId
+ secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey
+ secure := *utils.Cfg.FileSettings.AmazonS3SSL
+ s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure)
+ if err != nil {
+ return model.NewLocAppError("moveFile", "api.file.write_file.s3.app_error", nil, err.Error())
+ }
+ bucket := utils.Cfg.FileSettings.AmazonS3Bucket
+
+ var copyConds = s3.NewCopyConditions()
+ if err = s3Clnt.CopyObject(bucket, newPath, "/"+path.Join(bucket, oldPath), copyConds); err != nil {
+ return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error())
+ }
+ if err = s3Clnt.RemoveObject(bucket, oldPath); err != nil {
+ return model.NewLocAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error())
+ }
+ } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
+ if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+newPath), 0774); err != nil {
+ return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error())
+ }
+
+ if err := os.Rename(utils.Cfg.FileSettings.Directory+oldPath, utils.Cfg.FileSettings.Directory+newPath); err != nil {
+ return model.NewLocAppError("moveFile", "api.file.move_file.rename.app_error", nil, err.Error())
+ }
+ } else {
+ return model.NewLocAppError("moveFile", "api.file.move_file.configured.app_error", nil, "")
+ }
+
+ return nil
+}
+
+func WriteFile(f []byte, path string) *model.AppError {
+ if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
+ endpoint := utils.Cfg.FileSettings.AmazonS3Endpoint
+ accessKey := utils.Cfg.FileSettings.AmazonS3AccessKeyId
+ secretKey := utils.Cfg.FileSettings.AmazonS3SecretAccessKey
+ secure := *utils.Cfg.FileSettings.AmazonS3SSL
+ s3Clnt, err := s3.New(endpoint, accessKey, secretKey, secure)
+ if err != nil {
+ return model.NewLocAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error())
+ }
+ bucket := utils.Cfg.FileSettings.AmazonS3Bucket
+ ext := filepath.Ext(path)
+
+ if model.IsFileExtImage(ext) {
+ _, err = s3Clnt.PutObject(bucket, path, bytes.NewReader(f), model.GetImageMimeType(ext))
+ } else {
+ _, err = s3Clnt.PutObject(bucket, path, bytes.NewReader(f), "binary/octet-stream")
+ }
+ if err != nil {
+ return model.NewLocAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error())
+ }
+ } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
+ if err := writeFileLocally(f, utils.Cfg.FileSettings.Directory+path); err != nil {
+ return err
+ }
+ } else {
+ return model.NewLocAppError("WriteFile", "api.file.write_file.configured.app_error", nil, "")
+ }
+
+ return nil
+}
+
+func writeFileLocally(f []byte, path string) *model.AppError {
+ if err := os.MkdirAll(filepath.Dir(path), 0774); err != nil {
+ directory, _ := filepath.Abs(filepath.Dir(path))
+ return model.NewLocAppError("WriteFile", "api.file.write_file_locally.create_dir.app_error", nil, "directory="+directory+", err="+err.Error())
+ }
+
+ if err := ioutil.WriteFile(path, f, 0644); err != nil {
+ return model.NewLocAppError("WriteFile", "api.file.write_file_locally.writing.app_error", nil, err.Error())
+ }
+
+ return nil
+}
+
+func openFileWriteStream(path string) (io.Writer, *model.AppError) {
+ if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_S3 {
+ return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.s3.app_error", nil, "")
+ } else if utils.Cfg.FileSettings.DriverName == model.IMAGE_DRIVER_LOCAL {
+ if err := os.MkdirAll(filepath.Dir(utils.Cfg.FileSettings.Directory+path), 0774); err != nil {
+ return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.creating_dir.app_error", nil, err.Error())
+ }
+
+ if fileHandle, err := os.Create(utils.Cfg.FileSettings.Directory + path); err != nil {
+ return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.local_server.app_error", nil, err.Error())
+ } else {
+ fileHandle.Chmod(0644)
+ return fileHandle, nil
+ }
+ }
+
+ return nil, model.NewLocAppError("openFileWriteStream", "api.file.open_file_write_stream.configured.app_error", nil, "")
+}
+
+func closeFileWriteStream(file io.Writer) {
+ file.(*os.File).Close()
+}
+
+func GetInfoForFilename(post *model.Post, teamId string, filename string) *model.FileInfo {
+ // Find the path from the Filename of the form /{channelId}/{userId}/{uid}/{nameWithExtension}
+ split := strings.SplitN(filename, "/", 5)
+ if len(split) < 5 {
+ l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.unexpected_filename.error"), post.Id, filename)
+ return nil
+ }
+
+ channelId := split[1]
+ userId := split[2]
+ oldId := split[3]
+ name, _ := url.QueryUnescape(split[4])
+
+ if split[0] != "" || split[1] != post.ChannelId || split[2] != post.UserId || strings.Contains(split[4], "/") {
+ l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.mismatched_filename.warn"), post.Id, post.ChannelId, post.UserId, filename)
+ }
+
+ pathPrefix := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/", teamId, channelId, userId, oldId)
+ path := pathPrefix + name
+
+ // Open the file and populate the fields of the FileInfo
+ var info *model.FileInfo
+ if data, err := ReadFile(path); err != nil {
+ l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.file_not_found.error"), post.Id, filename, path, err)
+ return nil
+ } else {
+ var err *model.AppError
+ info, err = model.GetInfoForBytes(name, data)
+ if err != nil {
+ l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.info.app_error"), post.Id, filename, err)
+ }
+ }
+
+ // Generate a new ID because with the old system, you could very rarely get multiple posts referencing the same file
+ info.Id = model.NewId()
+ info.CreatorId = post.UserId
+ info.PostId = post.Id
+ info.CreateAt = post.CreateAt
+ info.UpdateAt = post.UpdateAt
+ info.Path = path
+
+ if info.IsImage() {
+ nameWithoutExtension := name[:strings.LastIndex(name, ".")]
+ info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg"
+ info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg"
+ }
+
+ return info
+}
+
+func FindTeamIdForFilename(post *model.Post, filename string) string {
+ split := strings.SplitN(filename, "/", 5)
+ id := split[3]
+ name, _ := url.QueryUnescape(split[4])
+
+ // This post is in a direct channel so we need to figure out what team the files are stored under.
+ if result := <-Srv.Store.Team().GetTeamsByUserId(post.UserId); result.Err != nil {
+ l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.teams.app_error"), post.Id, result.Err)
+ } else if teams := result.Data.([]*model.Team); len(teams) == 1 {
+ // The user has only one team so the post must've been sent from it
+ return teams[0].Id
+ } else {
+ for _, team := range teams {
+ path := fmt.Sprintf("teams/%s/channels/%s/users/%s/%s/%s", team.Id, post.ChannelId, post.UserId, id, name)
+ if _, err := ReadFile(path); err == nil {
+ // Found the team that this file was posted from
+ return team.Id
+ }
+ }
+ }
+
+ return ""
+}
+
+var fileMigrationLock sync.Mutex
+
+// Creates and stores FileInfos for a post created before the FileInfos table existed.
+func MigrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo {
+ if len(post.Filenames) == 0 {
+ l4g.Warn(utils.T("api.file.migrate_filenames_to_file_infos.no_filenames.warn"), post.Id)
+ return []*model.FileInfo{}
+ }
+
+ cchan := Srv.Store.Channel().Get(post.ChannelId, true)
+
+ // There's a weird bug that rarely happens where a post ends up with duplicate Filenames so remove those
+ filenames := utils.RemoveDuplicatesFromStringArray(post.Filenames)
+
+ var channel *model.Channel
+ if result := <-cchan; result.Err != nil {
+ l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.channel.app_error"), post.Id, post.ChannelId, result.Err)
+ return []*model.FileInfo{}
+ } else {
+ channel = result.Data.(*model.Channel)
+ }
+
+ // Find the team that was used to make this post since its part of the file path that isn't saved in the Filename
+ var teamId string
+ if channel.TeamId == "" {
+ // This post was made in a cross-team DM channel so we need to find where its files were saved
+ teamId = FindTeamIdForFilename(post, filenames[0])
+ } else {
+ teamId = channel.TeamId
+ }
+
+ // Create FileInfo objects for this post
+ infos := make([]*model.FileInfo, 0, len(filenames))
+ if teamId == "" {
+ l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.team_id.error"), post.Id, filenames)
+ } else {
+ for _, filename := range filenames {
+ info := GetInfoForFilename(post, teamId, filename)
+ if info == nil {
+ continue
+ }
+
+ infos = append(infos, info)
+ }
+ }
+
+ // Lock to prevent only one migration thread from trying to update the post at once, preventing duplicate FileInfos from being created
+ fileMigrationLock.Lock()
+ defer fileMigrationLock.Unlock()
+
+ if result := <-Srv.Store.Post().Get(post.Id); result.Err != nil {
+ l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.get_post_again.app_error"), post.Id, result.Err)
+ return []*model.FileInfo{}
+ } else if newPost := result.Data.(*model.PostList).Posts[post.Id]; len(newPost.Filenames) != len(post.Filenames) {
+ // Another thread has already created FileInfos for this post, so just return those
+ if result := <-Srv.Store.FileInfo().GetForPost(post.Id); result.Err != nil {
+ l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.get_post_file_infos_again.app_error"), post.Id, result.Err)
+ return []*model.FileInfo{}
+ } else {
+ l4g.Debug(utils.T("api.file.migrate_filenames_to_file_infos.not_migrating_post.debug"), post.Id)
+ return result.Data.([]*model.FileInfo)
+ }
+ }
+
+ l4g.Debug(utils.T("api.file.migrate_filenames_to_file_infos.migrating_post.debug"), post.Id)
+
+ savedInfos := make([]*model.FileInfo, 0, len(infos))
+ fileIds := make([]string, 0, len(filenames))
+ for _, info := range infos {
+ if result := <-Srv.Store.FileInfo().Save(info); result.Err != nil {
+ l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.save_file_info.app_error"), post.Id, info.Id, info.Path, result.Err)
+ continue
+ }
+
+ savedInfos = append(savedInfos, info)
+ fileIds = append(fileIds, info.Id)
+ }
+
+ // Copy and save the updated post
+ newPost := &model.Post{}
+ *newPost = *post
+
+ newPost.Filenames = []string{}
+ newPost.FileIds = fileIds
+
+ // Update Posts to clear Filenames and set FileIds
+ if result := <-Srv.Store.Post().Update(newPost, post); result.Err != nil {
+ l4g.Error(utils.T("api.file.migrate_filenames_to_file_infos.save_post.app_error"), post.Id, newPost.FileIds, post.Filenames, result.Err)
+ return []*model.FileInfo{}
+ } else {
+ return savedInfos
+ }
+}
+
+func GeneratePublicLink(siteURL string, info *model.FileInfo) string {
+ hash := GeneratePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt)
+ return fmt.Sprintf("%s%s/public/files/%v/get?h=%s", siteURL, model.API_URL_SUFFIX, info.Id, hash)
+}
+
+func GeneratePublicLinkHash(fileId, salt string) string {
+ hash := sha256.New()
+ hash.Write([]byte(salt))
+ hash.Write([]byte(fileId))
+
+ return base64.RawURLEncoding.EncodeToString(hash.Sum(nil))
+}
+
+func DoUploadFile(teamId string, channelId string, userId string, rawFilename string, data []byte) (*model.FileInfo, *model.AppError) {
+ filename := filepath.Base(rawFilename)
+
+ info, err := model.GetInfoForBytes(filename, data)
+ if err != nil {
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ info.Id = model.NewId()
+ info.CreatorId = userId
+
+ pathPrefix := "teams/" + teamId + "/channels/" + channelId + "/users/" + userId + "/" + info.Id + "/"
+ info.Path = pathPrefix + filename
+
+ if info.IsImage() {
+ // Check dimensions before loading the whole thing into memory later on
+ if info.Width*info.Height > MaxImageSize {
+ err := model.NewLocAppError("uploadFile", "api.file.upload_file.large_image.app_error", map[string]interface{}{"Filename": filename}, "")
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ nameWithoutExtension := filename[:strings.LastIndex(filename, ".")]
+ info.PreviewPath = pathPrefix + nameWithoutExtension + "_preview.jpg"
+ info.ThumbnailPath = pathPrefix + nameWithoutExtension + "_thumb.jpg"
+ }
+
+ if err := WriteFile(data, info.Path); err != nil {
+ return nil, err
+ }
+
+ if result := <-Srv.Store.FileInfo().Save(info); result.Err != nil {
+ return nil, result.Err
+ }
+
+ return info, nil
+}
+
+func HandleImages(previewPathList []string, thumbnailPathList []string, fileData [][]byte) {
+ for i, data := range fileData {
+ go func(i int, data []byte) {
+ img, width, height := prepareImage(fileData[i])
+ if img != nil {
+ go generateThumbnailImage(*img, thumbnailPathList[i], width, height)
+ go generatePreviewImage(*img, previewPathList[i], width)
+ }
+ }(i, data)
+ }
+}
+
+func prepareImage(fileData []byte) (*image.Image, int, int) {
+ // Decode image bytes into Image object
+ img, imgType, err := image.Decode(bytes.NewReader(fileData))
+ if err != nil {
+ l4g.Error(utils.T("api.file.handle_images_forget.decode.error"), err)
+ return nil, 0, 0
+ }
+
+ width := img.Bounds().Dx()
+ height := img.Bounds().Dy()
+
+ // Fill in the background of a potentially-transparent png file as white
+ if imgType == "png" {
+ dst := image.NewRGBA(img.Bounds())
+ draw.Draw(dst, dst.Bounds(), image.NewUniform(color.White), image.Point{}, draw.Src)
+ draw.Draw(dst, dst.Bounds(), img, img.Bounds().Min, draw.Over)
+ img = dst
+ }
+
+ // Flip the image to be upright
+ orientation, _ := getImageOrientation(fileData)
+
+ switch orientation {
+ case UprightMirrored:
+ img = imaging.FlipH(img)
+ case UpsideDown:
+ img = imaging.Rotate180(img)
+ case UpsideDownMirrored:
+ img = imaging.FlipV(img)
+ case RotatedCWMirrored:
+ img = imaging.Transpose(img)
+ case RotatedCCW:
+ img = imaging.Rotate270(img)
+ case RotatedCCWMirrored:
+ img = imaging.Transverse(img)
+ case RotatedCW:
+ img = imaging.Rotate90(img)
+ }
+
+ return &img, width, height
+}
+
+func getImageOrientation(imageData []byte) (int, error) {
+ if exifData, err := exif.Decode(bytes.NewReader(imageData)); err != nil {
+ return Upright, err
+ } else {
+ if tag, err := exifData.Get("Orientation"); err != nil {
+ return Upright, err
+ } else {
+ orientation, err := tag.Int(0)
+ if err != nil {
+ return Upright, err
+ } else {
+ return orientation, nil
+ }
+ }
+ }
+}
+
+func generateThumbnailImage(img image.Image, thumbnailPath string, width int, height int) {
+ thumbWidth := float64(utils.Cfg.FileSettings.ThumbnailWidth)
+ thumbHeight := float64(utils.Cfg.FileSettings.ThumbnailHeight)
+ imgWidth := float64(width)
+ imgHeight := float64(height)
+
+ var thumbnail image.Image
+ if imgHeight < thumbHeight && imgWidth < thumbWidth {
+ thumbnail = img
+ } else if imgHeight/imgWidth < thumbHeight/thumbWidth {
+ thumbnail = imaging.Resize(img, 0, utils.Cfg.FileSettings.ThumbnailHeight, imaging.Lanczos)
+ } else {
+ thumbnail = imaging.Resize(img, utils.Cfg.FileSettings.ThumbnailWidth, 0, imaging.Lanczos)
+ }
+
+ buf := new(bytes.Buffer)
+ if err := jpeg.Encode(buf, thumbnail, &jpeg.Options{Quality: 90}); err != nil {
+ l4g.Error(utils.T("api.file.handle_images_forget.encode_jpeg.error"), thumbnailPath, err)
+ return
+ }
+
+ if err := WriteFile(buf.Bytes(), thumbnailPath); err != nil {
+ l4g.Error(utils.T("api.file.handle_images_forget.upload_thumb.error"), thumbnailPath, err)
+ return
+ }
+}
+
+func generatePreviewImage(img image.Image, previewPath string, width int) {
+ var preview image.Image
+ if width > int(utils.Cfg.FileSettings.PreviewWidth) {
+ preview = imaging.Resize(img, utils.Cfg.FileSettings.PreviewWidth, utils.Cfg.FileSettings.PreviewHeight, imaging.Lanczos)
+ } else {
+ preview = img
+ }
+
+ buf := new(bytes.Buffer)
+
+ if err := jpeg.Encode(buf, preview, &jpeg.Options{Quality: 90}); err != nil {
+ l4g.Error(utils.T("api.file.handle_images_forget.encode_preview.error"), previewPath, err)
+ return
+ }
+
+ if err := WriteFile(buf.Bytes(), previewPath); err != nil {
+ l4g.Error(utils.T("api.file.handle_images_forget.upload_preview.error"), previewPath, err)
+ return
+ }
+}
diff --git a/app/file_test.go b/app/file_test.go
new file mode 100644
index 000000000..9df03315e
--- /dev/null
+++ b/app/file_test.go
@@ -0,0 +1,33 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "testing"
+
+ "github.com/mattermost/platform/model"
+)
+
+func TestGeneratePublicLinkHash(t *testing.T) {
+ filename1 := model.NewId() + "/" + model.NewRandomString(16) + ".txt"
+ filename2 := model.NewId() + "/" + model.NewRandomString(16) + ".txt"
+ salt1 := model.NewRandomString(32)
+ salt2 := model.NewRandomString(32)
+
+ hash1 := GeneratePublicLinkHash(filename1, salt1)
+ hash2 := GeneratePublicLinkHash(filename2, salt1)
+ hash3 := GeneratePublicLinkHash(filename1, salt2)
+
+ if hash1 != GeneratePublicLinkHash(filename1, salt1) {
+ t.Fatal("hash should be equal for the same file name and salt")
+ }
+
+ if hash1 == hash2 {
+ t.Fatal("hashes for different files should not be equal")
+ }
+
+ if hash1 == hash3 {
+ t.Fatal("hashes for the same file with different salts should not be equal")
+ }
+}
diff --git a/api/import.go b/app/import.go
index a6db73126..8f2cf552e 100644
--- a/api/import.go
+++ b/app/import.go
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package api
+package app
import (
"bytes"
@@ -88,7 +88,7 @@ func ImportFile(file io.Reader, teamId string, channelId string, userId string,
io.Copy(buf, file)
data := buf.Bytes()
- fileInfo, err := doUploadFile(teamId, channelId, userId, fileName, data)
+ fileInfo, err := DoUploadFile(teamId, channelId, userId, fileName, data)
if err != nil {
return nil, err
}
diff --git a/app/ldap.go b/app/ldap.go
new file mode 100644
index 000000000..fe68dfa81
--- /dev/null
+++ b/app/ldap.go
@@ -0,0 +1,40 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "net/http"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func SyncLdap() {
+ go func() {
+ if utils.IsLicensed && *utils.License.Features.LDAP && *utils.Cfg.LdapSettings.Enable {
+ if ldapI := einterfaces.GetLdapInterface(); ldapI != nil {
+ ldapI.SyncNow()
+ } else {
+ l4g.Error("%v", model.NewLocAppError("ldapSyncNow", "ent.ldap.disabled.app_error", nil, "").Error())
+ }
+ }
+ }()
+}
+
+func TestLdap() *model.AppError {
+ if ldapI := einterfaces.GetLdapInterface(); ldapI != nil && utils.IsLicensed && *utils.License.Features.LDAP && *utils.Cfg.LdapSettings.Enable {
+ if err := ldapI.RunTest(); err != nil {
+ err.StatusCode = 500
+ return err
+ }
+ } else {
+ err := model.NewLocAppError("ldapTest", "ent.ldap.disabled.app_error", nil, "")
+ err.StatusCode = http.StatusNotImplemented
+ return err
+ }
+
+ return nil
+}
diff --git a/app/notification.go b/app/notification.go
new file mode 100644
index 000000000..ec78c416b
--- /dev/null
+++ b/app/notification.go
@@ -0,0 +1,721 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "crypto/tls"
+ "fmt"
+ "html"
+ "html/template"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "path/filepath"
+ "sort"
+ "strings"
+ "time"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+ "github.com/mattermost/platform/utils"
+ "github.com/nicksnyder/go-i18n/i18n"
+)
+
+func SendNotifications(post *model.Post, team *model.Team, channel *model.Channel, sender *model.User) ([]string, *model.AppError) {
+ pchan := Srv.Store.User().GetProfilesInChannel(channel.Id, -1, -1, true)
+ fchan := Srv.Store.FileInfo().GetForPost(post.Id)
+
+ var profileMap map[string]*model.User
+ if result := <-pchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ profileMap = result.Data.(map[string]*model.User)
+ }
+
+ // If the user who made the post isn't in the channel, don't send a notification
+ if _, ok := profileMap[post.UserId]; !ok && post.Props["from_webhook"] != "true" {
+ l4g.Debug(utils.T("api.post.send_notifications.user_id.debug"), post.Id, channel.Id, post.UserId)
+ return []string{}, nil
+ }
+
+ mentionedUserIds := make(map[string]bool)
+ allActivityPushUserIds := []string{}
+ hereNotification := false
+ channelNotification := false
+ allNotification := false
+ updateMentionChans := []store.StoreChannel{}
+
+ if channel.Type == model.CHANNEL_DIRECT {
+ var otherUserId string
+ if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId {
+ otherUserId = userIds[1]
+ } else {
+ otherUserId = userIds[0]
+ }
+
+ mentionedUserIds[otherUserId] = true
+ if post.Props["from_webhook"] == "true" {
+ mentionedUserIds[post.UserId] = true
+ }
+ } else {
+ keywords := GetMentionKeywordsInChannel(profileMap)
+
+ var potentialOtherMentions []string
+ mentionedUserIds, potentialOtherMentions, hereNotification, channelNotification, allNotification = GetExplicitMentions(post.Message, keywords)
+
+ // get users that have comment thread mentions enabled
+ if len(post.RootId) > 0 {
+ if result := <-Srv.Store.Post().Get(post.RootId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ list := result.Data.(*model.PostList)
+
+ for _, threadPost := range list.Posts {
+ profile := profileMap[threadPost.UserId]
+ if profile.NotifyProps["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == list.Order[0]) {
+ mentionedUserIds[threadPost.UserId] = true
+ }
+ }
+ }
+ }
+
+ // prevent the user from mentioning themselves
+ if post.Props["from_webhook"] != "true" {
+ delete(mentionedUserIds, post.UserId)
+ }
+
+ if len(potentialOtherMentions) > 0 {
+ if result := <-Srv.Store.User().GetProfilesByUsernames(potentialOtherMentions, team.Id); result.Err == nil {
+ outOfChannelMentions := result.Data.(map[string]*model.User)
+ go sendOutOfChannelMentions(sender, post, team.Id, outOfChannelMentions)
+ }
+ }
+
+ // find which users in the channel are set up to always receive mobile notifications
+ for _, profile := range profileMap {
+ if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL &&
+ (post.UserId != profile.Id || post.Props["from_webhook"] == "true") &&
+ !post.IsSystemMessage() {
+ allActivityPushUserIds = append(allActivityPushUserIds, profile.Id)
+ }
+ }
+ }
+
+ mentionedUsersList := make([]string, 0, len(mentionedUserIds))
+ for id := range mentionedUserIds {
+ mentionedUsersList = append(mentionedUsersList, id)
+ updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id))
+ }
+
+ senderName := make(map[string]string)
+ for _, id := range mentionedUsersList {
+ senderName[id] = ""
+ if post.IsSystemMessage() {
+ senderName[id] = utils.T("system.message.name")
+ } else {
+ if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" {
+ senderName[id] = value.(string)
+ } else {
+ // Get the Display name preference from the receiver
+ if result := <-Srv.Store.Preference().Get(id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, "name_format"); result.Err != nil {
+ // Show default sender's name if user doesn't set display settings.
+ senderName[id] = sender.Username
+ } else {
+ senderName[id] = sender.GetDisplayNameForPreference(result.Data.(model.Preference).Value)
+ }
+ }
+ }
+ }
+
+ var senderUsername string
+ if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" {
+ senderUsername = value.(string)
+ } else {
+ senderUsername = sender.Username
+ }
+
+ if utils.Cfg.EmailSettings.SendEmailNotifications {
+ for _, id := range mentionedUsersList {
+ userAllowsEmails := profileMap[id].NotifyProps["email"] != "false"
+
+ var status *model.Status
+ var err *model.AppError
+ if status, err = GetStatus(id); err != nil {
+ status = &model.Status{
+ UserId: id,
+ Status: model.STATUS_OFFLINE,
+ Manual: false,
+ LastActivityAt: 0,
+ ActiveChannel: "",
+ }
+ }
+
+ if userAllowsEmails && status.Status != model.STATUS_ONLINE && profileMap[id].DeleteAt == 0 {
+ sendNotificationEmail(post, profileMap[id], channel, team, senderName[id], sender)
+ }
+ }
+ }
+
+ T := utils.GetUserTranslations(sender.Locale)
+
+ // If the channel has more than 1K users then @here is disabled
+ if hereNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
+ hereNotification = false
+ SendEphemeralPost(
+ team.Id,
+ post.UserId,
+ &model.Post{
+ ChannelId: post.ChannelId,
+ Message: T("api.post.disabled_here", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}),
+ CreateAt: post.CreateAt + 1,
+ },
+ )
+ }
+
+ // If the channel has more than 1K users then @channel is disabled
+ if channelNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
+ SendEphemeralPost(
+ team.Id,
+ post.UserId,
+ &model.Post{
+ ChannelId: post.ChannelId,
+ Message: T("api.post.disabled_channel", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}),
+ CreateAt: post.CreateAt + 1,
+ },
+ )
+ }
+
+ // If the channel has more than 1K users then @all is disabled
+ if allNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
+ SendEphemeralPost(
+ team.Id,
+ post.UserId,
+ &model.Post{
+ ChannelId: post.ChannelId,
+ Message: T("api.post.disabled_all", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}),
+ CreateAt: post.CreateAt + 1,
+ },
+ )
+ }
+
+ if hereNotification {
+ if result := <-Srv.Store.Status().GetOnline(); result.Err != nil {
+ return nil, result.Err
+ } else {
+ statuses := result.Data.([]*model.Status)
+ for _, status := range statuses {
+ if status.UserId == post.UserId {
+ continue
+ }
+
+ _, profileFound := profileMap[status.UserId]
+ _, alreadyMentioned := mentionedUserIds[status.UserId]
+
+ if status.Status == model.STATUS_ONLINE && profileFound && !alreadyMentioned {
+ mentionedUsersList = append(mentionedUsersList, status.UserId)
+ updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, status.UserId))
+ }
+ }
+ }
+ }
+
+ // Make sure all mention updates are complete to prevent race
+ // Probably better to batch these DB updates in the future
+ // MUST be completed before push notifications send
+ for _, uchan := range updateMentionChans {
+ if result := <-uchan; result.Err != nil {
+ l4g.Warn(utils.T("api.post.update_mention_count_and_forget.update_error"), post.Id, post.ChannelId, result.Err)
+ }
+ }
+
+ sendPushNotifications := false
+ if *utils.Cfg.EmailSettings.SendPushNotifications {
+ pushServer := *utils.Cfg.EmailSettings.PushNotificationServer
+ if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) {
+ l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn"))
+ sendPushNotifications = false
+ } else {
+ sendPushNotifications = true
+ }
+ }
+
+ if sendPushNotifications {
+ for _, id := range mentionedUsersList {
+ var status *model.Status
+ var err *model.AppError
+ if status, err = GetStatus(id); err != nil {
+ status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""}
+ }
+
+ if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) {
+ sendPushNotification(post, profileMap[id], channel, senderName[id], true)
+ }
+ }
+
+ for _, id := range allActivityPushUserIds {
+ if _, ok := mentionedUserIds[id]; !ok {
+ var status *model.Status
+ var err *model.AppError
+ if status, err = GetStatus(id); err != nil {
+ status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""}
+ }
+
+ if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) {
+ sendPushNotification(post, profileMap[id], channel, senderName[id], false)
+ }
+ }
+ }
+ }
+
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POSTED, "", post.ChannelId, "", nil)
+ message.Add("post", post.ToJson())
+ message.Add("channel_type", channel.Type)
+ message.Add("channel_display_name", channel.DisplayName)
+ message.Add("channel_name", channel.Name)
+ message.Add("sender_name", senderUsername)
+ message.Add("team_id", team.Id)
+
+ if len(post.FileIds) != 0 {
+ message.Add("otherFile", "true")
+
+ var infos []*model.FileInfo
+ if result := <-fchan; result.Err != nil {
+ l4g.Warn(utils.T("api.post.send_notifications.files.error"), post.Id, result.Err)
+ } else {
+ infos = result.Data.([]*model.FileInfo)
+ }
+
+ for _, info := range infos {
+ if info.IsImage() {
+ message.Add("image", "true")
+ break
+ }
+ }
+ }
+
+ if len(mentionedUsersList) != 0 {
+ message.Add("mentions", model.ArrayToJson(mentionedUsersList))
+ }
+
+ Publish(message)
+ return mentionedUsersList, nil
+}
+
+func sendNotificationEmail(post *model.Post, user *model.User, channel *model.Channel, team *model.Team, senderName string, sender *model.User) *model.AppError {
+ if channel.Type == model.CHANNEL_DIRECT && channel.TeamId != team.Id {
+ // this message is a cross-team DM so it we need to find a team that the recipient is on to use in the link
+ if result := <-Srv.Store.Team().GetTeamsByUserId(user.Id); result.Err != nil {
+ return result.Err
+ } else {
+ // if the recipient isn't in the current user's team, just pick one
+ teams := result.Data.([]*model.Team)
+ found := false
+
+ for i := range teams {
+ if teams[i].Id == team.Id {
+ found = true
+ break
+ }
+ }
+
+ if !found && len(teams) > 0 {
+ team = teams[0]
+ } else {
+ // in case the user hasn't joined any teams we send them to the select_team page
+ team = &model.Team{Name: "select_team", DisplayName: utils.Cfg.TeamSettings.SiteName}
+ }
+ }
+ }
+ if *utils.Cfg.EmailSettings.EnableEmailBatching {
+ var sendBatched bool
+
+ if result := <-Srv.Store.Preference().Get(user.Id, model.PREFERENCE_CATEGORY_NOTIFICATIONS, model.PREFERENCE_NAME_EMAIL_INTERVAL); result.Err != nil {
+ // if the call fails, assume it hasn't been set and use the default
+ sendBatched = false
+ } else {
+ // default to not using batching if the setting is set to immediate
+ sendBatched = result.Data.(model.Preference).Value != model.PREFERENCE_DEFAULT_EMAIL_INTERVAL
+ }
+
+ if sendBatched {
+ if err := AddNotificationEmailToBatch(user, post, team); err == nil {
+ return nil
+ }
+ }
+
+ // fall back to sending a single email if we can't batch it for some reason
+ }
+
+ var channelName string
+ var bodyText string
+ var subjectText string
+ var mailTemplate string
+ var mailParameters map[string]interface{}
+
+ teamURL := utils.GetSiteURL() + "/" + team.Name
+ tm := time.Unix(post.CreateAt/1000, 0)
+
+ userLocale := utils.GetUserTranslations(user.Locale)
+ month := userLocale(tm.Month().String())
+ day := fmt.Sprintf("%d", tm.Day())
+ year := fmt.Sprintf("%d", tm.Year())
+ zone, _ := tm.Zone()
+
+ if channel.Type == model.CHANNEL_DIRECT {
+ bodyText = userLocale("api.post.send_notifications_and_forget.message_body")
+ subjectText = userLocale("api.post.send_notifications_and_forget.message_subject")
+
+ senderDisplayName := senderName
+
+ mailTemplate = "api.templates.post_subject_in_direct_message"
+ mailParameters = map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName,
+ "SenderDisplayName": senderDisplayName, "Month": month, "Day": day, "Year": year}
+ } else {
+ bodyText = userLocale("api.post.send_notifications_and_forget.mention_body")
+ subjectText = userLocale("api.post.send_notifications_and_forget.mention_subject")
+ channelName = channel.DisplayName
+ mailTemplate = "api.templates.post_subject_in_channel"
+ mailParameters = map[string]interface{}{"SubjectText": subjectText, "TeamDisplayName": team.DisplayName,
+ "ChannelName": channelName, "Month": month, "Day": day, "Year": year}
+ }
+
+ subject := fmt.Sprintf("[%v] %v", utils.Cfg.TeamSettings.SiteName, userLocale(mailTemplate, mailParameters))
+
+ bodyPage := utils.NewHTMLTemplate("post_body", user.Locale)
+ bodyPage.Props["SiteURL"] = utils.GetSiteURL()
+ bodyPage.Props["PostMessage"] = GetMessageForNotification(post, userLocale)
+ if team.Name != "select_team" {
+ bodyPage.Props["TeamLink"] = teamURL + "/pl/" + post.Id
+ } else {
+ bodyPage.Props["TeamLink"] = teamURL
+ }
+
+ bodyPage.Props["BodyText"] = bodyText
+ bodyPage.Props["Button"] = userLocale("api.templates.post_body.button")
+ bodyPage.Html["Info"] = template.HTML(userLocale("api.templates.post_body.info",
+ map[string]interface{}{"ChannelName": channelName, "SenderName": senderName,
+ "Hour": fmt.Sprintf("%02d", tm.Hour()), "Minute": fmt.Sprintf("%02d", tm.Minute()),
+ "TimeZone": zone, "Month": month, "Day": day}))
+
+ if err := utils.SendMail(user.Email, html.UnescapeString(subject), bodyPage.Render()); err != nil {
+ return err
+ }
+
+ if einterfaces.GetMetricsInterface() != nil {
+ einterfaces.GetMetricsInterface().IncrementPostSentEmail()
+ }
+
+ return nil
+}
+
+func GetMessageForNotification(post *model.Post, translateFunc i18n.TranslateFunc) string {
+ if len(strings.TrimSpace(post.Message)) != 0 || len(post.FileIds) == 0 {
+ return post.Message
+ }
+
+ // extract the filenames from their paths and determine what type of files are attached
+ var infos []*model.FileInfo
+ if result := <-Srv.Store.FileInfo().GetForPost(post.Id); result.Err != nil {
+ l4g.Warn(utils.T("api.post.get_message_for_notification.get_files.error"), post.Id, result.Err)
+ } else {
+ infos = result.Data.([]*model.FileInfo)
+ }
+
+ filenames := make([]string, len(infos))
+ onlyImages := true
+ for i, info := range infos {
+ if escaped, err := url.QueryUnescape(filepath.Base(info.Name)); err != nil {
+ // this should never error since filepath was escaped using url.QueryEscape
+ filenames[i] = escaped
+ } else {
+ filenames[i] = info.Name
+ }
+
+ onlyImages = onlyImages && info.IsImage()
+ }
+
+ props := map[string]interface{}{"Filenames": strings.Join(filenames, ", ")}
+
+ if onlyImages {
+ return translateFunc("api.post.get_message_for_notification.images_sent", len(filenames), props)
+ } else {
+ return translateFunc("api.post.get_message_for_notification.files_sent", len(filenames), props)
+ }
+}
+
+func sendPushNotification(post *model.Post, user *model.User, channel *model.Channel, senderName string, wasMentioned bool) *model.AppError {
+ sessions, err := getMobileAppSessions(user.Id)
+ if err != nil {
+ return err
+ }
+
+ var channelName string
+
+ if channel.Type == model.CHANNEL_DIRECT {
+ channelName = senderName
+ } else {
+ channelName = channel.DisplayName
+ }
+
+ userLocale := utils.GetUserTranslations(user.Locale)
+
+ msg := model.PushNotification{}
+ if badge := <-Srv.Store.User().GetUnreadCount(user.Id); badge.Err != nil {
+ msg.Badge = 1
+ l4g.Error(utils.T("store.sql_user.get_unread_count.app_error"), user.Id, badge.Err)
+ } else {
+ msg.Badge = int(badge.Data.(int64))
+ }
+ msg.Type = model.PUSH_TYPE_MESSAGE
+ msg.TeamId = channel.TeamId
+ msg.ChannelId = channel.Id
+ msg.ChannelName = channel.Name
+
+ if *utils.Cfg.EmailSettings.PushNotificationContents == model.FULL_NOTIFICATION {
+ if channel.Type == model.CHANNEL_DIRECT {
+ msg.Category = model.CATEGORY_DM
+ msg.Message = "@" + senderName + ": " + model.ClearMentionTags(post.Message)
+ } else {
+ msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_in") + channelName + ": " + model.ClearMentionTags(post.Message)
+ }
+ } else {
+ if channel.Type == model.CHANNEL_DIRECT {
+ msg.Category = model.CATEGORY_DM
+ msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_message")
+ } else if wasMentioned {
+ msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_mention") + channelName
+ } else {
+ msg.Message = senderName + userLocale("api.post.send_notifications_and_forget.push_non_mention") + channelName
+ }
+ }
+
+ l4g.Debug(utils.T("api.post.send_notifications_and_forget.push_notification.debug"), msg.DeviceId, msg.Message)
+
+ for _, session := range sessions {
+ tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson()))
+ tmpMessage.SetDeviceIdAndPlatform(session.DeviceId)
+ if err := sendToPushProxy(tmpMessage); err != nil {
+ return err
+ }
+
+ if einterfaces.GetMetricsInterface() != nil {
+ einterfaces.GetMetricsInterface().IncrementPostSentPush()
+ }
+ }
+
+ return nil
+}
+
+func ClearPushNotification(userId string, channelId string) *model.AppError {
+ sessions, err := getMobileAppSessions(userId)
+ if err != nil {
+ return err
+ }
+
+ msg := model.PushNotification{}
+ msg.Type = model.PUSH_TYPE_CLEAR
+ msg.ChannelId = channelId
+ msg.ContentAvailable = 0
+ if badge := <-Srv.Store.User().GetUnreadCount(userId); badge.Err != nil {
+ msg.Badge = 0
+ l4g.Error(utils.T("store.sql_user.get_unread_count.app_error"), userId, badge.Err)
+ } else {
+ msg.Badge = int(badge.Data.(int64))
+ }
+
+ l4g.Debug(utils.T("api.post.send_notifications_and_forget.clear_push_notification.debug"), msg.DeviceId, msg.ChannelId)
+
+ for _, session := range sessions {
+ tmpMessage := *model.PushNotificationFromJson(strings.NewReader(msg.ToJson()))
+ tmpMessage.SetDeviceIdAndPlatform(session.DeviceId)
+ if err := sendToPushProxy(tmpMessage); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func sendToPushProxy(msg model.PushNotification) *model.AppError {
+ msg.ServerId = utils.CfgDiagnosticId
+
+ tr := &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections},
+ }
+ httpClient := &http.Client{Transport: tr}
+ request, _ := http.NewRequest("POST", *utils.Cfg.EmailSettings.PushNotificationServer+model.API_URL_SUFFIX_V1+"/send_push", strings.NewReader(msg.ToJson()))
+
+ if resp, err := httpClient.Do(request); err != nil {
+ return model.NewLocAppError("sendToPushProxy", "api.post.send_notifications_and_forget.push_notification.error", map[string]interface{}{"DeviceId": msg.DeviceId, "Error": err.Error()}, "")
+ } else {
+ ioutil.ReadAll(resp.Body)
+ resp.Body.Close()
+ }
+
+ return nil
+}
+
+func getMobileAppSessions(userId string) ([]*model.Session, *model.AppError) {
+ if result := <-Srv.Store.Session().GetSessionsWithActiveDeviceIds(userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.Session), nil
+ }
+}
+
+func sendOutOfChannelMentions(sender *model.User, post *model.Post, teamId string, profiles map[string]*model.User) *model.AppError {
+ if len(profiles) == 0 {
+ return nil
+ }
+
+ var usernames []string
+ for _, user := range profiles {
+ usernames = append(usernames, user.Username)
+ }
+ sort.Strings(usernames)
+
+ T := utils.GetUserTranslations(sender.Locale)
+
+ var message string
+ if len(usernames) == 1 {
+ message = T("api.post.check_for_out_of_channel_mentions.message.one", map[string]interface{}{
+ "Username": usernames[0],
+ })
+ } else {
+ message = T("api.post.check_for_out_of_channel_mentions.message.multiple", map[string]interface{}{
+ "Usernames": strings.Join(usernames[:len(usernames)-1], ", "),
+ "LastUsername": usernames[len(usernames)-1],
+ })
+ }
+
+ SendEphemeralPost(
+ teamId,
+ post.UserId,
+ &model.Post{
+ ChannelId: post.ChannelId,
+ Message: message,
+ CreateAt: post.CreateAt + 1,
+ },
+ )
+
+ return nil
+}
+
+// Given a message and a map mapping mention keywords to the users who use them, returns a map of mentioned
+// users and a slice of potential mention users not in the channel and whether or not @here was mentioned.
+func GetExplicitMentions(message string, keywords map[string][]string) (map[string]bool, []string, bool, bool, bool) {
+ mentioned := make(map[string]bool)
+ potentialOthersMentioned := make([]string, 0)
+ systemMentions := map[string]bool{"@here": true, "@channel": true, "@all": true}
+ hereMentioned := false
+ allMentioned := false
+ channelMentioned := false
+
+ addMentionedUsers := func(ids []string) {
+ for _, id := range ids {
+ mentioned[id] = true
+ }
+ }
+
+ for _, word := range strings.Fields(message) {
+ isMention := false
+
+ if word == "@here" {
+ hereMentioned = true
+ }
+
+ if word == "@channel" {
+ channelMentioned = true
+ }
+
+ if word == "@all" {
+ allMentioned = true
+ }
+
+ // Non-case-sensitive check for regular keys
+ if ids, match := keywords[strings.ToLower(word)]; match {
+ addMentionedUsers(ids)
+ isMention = true
+ }
+
+ // Case-sensitive check for first name
+ if ids, match := keywords[word]; match {
+ addMentionedUsers(ids)
+ isMention = true
+ }
+
+ if !isMention {
+ // No matches were found with the string split just on whitespace so try further splitting
+ // the message on punctuation
+ splitWords := strings.FieldsFunc(word, func(c rune) bool {
+ return model.SplitRunes[c]
+ })
+
+ for _, splitWord := range splitWords {
+ if splitWord == "@here" {
+ hereMentioned = true
+ }
+
+ if splitWord == "@all" {
+ allMentioned = true
+ }
+
+ if splitWord == "@channel" {
+ channelMentioned = true
+ }
+
+ // Non-case-sensitive check for regular keys
+ if ids, match := keywords[strings.ToLower(splitWord)]; match {
+ addMentionedUsers(ids)
+ }
+
+ // Case-sensitive check for first name
+ if ids, match := keywords[splitWord]; match {
+ addMentionedUsers(ids)
+ } else if _, ok := systemMentions[word]; !ok && strings.HasPrefix(word, "@") {
+ username := word[1:len(splitWord)]
+ potentialOthersMentioned = append(potentialOthersMentioned, username)
+ }
+ }
+ }
+ }
+
+ return mentioned, potentialOthersMentioned, hereMentioned, channelMentioned, allMentioned
+}
+
+// Given a map of user IDs to profiles, returns a list of mention
+// keywords for all users in the channel.
+func GetMentionKeywordsInChannel(profiles map[string]*model.User) map[string][]string {
+ keywords := make(map[string][]string)
+
+ for id, profile := range profiles {
+ userMention := "@" + strings.ToLower(profile.Username)
+ keywords[userMention] = append(keywords[userMention], id)
+
+ if len(profile.NotifyProps["mention_keys"]) > 0 {
+ // Add all the user's mention keys
+ splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",")
+ for _, k := range splitKeys {
+ // note that these are made lower case so that we can do a case insensitive check for them
+ key := strings.ToLower(k)
+ keywords[key] = append(keywords[key], id)
+ }
+ }
+
+ // If turned on, add the user's case sensitive first name
+ if profile.NotifyProps["first_name"] == "true" {
+ keywords[profile.FirstName] = append(keywords[profile.FirstName], profile.Id)
+ }
+
+ // Add @channel and @all to keywords if user has them turned on
+ if int64(len(profiles)) < *utils.Cfg.TeamSettings.MaxNotificationsPerChannel && profile.NotifyProps["channel"] == "true" {
+ keywords["@channel"] = append(keywords["@channel"], profile.Id)
+ keywords["@all"] = append(keywords["@all"], profile.Id)
+ }
+ }
+
+ return keywords
+}
diff --git a/app/notification_test.go b/app/notification_test.go
new file mode 100644
index 000000000..10eb09247
--- /dev/null
+++ b/app/notification_test.go
@@ -0,0 +1,313 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "testing"
+
+ "github.com/mattermost/platform/model"
+)
+
+func TestSendNotifications(t *testing.T) {
+ th := Setup().InitBasic()
+
+ AddUserToChannel(th.BasicUser2, th.BasicChannel)
+
+ post1, postErr := CreatePost(&model.Post{
+ UserId: th.BasicUser.Id,
+ ChannelId: th.BasicChannel.Id,
+ Message: "@" + th.BasicUser2.Username,
+ }, th.BasicTeam.Id, true)
+
+ if postErr != nil {
+ t.Fatal(postErr)
+ }
+
+ mentions, err := SendNotifications(post1, th.BasicTeam, th.BasicChannel, th.BasicUser)
+ if err != nil {
+ t.Fatal(err)
+ } else if mentions == nil {
+ t.Log(mentions)
+ t.Fatal("user should have been mentioned")
+ } else if mentions[0] != th.BasicUser2.Id {
+ t.Log(mentions)
+ t.Fatal("user should have been mentioned")
+ }
+}
+
+func TestGetExplicitMentions(t *testing.T) {
+ id1 := model.NewId()
+ id2 := model.NewId()
+
+ // not mentioning anybody
+ message := "this is a message"
+ keywords := map[string][]string{}
+ if mentions, potential, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 0 || len(potential) != 0 {
+ t.Fatal("shouldn't have mentioned anybody or have any potencial mentions")
+ }
+
+ // mentioning a user that doesn't exist
+ message = "this is a message for @user"
+ if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 0 {
+ t.Fatal("shouldn't have mentioned user that doesn't exist")
+ }
+
+ // mentioning one person
+ keywords = map[string][]string{"@user": {id1}}
+ if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] {
+ t.Fatal("should've mentioned @user")
+ }
+
+ // mentioning one person without an @mention
+ message = "this is a message for @user"
+ keywords = map[string][]string{"this": {id1}}
+ if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] {
+ t.Fatal("should've mentioned this")
+ }
+
+ // mentioning multiple people with one word
+ message = "this is a message for @user"
+ keywords = map[string][]string{"@user": {id1, id2}}
+ if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] {
+ t.Fatal("should've mentioned two users with @user")
+ }
+
+ // mentioning only one of multiple people
+ keywords = map[string][]string{"@user": {id1}, "@mention": {id2}}
+ if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] {
+ t.Fatal("should've mentioned @user and not @mention")
+ }
+
+ // mentioning multiple people with multiple words
+ message = "this is an @mention for @user"
+ keywords = map[string][]string{"@user": {id1}, "@mention": {id2}}
+ if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] {
+ t.Fatal("should've mentioned two users with @user and @mention")
+ }
+
+ // mentioning @channel (not a special case, but it's good to double check)
+ message = "this is an message for @channel"
+ keywords = map[string][]string{"@channel": {id1, id2}}
+ if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] {
+ t.Fatal("should've mentioned two users with @channel")
+ }
+
+ // mentioning @all (not a special case, but it's good to double check)
+ message = "this is an message for @all"
+ keywords = map[string][]string{"@all": {id1, id2}}
+ if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 2 || !mentions[id1] || !mentions[id2] {
+ t.Fatal("should've mentioned two users with @all")
+ }
+
+ // mentioning user.period without mentioning user (PLT-3222)
+ message = "user.period doesn't complicate things at all by including periods in their username"
+ keywords = map[string][]string{"user.period": {id1}, "user": {id2}}
+ if mentions, _, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || mentions[id2] {
+ t.Fatal("should've mentioned user.period and not user")
+ }
+
+ // mentioning a potential out of channel user
+ message = "this is an message for @potential and @user"
+ keywords = map[string][]string{"@user": {id1}}
+ if mentions, potential, _, _, _ := GetExplicitMentions(message, keywords); len(mentions) != 1 || !mentions[id1] || len(potential) != 1 {
+ t.Fatal("should've mentioned user and have a potential not in channel")
+ }
+}
+
+func TestGetExplicitMentionsAtHere(t *testing.T) {
+ // test all the boundary cases that we know can break up terms (and those that we know won't)
+ cases := map[string]bool{
+ "": false,
+ "here": false,
+ "@here": true,
+ " @here ": true,
+ "\t@here\t": true,
+ "\n@here\n": true,
+ // "!@here!": true,
+ // "@@here@": true,
+ // "#@here#": true,
+ // "$@here$": true,
+ // "%@here%": true,
+ // "^@here^": true,
+ // "&@here&": true,
+ // "*@here*": true,
+ "(@here(": true,
+ ")@here)": true,
+ // "-@here-": true,
+ // "_@here_": true,
+ // "=@here=": true,
+ "+@here+": true,
+ "[@here[": true,
+ "{@here{": true,
+ "]@here]": true,
+ "}@here}": true,
+ "\\@here\\": true,
+ // "|@here|": true,
+ ";@here;": true,
+ ":@here:": true,
+ // "'@here'": true,
+ // "\"@here\"": true,
+ ",@here,": true,
+ "<@here<": true,
+ ".@here.": true,
+ ">@here>": true,
+ "/@here/": true,
+ "?@here?": true,
+ // "`@here`": true,
+ // "~@here~": true,
+ }
+
+ for message, shouldMention := range cases {
+ if _, _, hereMentioned, _, _ := GetExplicitMentions(message, nil); hereMentioned && !shouldMention {
+ t.Fatalf("shouldn't have mentioned @here with \"%v\"", message)
+ } else if !hereMentioned && shouldMention {
+ t.Fatalf("should've have mentioned @here with \"%v\"", message)
+ }
+ }
+
+ // mentioning @here and someone
+ id := model.NewId()
+ if mentions, potential, hereMentioned, _, _ := GetExplicitMentions("@here @user @potential", map[string][]string{"@user": {id}}); !hereMentioned {
+ t.Fatal("should've mentioned @here with \"@here @user\"")
+ } else if len(mentions) != 1 || !mentions[id] {
+ t.Fatal("should've mentioned @user with \"@here @user\"")
+ } else if len(potential) > 1 {
+ t.Fatal("should've potential mentions for @potential")
+ }
+}
+
+func TestGetMentionKeywords(t *testing.T) {
+ Setup()
+ // user with username or custom mentions enabled
+ user1 := &model.User{
+ Id: model.NewId(),
+ FirstName: "First",
+ Username: "User",
+ NotifyProps: map[string]string{
+ "mention_keys": "User,@User,MENTION",
+ },
+ }
+
+ profiles := map[string]*model.User{user1.Id: user1}
+ mentions := GetMentionKeywordsInChannel(profiles)
+ if len(mentions) != 3 {
+ t.Fatal("should've returned three mention keywords")
+ } else if ids, ok := mentions["user"]; !ok || ids[0] != user1.Id {
+ t.Fatal("should've returned mention key of user")
+ } else if ids, ok := mentions["@user"]; !ok || ids[0] != user1.Id {
+ t.Fatal("should've returned mention key of @user")
+ } else if ids, ok := mentions["mention"]; !ok || ids[0] != user1.Id {
+ t.Fatal("should've returned mention key of mention")
+ }
+
+ // user with first name mention enabled
+ user2 := &model.User{
+ Id: model.NewId(),
+ FirstName: "First",
+ Username: "User",
+ NotifyProps: map[string]string{
+ "first_name": "true",
+ },
+ }
+
+ profiles = map[string]*model.User{user2.Id: user2}
+ mentions = GetMentionKeywordsInChannel(profiles)
+ if len(mentions) != 2 {
+ t.Fatal("should've returned two mention keyword")
+ } else if ids, ok := mentions["First"]; !ok || ids[0] != user2.Id {
+ t.Fatal("should've returned mention key of First")
+ }
+
+ // user with @channel/@all mentions enabled
+ user3 := &model.User{
+ Id: model.NewId(),
+ FirstName: "First",
+ Username: "User",
+ NotifyProps: map[string]string{
+ "channel": "true",
+ },
+ }
+
+ profiles = map[string]*model.User{user3.Id: user3}
+ mentions = GetMentionKeywordsInChannel(profiles)
+ if len(mentions) != 3 {
+ t.Fatal("should've returned three mention keywords")
+ } else if ids, ok := mentions["@channel"]; !ok || ids[0] != user3.Id {
+ t.Fatal("should've returned mention key of @channel")
+ } else if ids, ok := mentions["@all"]; !ok || ids[0] != user3.Id {
+ t.Fatal("should've returned mention key of @all")
+ }
+
+ // user with all types of mentions enabled
+ user4 := &model.User{
+ Id: model.NewId(),
+ FirstName: "First",
+ Username: "User",
+ NotifyProps: map[string]string{
+ "mention_keys": "User,@User,MENTION",
+ "first_name": "true",
+ "channel": "true",
+ },
+ }
+
+ profiles = map[string]*model.User{user4.Id: user4}
+ mentions = GetMentionKeywordsInChannel(profiles)
+ if len(mentions) != 6 {
+ t.Fatal("should've returned six mention keywords")
+ } else if ids, ok := mentions["user"]; !ok || ids[0] != user4.Id {
+ t.Fatal("should've returned mention key of user")
+ } else if ids, ok := mentions["@user"]; !ok || ids[0] != user4.Id {
+ t.Fatal("should've returned mention key of @user")
+ } else if ids, ok := mentions["mention"]; !ok || ids[0] != user4.Id {
+ t.Fatal("should've returned mention key of mention")
+ } else if ids, ok := mentions["First"]; !ok || ids[0] != user4.Id {
+ t.Fatal("should've returned mention key of First")
+ } else if ids, ok := mentions["@channel"]; !ok || ids[0] != user4.Id {
+ t.Fatal("should've returned mention key of @channel")
+ } else if ids, ok := mentions["@all"]; !ok || ids[0] != user4.Id {
+ t.Fatal("should've returned mention key of @all")
+ }
+
+ dup_count := func(list []string) map[string]int {
+
+ duplicate_frequency := make(map[string]int)
+
+ for _, item := range list {
+ // check if the item/element exist in the duplicate_frequency map
+
+ _, exist := duplicate_frequency[item]
+
+ if exist {
+ duplicate_frequency[item] += 1 // increase counter by 1 if already in the map
+ } else {
+ duplicate_frequency[item] = 1 // else start counting from 1
+ }
+ }
+ return duplicate_frequency
+ }
+
+ // multiple users
+ profiles = map[string]*model.User{
+ user1.Id: user1,
+ user2.Id: user2,
+ user3.Id: user3,
+ user4.Id: user4,
+ }
+ mentions = GetMentionKeywordsInChannel(profiles)
+ if len(mentions) != 6 {
+ t.Fatal("should've returned six mention keywords")
+ } else if ids, ok := mentions["user"]; !ok || len(ids) != 2 || (ids[0] != user1.Id && ids[1] != user1.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) {
+ t.Fatal("should've mentioned user1 and user4 with user")
+ } else if ids := dup_count(mentions["@user"]); len(ids) != 4 || (ids[user1.Id] != 2) || (ids[user4.Id] != 2) {
+ t.Fatal("should've mentioned user1 and user4 with @user")
+ } else if ids, ok := mentions["mention"]; !ok || len(ids) != 2 || (ids[0] != user1.Id && ids[1] != user1.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) {
+ t.Fatal("should've mentioned user1 and user4 with mention")
+ } else if ids, ok := mentions["First"]; !ok || len(ids) != 2 || (ids[0] != user2.Id && ids[1] != user2.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) {
+ t.Fatal("should've mentioned user2 and user4 with mention")
+ } else if ids, ok := mentions["@channel"]; !ok || len(ids) != 2 || (ids[0] != user3.Id && ids[1] != user3.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) {
+ t.Fatal("should've mentioned user3 and user4 with @channel")
+ } else if ids, ok := mentions["@all"]; !ok || len(ids) != 2 || (ids[0] != user3.Id && ids[1] != user3.Id) || (ids[0] != user4.Id && ids[1] != user4.Id) {
+ t.Fatal("should've mentioned user3 and user4 with @all")
+ }
+}
diff --git a/app/oauth.go b/app/oauth.go
new file mode 100644
index 000000000..3e8b0b8d2
--- /dev/null
+++ b/app/oauth.go
@@ -0,0 +1,34 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "github.com/mattermost/platform/model"
+)
+
+func RevokeAccessToken(token string) *model.AppError {
+
+ session, _ := GetSession(token)
+ schan := Srv.Store.Session().Remove(token)
+
+ if result := <-Srv.Store.OAuth().GetAccessData(token); result.Err != nil {
+ return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.get.app_error", nil, "")
+ }
+
+ tchan := Srv.Store.OAuth().RemoveAccessData(token)
+
+ if result := <-tchan; result.Err != nil {
+ return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_token.app_error", nil, "")
+ }
+
+ if result := <-schan; result.Err != nil {
+ return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_session.app_error", nil, "")
+ }
+
+ if session != nil {
+ ClearSessionCacheForUser(session.UserId)
+ }
+
+ return nil
+}
diff --git a/app/post.go b/app/post.go
new file mode 100644
index 000000000..6d34cc035
--- /dev/null
+++ b/app/post.go
@@ -0,0 +1,501 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "net/http"
+ "regexp"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/dyatlov/go-opengraph/opengraph"
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+ "github.com/mattermost/platform/utils"
+)
+
+func CreatePostAsUser(post *model.Post, teamId string) (*model.Post, *model.AppError) {
+ // Check that channel has not been deleted
+ var channel *model.Channel
+ if result := <-Srv.Store.Channel().Get(post.ChannelId, true); result.Err != nil {
+ err := model.NewLocAppError("CreatePostAsUser", "api.context.invalid_param.app_error", map[string]interface{}{"Name": "post.channel_id"}, result.Err.Error())
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ } else {
+ channel = result.Data.(*model.Channel)
+ }
+
+ if channel.DeleteAt != 0 {
+ err := model.NewLocAppError("createPost", "api.post.create_post.can_not_post_to_deleted.error", nil, "")
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ if rp, err := CreatePost(post, teamId, true); err != nil {
+ if err.Id == "api.post.create_post.root_id.app_error" ||
+ err.Id == "api.post.create_post.channel_root_id.app_error" ||
+ err.Id == "api.post.create_post.parent_id.app_error" {
+ err.StatusCode = http.StatusBadRequest
+ }
+
+ return nil, err
+ } else {
+ // Update the LastViewAt only if the post does not have from_webhook prop set (eg. Zapier app)
+ if _, ok := post.Props["from_webhook"]; !ok {
+ if result := <-Srv.Store.Channel().UpdateLastViewedAt([]string{post.ChannelId}, post.UserId); result.Err != nil {
+ l4g.Error(utils.T("api.post.create_post.last_viewed.error"), post.ChannelId, post.UserId, result.Err)
+ }
+ }
+
+ return rp, nil
+ }
+
+}
+
+func CreatePost(post *model.Post, teamId string, triggerWebhooks bool) (*model.Post, *model.AppError) {
+ var pchan store.StoreChannel
+ if len(post.RootId) > 0 {
+ pchan = Srv.Store.Post().Get(post.RootId)
+ }
+
+ // Verify the parent/child relationships are correct
+ if pchan != nil {
+ if presult := <-pchan; presult.Err != nil {
+ return nil, model.NewLocAppError("createPost", "api.post.create_post.root_id.app_error", nil, "")
+ } else {
+ list := presult.Data.(*model.PostList)
+ if len(list.Posts) == 0 || !list.IsChannelId(post.ChannelId) {
+ return nil, model.NewLocAppError("createPost", "api.post.create_post.channel_root_id.app_error", nil, "")
+ }
+
+ if post.ParentId == "" {
+ post.ParentId = post.RootId
+ }
+
+ if post.RootId != post.ParentId {
+ parent := list.Posts[post.ParentId]
+ if parent == nil {
+ return nil, model.NewLocAppError("createPost", "api.post.create_post.parent_id.app_error", nil, "")
+ }
+ }
+ }
+ }
+
+ post.Hashtags, _ = model.ParseHashtags(post.Message)
+
+ var rpost *model.Post
+ if result := <-Srv.Store.Post().Save(post); result.Err != nil {
+ return nil, result.Err
+ } else {
+ rpost = result.Data.(*model.Post)
+ }
+
+ if einterfaces.GetMetricsInterface() != nil {
+ einterfaces.GetMetricsInterface().IncrementPostCreate()
+ }
+
+ if len(post.FileIds) > 0 {
+ // There's a rare bug where the client sends up duplicate FileIds so protect against that
+ post.FileIds = utils.RemoveDuplicatesFromStringArray(post.FileIds)
+
+ for _, fileId := range post.FileIds {
+ if result := <-Srv.Store.FileInfo().AttachToPost(fileId, post.Id); result.Err != nil {
+ l4g.Error(utils.T("api.post.create_post.attach_files.error"), post.Id, post.FileIds, post.UserId, result.Err)
+ }
+ }
+
+ if einterfaces.GetMetricsInterface() != nil {
+ einterfaces.GetMetricsInterface().IncrementPostFileAttachment(len(post.FileIds))
+ }
+ }
+
+ InvalidateCacheForChannel(rpost.ChannelId)
+ InvalidateCacheForChannelPosts(rpost.ChannelId)
+
+ if err := handlePostEvents(rpost, teamId, triggerWebhooks); err != nil {
+ return nil, err
+ }
+
+ return rpost, nil
+}
+
+func handlePostEvents(post *model.Post, teamId string, triggerWebhooks bool) *model.AppError {
+ tchan := Srv.Store.Team().Get(teamId)
+ cchan := Srv.Store.Channel().Get(post.ChannelId, true)
+ uchan := Srv.Store.User().Get(post.UserId)
+
+ var team *model.Team
+ if result := <-tchan; result.Err != nil {
+ return result.Err
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ var channel *model.Channel
+ if result := <-cchan; result.Err != nil {
+ return result.Err
+ } else {
+ channel = result.Data.(*model.Channel)
+ }
+
+ var user *model.User
+ if result := <-uchan; result.Err != nil {
+ return result.Err
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ if _, err := SendNotifications(post, team, channel, user); err != nil {
+ return err
+ }
+
+ if triggerWebhooks {
+ go func() {
+ if err := handleWebhookEvents(post, team, channel, user); err != nil {
+ l4g.Error(err.Error())
+ }
+ }()
+ }
+
+ if channel.Type == model.CHANNEL_DIRECT {
+ go func() {
+ if err := MakeDirectChannelVisible(post.ChannelId); err != nil {
+ l4g.Error(err.Error())
+ }
+ }()
+ }
+
+ return nil
+}
+
+var linkWithTextRegex *regexp.Regexp = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
+
+// This method only parses and processes the attachments,
+// all else should be set in the post which is passed
+func parseSlackAttachment(post *model.Post, attachments interface{}) {
+ post.Type = model.POST_SLACK_ATTACHMENT
+
+ if list, success := attachments.([]interface{}); success {
+ for i, aInt := range list {
+ attachment := aInt.(map[string]interface{})
+ if aText, ok := attachment["text"].(string); ok {
+ aText = linkWithTextRegex.ReplaceAllString(aText, "[${2}](${1})")
+ attachment["text"] = aText
+ list[i] = attachment
+ }
+ if aText, ok := attachment["pretext"].(string); ok {
+ aText = linkWithTextRegex.ReplaceAllString(aText, "[${2}](${1})")
+ attachment["pretext"] = aText
+ list[i] = attachment
+ }
+ if fVal, ok := attachment["fields"]; ok {
+ if fields, ok := fVal.([]interface{}); ok {
+ // parse attachment field links into Markdown format
+ for j, fInt := range fields {
+ field := fInt.(map[string]interface{})
+ if fValue, ok := field["value"].(string); ok {
+ fValue = linkWithTextRegex.ReplaceAllString(fValue, "[${2}](${1})")
+ field["value"] = fValue
+ fields[j] = field
+ }
+ }
+ attachment["fields"] = fields
+ list[i] = attachment
+ }
+ }
+ }
+ post.AddProp("attachments", list)
+ }
+}
+
+func parseSlackLinksToMarkdown(text string) string {
+ return linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
+}
+
+func SendEphemeralPost(teamId, userId string, post *model.Post) *model.Post {
+ post.Type = model.POST_EPHEMERAL
+
+ // fill in fields which haven't been specified which have sensible defaults
+ if post.Id == "" {
+ post.Id = model.NewId()
+ }
+ if post.CreateAt == 0 {
+ post.CreateAt = model.GetMillis()
+ }
+ if post.Props == nil {
+ post.Props = model.StringInterface{}
+ }
+
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_EPHEMERAL_MESSAGE, "", post.ChannelId, userId, nil)
+ message.Add("post", post.ToJson())
+
+ go Publish(message)
+
+ return post
+}
+
+func UpdatePost(post *model.Post) (*model.Post, *model.AppError) {
+ if utils.IsLicensed {
+ if *utils.Cfg.ServiceSettings.AllowEditPost == model.ALLOW_EDIT_POST_NEVER {
+ err := model.NewLocAppError("updatePost", "api.post.update_post.permissions_denied.app_error", nil, "")
+ err.StatusCode = http.StatusForbidden
+ return nil, err
+ }
+ }
+
+ var oldPost *model.Post
+ if result := <-Srv.Store.Post().Get(post.Id); result.Err != nil {
+ return nil, result.Err
+ } else {
+ oldPost = result.Data.(*model.PostList).Posts[post.Id]
+
+ if oldPost == nil {
+ err := model.NewLocAppError("updatePost", "api.post.update_post.find.app_error", nil, "id="+post.Id)
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ if oldPost.UserId != post.UserId {
+ err := model.NewLocAppError("updatePost", "api.post.update_post.permissions.app_error", nil, "oldUserId="+oldPost.UserId)
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ if oldPost.DeleteAt != 0 {
+ err := model.NewLocAppError("updatePost", "api.post.update_post.permissions_details.app_error", map[string]interface{}{"PostId": post.Id}, "")
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ if oldPost.IsSystemMessage() {
+ err := model.NewLocAppError("updatePost", "api.post.update_post.system_message.app_error", nil, "id="+post.Id)
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ if utils.IsLicensed {
+ if *utils.Cfg.ServiceSettings.AllowEditPost == model.ALLOW_EDIT_POST_TIME_LIMIT && model.GetMillis() > oldPost.CreateAt+int64(*utils.Cfg.ServiceSettings.PostEditTimeLimit*1000) {
+ err := model.NewLocAppError("updatePost", "api.post.update_post.permissions_time_limit.app_error", map[string]interface{}{"timeLimit": *utils.Cfg.ServiceSettings.PostEditTimeLimit}, "")
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+ }
+ }
+
+ newPost := &model.Post{}
+ *newPost = *oldPost
+
+ newPost.Message = post.Message
+ newPost.EditAt = model.GetMillis()
+ newPost.Hashtags, _ = model.ParseHashtags(post.Message)
+
+ if result := <-Srv.Store.Post().Update(newPost, oldPost); result.Err != nil {
+ return nil, result.Err
+ } else {
+ rpost := result.Data.(*model.Post)
+
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", rpost.ChannelId, "", nil)
+ message.Add("post", rpost.ToJson())
+
+ go Publish(message)
+
+ InvalidateCacheForChannelPosts(rpost.ChannelId)
+
+ return rpost, nil
+ }
+}
+
+func GetPosts(channelId string, offset int, limit int) (*model.PostList, *model.AppError) {
+ if result := <-Srv.Store.Post().GetPosts(channelId, offset, limit, true); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.PostList), nil
+ }
+}
+
+func GetPostsEtag(channelId string) string {
+ return (<-Srv.Store.Post().GetEtag(channelId, true)).Data.(string)
+}
+
+func GetPostsSince(channelId string, time int64) (*model.PostList, *model.AppError) {
+ if result := <-Srv.Store.Post().GetPostsSince(channelId, time, true); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.PostList), nil
+ }
+}
+
+func GetSinglePost(postId string) (*model.Post, *model.AppError) {
+ if result := <-Srv.Store.Post().GetSingle(postId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.Post), nil
+ }
+}
+
+func GetPostThread(postId string) (*model.PostList, *model.AppError) {
+ if result := <-Srv.Store.Post().Get(postId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.PostList), nil
+ }
+}
+
+func GetFlaggedPosts(userId string, offset int, limit int) (*model.PostList, *model.AppError) {
+ if result := <-Srv.Store.Post().GetFlaggedPosts(userId, offset, limit); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.PostList), nil
+ }
+}
+
+func GetPermalinkPost(postId string, userId string) (*model.PostList, *model.AppError) {
+ if result := <-Srv.Store.Post().Get(postId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ list := result.Data.(*model.PostList)
+
+ if len(list.Order) != 1 {
+ return nil, model.NewLocAppError("getPermalinkTmp", "api.post_get_post_by_id.get.app_error", nil, "")
+ }
+ post := list.Posts[list.Order[0]]
+
+ var channel *model.Channel
+ var err *model.AppError
+ if channel, err = GetChannel(post.ChannelId); err != nil {
+ return nil, err
+ }
+
+ if err = JoinChannel(channel, userId); err != nil {
+ return nil, err
+ }
+
+ return list, nil
+ }
+}
+
+func GetPostsAroundPost(postId, channelId string, offset, limit int, before bool) (*model.PostList, *model.AppError) {
+ var pchan store.StoreChannel
+ if before {
+ pchan = Srv.Store.Post().GetPostsBefore(channelId, postId, limit, offset)
+ } else {
+ pchan = Srv.Store.Post().GetPostsAfter(channelId, postId, limit, offset)
+ }
+
+ if result := <-pchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.PostList), nil
+ }
+}
+
+func DeletePost(postId string) (*model.Post, *model.AppError) {
+ if result := <-Srv.Store.Post().GetSingle(postId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ post := result.Data.(*model.Post)
+
+ if result := <-Srv.Store.Post().Delete(postId, model.GetMillis()); result.Err != nil {
+ return nil, result.Err
+ }
+
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_DELETED, "", post.ChannelId, "", nil)
+ message.Add("post", post.ToJson())
+
+ go Publish(message)
+ go DeletePostFiles(post)
+ go DeleteFlaggedPosts(post.Id)
+
+ InvalidateCacheForChannelPosts(post.ChannelId)
+
+ return post, nil
+ }
+}
+
+func DeleteFlaggedPosts(postId string) {
+ if result := <-Srv.Store.Preference().DeleteCategoryAndName(model.PREFERENCE_CATEGORY_FLAGGED_POST, postId); result.Err != nil {
+ l4g.Warn(utils.T("api.post.delete_flagged_post.app_error.warn"), result.Err)
+ return
+ }
+}
+
+func DeletePostFiles(post *model.Post) {
+ if len(post.FileIds) != 0 {
+ return
+ }
+
+ if result := <-Srv.Store.FileInfo().DeleteForPost(post.Id); result.Err != nil {
+ l4g.Warn(utils.T("api.post.delete_post_files.app_error.warn"), post.Id, result.Err)
+ }
+}
+
+func SearchPostsInTeam(terms string, userId string, teamId string, isOrSearch bool) (*model.PostList, *model.AppError) {
+ paramsList := model.ParseSearchParams(terms)
+ channels := []store.StoreChannel{}
+
+ for _, params := range paramsList {
+ params.OrTerms = isOrSearch
+ // don't allow users to search for everything
+ if params.Terms != "*" {
+ channels = append(channels, Srv.Store.Post().Search(teamId, userId, params))
+ }
+ }
+
+ posts := &model.PostList{}
+ for _, channel := range channels {
+ if result := <-channel; result.Err != nil {
+ return nil, result.Err
+ } else {
+ data := result.Data.(*model.PostList)
+ posts.Extend(data)
+ }
+ }
+
+ return posts, nil
+}
+
+func GetFileInfosForPost(postId string) ([]*model.FileInfo, *model.AppError) {
+ pchan := Srv.Store.Post().Get(postId)
+ fchan := Srv.Store.FileInfo().GetForPost(postId)
+
+ var infos []*model.FileInfo
+ if result := <-fchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ infos = result.Data.([]*model.FileInfo)
+ }
+
+ if len(infos) == 0 {
+ // No FileInfos were returned so check if they need to be created for this post
+ var post *model.Post
+ if result := <-pchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ post = result.Data.(*model.PostList).Posts[postId]
+ }
+
+ if len(post.Filenames) > 0 {
+ // The post has Filenames that need to be replaced with FileInfos
+ infos = MigrateFilenamesToFileInfos(post)
+ }
+ }
+
+ return infos, nil
+}
+
+func GetOpenGraphMetadata(url string) *opengraph.OpenGraph {
+ og := opengraph.NewOpenGraph()
+
+ res, err := http.Get(url)
+ defer CloseBody(res)
+ if err != nil {
+ return og
+ }
+
+ if err := og.ProcessHTML(res.Body); err != nil {
+ return og
+ }
+
+ return og
+}
diff --git a/app/preference.go b/app/preference.go
new file mode 100644
index 000000000..4e492c4a8
--- /dev/null
+++ b/app/preference.go
@@ -0,0 +1,16 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "github.com/mattermost/platform/model"
+)
+
+func GetPreferencesForUser(userId string) (model.Preferences, *model.AppError) {
+ if result := <-Srv.Store.Preference().GetAll(userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(model.Preferences), nil
+ }
+}
diff --git a/app/saml.go b/app/saml.go
new file mode 100644
index 000000000..cc39d4540
--- /dev/null
+++ b/app/saml.go
@@ -0,0 +1,67 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "io"
+ "mime/multipart"
+ "net/http"
+ "os"
+
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func GetSamlMetadata() (string, *model.AppError) {
+ samlInterface := einterfaces.GetSamlInterface()
+
+ if samlInterface == nil {
+ err := model.NewLocAppError("GetSamlMetadata", "api.admin.saml.not_available.app_error", nil, "")
+ err.StatusCode = http.StatusNotImplemented
+ return "", err
+ }
+
+ if result, err := samlInterface.GetMetadata(); err != nil {
+ return "", model.NewLocAppError("GetSamlMetadata", "api.admin.saml.metadata.app_error", nil, "err="+err.Message)
+ } else {
+ return result, nil
+ }
+}
+
+func AddSamlCertificate(fileData *multipart.FileHeader) *model.AppError {
+ file, err := fileData.Open()
+ defer file.Close()
+ if err != nil {
+ return model.NewLocAppError("AddSamlCertificate", "api.admin.add_certificate.open.app_error", nil, err.Error())
+ }
+
+ out, err := os.Create(utils.FindDir("config") + fileData.Filename)
+ if err != nil {
+ return model.NewLocAppError("AddSamlCertificate", "api.admin.add_certificate.saving.app_error", nil, err.Error())
+ }
+ defer out.Close()
+
+ io.Copy(out, file)
+ return nil
+}
+
+func RemoveSamlCertificate(filename string) *model.AppError {
+ if err := os.Remove(utils.FindConfigFile(filename)); err != nil {
+ return model.NewLocAppError("removeCertificate", "api.admin.remove_certificate.delete.app_error",
+ map[string]interface{}{"Filename": filename}, err.Error())
+ }
+
+ return nil
+}
+
+func GetSamlCertificateStatus() map[string]interface{} {
+ status := make(map[string]interface{})
+
+ status["IdpCertificateFile"] = utils.FileExistsInConfigFolder(*utils.Cfg.SamlSettings.IdpCertificateFile)
+ status["PrivateKeyFile"] = utils.FileExistsInConfigFolder(*utils.Cfg.SamlSettings.PrivateKeyFile)
+ status["PublicCertificateFile"] = utils.FileExistsInConfigFolder(*utils.Cfg.SamlSettings.PublicCertificateFile)
+
+ return status
+}
diff --git a/api/server.go b/app/server.go
index 2698f159c..972c91ea3 100644
--- a/api/server.go
+++ b/app/server.go
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package api
+package app
import (
"crypto/tls"
@@ -23,15 +23,50 @@ import (
)
type Server struct {
- Store store.Store
- Router *mux.Router
- GracefulServer *graceful.Server
+ Store store.Store
+ WebSocketRouter *WebSocketRouter
+ Router *mux.Router
+ GracefulServer *graceful.Server
+}
+
+var allowedMethods []string = []string{
+ "POST",
+ "GET",
+ "OPTIONS",
+ "PUT",
+ "PATCH",
+ "DELETE",
}
type CorsWrapper struct {
router *mux.Router
}
+func (cw *CorsWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if len(*utils.Cfg.ServiceSettings.AllowCorsFrom) > 0 {
+ origin := r.Header.Get("Origin")
+ if *utils.Cfg.ServiceSettings.AllowCorsFrom == "*" || strings.Contains(*utils.Cfg.ServiceSettings.AllowCorsFrom, origin) {
+ w.Header().Set("Access-Control-Allow-Origin", origin)
+
+ if r.Method == "OPTIONS" {
+ w.Header().Set(
+ "Access-Control-Allow-Methods",
+ strings.Join(allowedMethods, ", "))
+
+ w.Header().Set(
+ "Access-Control-Allow-Headers",
+ r.Header.Get("Access-Control-Request-Headers"))
+ }
+ }
+ }
+
+ if r.Method == "OPTIONS" {
+ return
+ }
+
+ cw.router.ServeHTTP(w, r)
+}
+
const TIME_TO_WAIT_FOR_CONNECTIONS_TO_CLOSE_ON_SERVER_SHUTDOWN = time.Second
var Srv *Server
@@ -46,15 +81,10 @@ func InitStores() {
Srv.Store = store.NewSqlStore()
}
-func InitRouter() {
- Srv.Router = mux.NewRouter()
- Srv.Router.NotFoundHandler = http.HandlerFunc(Handle404)
-}
-
type VaryBy struct{}
func (m *VaryBy) Key(r *http.Request) string {
- return GetIpAddress(r)
+ return utils.GetIpAddress(r)
}
func initalizeThrottledVaryBy() *throttled.VaryBy {
@@ -116,7 +146,7 @@ func StartServer() {
RateLimiter: rateLimiter,
VaryBy: &VaryBy{},
DeniedHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- l4g.Error("%v: Denied due to throttling settings code=429 ip=%v", r.URL.Path, GetIpAddress(r))
+ l4g.Error("%v: Denied due to throttling settings code=429 ip=%v", r.URL.Path, utils.GetIpAddress(r))
throttled.DefaultDeniedHandler.ServeHTTP(w, r)
}),
}
diff --git a/app/session.go b/app/session.go
new file mode 100644
index 000000000..83e5f343a
--- /dev/null
+++ b/app/session.go
@@ -0,0 +1,180 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+
+ l4g "github.com/alecthomas/log4go"
+)
+
+var sessionCache *utils.Cache = utils.NewLru(model.SESSION_CACHE_SIZE)
+
+func CreateSession(session *model.Session) (*model.Session, *model.AppError) {
+ if result := <-Srv.Store.Session().Save(session); result.Err != nil {
+ return nil, result.Err
+ } else {
+ session := result.Data.(*model.Session)
+
+ AddSessionToCache(session)
+
+ return session, nil
+ }
+}
+
+func GetSession(token string) (*model.Session, *model.AppError) {
+ metrics := einterfaces.GetMetricsInterface()
+
+ var session *model.Session
+ if ts, ok := sessionCache.Get(token); ok {
+ session = ts.(*model.Session)
+ if metrics != nil {
+ metrics.IncrementMemCacheHitCounter("Session")
+ }
+ } else {
+ if metrics != nil {
+ metrics.IncrementMemCacheMissCounter("Session")
+ }
+ }
+
+ if session == nil {
+ if sessionResult := <-Srv.Store.Session().Get(token); sessionResult.Err != nil {
+ return nil, model.NewLocAppError("GetSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token, "Error": sessionResult.Err.DetailedError}, "")
+ } else {
+ session = sessionResult.Data.(*model.Session)
+
+ if session.IsExpired() || session.Token != token {
+ return nil, model.NewLocAppError("GetSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token, "Error": sessionResult.Err.DetailedError}, "")
+ } else {
+ AddSessionToCache(session)
+ return session, nil
+ }
+ }
+ }
+
+ if session == nil || session.IsExpired() {
+ return nil, model.NewLocAppError("GetSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token}, "")
+ }
+
+ return session, nil
+}
+
+func GetSessions(userId string) ([]*model.Session, *model.AppError) {
+ if result := <-Srv.Store.Session().GetSessions(userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.Session), nil
+ }
+}
+
+func RevokeAllSessions(userId string) *model.AppError {
+ if result := <-Srv.Store.Session().GetSessions(userId); result.Err != nil {
+ return result.Err
+ } else {
+ sessions := result.Data.([]*model.Session)
+
+ for _, session := range sessions {
+ if session.IsOAuth {
+ RevokeAccessToken(session.Token)
+ } else {
+ if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
+ return result.Err
+ }
+ }
+
+ RevokeWebrtcToken(session.Id)
+ }
+ }
+
+ ClearSessionCacheForUser(userId)
+
+ return nil
+}
+
+func ClearSessionCacheForUser(userId string) {
+
+ ClearSessionCacheForUserSkipClusterSend(userId)
+
+ if einterfaces.GetClusterInterface() != nil {
+ einterfaces.GetClusterInterface().ClearSessionCacheForUser(userId)
+ }
+}
+
+func ClearSessionCacheForUserSkipClusterSend(userId string) {
+ keys := sessionCache.Keys()
+
+ for _, key := range keys {
+ if ts, ok := sessionCache.Get(key); ok {
+ session := ts.(*model.Session)
+ if session.UserId == userId {
+ sessionCache.Remove(key)
+ }
+ }
+ }
+
+ InvalidateWebConnSessionCacheForUser(userId)
+
+}
+
+func AddSessionToCache(session *model.Session) {
+ sessionCache.AddWithExpiresInSecs(session.Token, session, int64(*utils.Cfg.ServiceSettings.SessionCacheInMinutes*60))
+}
+
+func SessionCacheLength() int {
+ return sessionCache.Len()
+}
+
+func RevokeSessionsForDeviceId(userId string, deviceId string, currentSessionId string) *model.AppError {
+ if result := <-Srv.Store.Session().GetSessions(userId); result.Err != nil {
+ return result.Err
+ } else {
+ sessions := result.Data.([]*model.Session)
+ for _, session := range sessions {
+ if session.DeviceId == deviceId && session.Id != currentSessionId {
+ l4g.Debug(utils.T("api.user.login.revoking.app_error"), session.Id, userId)
+ if err := RevokeSession(session); err != nil {
+ // Soft error so we still remove the other sessions
+ l4g.Error(err.Error())
+ }
+ }
+ }
+ }
+
+ return nil
+}
+
+func RevokeSessionById(sessionId string) *model.AppError {
+ if result := <-Srv.Store.Session().Get(sessionId); result.Err != nil {
+ return result.Err
+ } else {
+ return RevokeSession(result.Data.(*model.Session))
+ }
+}
+
+func RevokeSession(session *model.Session) *model.AppError {
+ if session.IsOAuth {
+ if err := RevokeAccessToken(session.Token); err != nil {
+ return err
+ }
+ } else {
+ if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
+ return result.Err
+ }
+ }
+
+ RevokeWebrtcToken(session.Id)
+ ClearSessionCacheForUser(session.UserId)
+
+ return nil
+}
+
+func AttachDeviceId(sessionId string, deviceId string, expiresAt int64) *model.AppError {
+ if result := <-Srv.Store.Session().UpdateDeviceId(sessionId, deviceId, expiresAt); result.Err != nil {
+ return result.Err
+ }
+
+ return nil
+}
diff --git a/app/session_test.go b/app/session_test.go
new file mode 100644
index 000000000..aea31cf86
--- /dev/null
+++ b/app/session_test.go
@@ -0,0 +1,31 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "github.com/mattermost/platform/model"
+ "testing"
+)
+
+func TestCache(t *testing.T) {
+ session := &model.Session{
+ Id: model.NewId(),
+ Token: model.NewId(),
+ UserId: model.NewId(),
+ }
+
+ sessionCache.AddWithExpiresInSecs(session.Token, session, 5*60)
+
+ keys := sessionCache.Keys()
+ if len(keys) <= 0 {
+ t.Fatal("should have items")
+ }
+
+ ClearSessionCacheForUser(session.UserId)
+
+ rkeys := sessionCache.Keys()
+ if len(rkeys) != len(keys)-1 {
+ t.Fatal("should have one less")
+ }
+}
diff --git a/api/slackimport.go b/app/slackimport.go
index 3fd0ec3f6..508803126 100644
--- a/api/slackimport.go
+++ b/app/slackimport.go
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package api
+package app
import (
"archive/zip"
@@ -330,6 +330,54 @@ func SlackAddPosts(teamId string, channel *model.Channel, posts []SlackPost, use
CreateAt: SlackConvertTimeStamp(sPost.TimeStamp),
}
ImportPost(&newPost)
+ case sPost.Type == "message" && sPost.SubType == "channel_topic":
+ if sPost.User == "" {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.msg_no_usr.debug"))
+ continue
+ } else if users[sPost.User] == nil {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.user_no_exists.debug"), sPost.User)
+ continue
+ }
+ newPost := model.Post{
+ UserId: users[sPost.User].Id,
+ ChannelId: channel.Id,
+ Message: sPost.Text,
+ CreateAt: SlackConvertTimeStamp(sPost.TimeStamp),
+ Type: model.POST_HEADER_CHANGE,
+ }
+ ImportPost(&newPost)
+ case sPost.Type == "message" && sPost.SubType == "channel_purpose":
+ if sPost.User == "" {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.msg_no_usr.debug"))
+ continue
+ } else if users[sPost.User] == nil {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.user_no_exists.debug"), sPost.User)
+ continue
+ }
+ newPost := model.Post{
+ UserId: users[sPost.User].Id,
+ ChannelId: channel.Id,
+ Message: sPost.Text,
+ CreateAt: SlackConvertTimeStamp(sPost.TimeStamp),
+ Type: model.POST_PURPOSE_CHANGE,
+ }
+ ImportPost(&newPost)
+ case sPost.Type == "message" && sPost.SubType == "channel_name":
+ if sPost.User == "" {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.msg_no_usr.debug"))
+ continue
+ } else if users[sPost.User] == nil {
+ l4g.Debug(utils.T("api.slackimport.slack_add_posts.user_no_exists.debug"), sPost.User)
+ continue
+ }
+ newPost := model.Post{
+ UserId: users[sPost.User].Id,
+ ChannelId: channel.Id,
+ Message: sPost.Text,
+ CreateAt: SlackConvertTimeStamp(sPost.TimeStamp),
+ Type: model.POST_DISPLAYNAME_CHANGE,
+ }
+ ImportPost(&newPost)
default:
l4g.Warn(utils.T("api.slackimport.slack_add_posts.unsupported.warn"), sPost.Type, sPost.SubType)
}
@@ -422,18 +470,28 @@ func SlackAddChannels(teamId string, slackchannels []SlackChannel, posts map[str
Header: sChannel.Topic["value"],
}
newChannel = SlackSanitiseChannelProperties(newChannel)
- mChannel := ImportChannel(&newChannel)
+
+ var mChannel *model.Channel
+ if result := <-Srv.Store.Channel().GetByName(teamId, sChannel.Name); result.Err == nil {
+ // The channel already exists as an active channel. Merge with the existing one.
+ mChannel = result.Data.(*model.Channel)
+ log.WriteString(utils.T("api.slackimport.slack_add_channels.merge", map[string]interface{}{"DisplayName": newChannel.DisplayName}))
+ } else if result := <-Srv.Store.Channel().GetDeletedByName(teamId, sChannel.Name); result.Err == nil {
+ // The channel already exists but has been deleted. Generate a random string for the handle instead.
+ newChannel.Name = model.NewId()
+ newChannel = SlackSanitiseChannelProperties(newChannel)
+ }
+
if mChannel == nil {
- // Maybe it already exists?
- if result := <-Srv.Store.Channel().GetByName(teamId, sChannel.Name); result.Err != nil {
+ // Haven't found an existing channel to merge with. Try importing it as a new one.
+ mChannel = ImportChannel(&newChannel)
+ if mChannel == nil {
l4g.Warn(utils.T("api.slackimport.slack_add_channels.import_failed.warn"), newChannel.DisplayName)
log.WriteString(utils.T("api.slackimport.slack_add_channels.import_failed", map[string]interface{}{"DisplayName": newChannel.DisplayName}))
continue
- } else {
- mChannel = result.Data.(*model.Channel)
- log.WriteString(utils.T("api.slackimport.slack_add_channels.merge", map[string]interface{}{"DisplayName": newChannel.DisplayName}))
}
}
+
addSlackUsersToChannel(sChannel.Members, users, mChannel, log)
log.WriteString(newChannel.DisplayName + "\r\n")
addedChannels[sChannel.Id] = mChannel
diff --git a/api/slackimport_test.go b/app/slackimport_test.go
index efe6e635f..3389c5217 100644
--- a/api/slackimport_test.go
+++ b/app/slackimport_test.go
@@ -1,7 +1,7 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package api
+package app
import (
"github.com/mattermost/platform/model"
diff --git a/app/status.go b/app/status.go
new file mode 100644
index 000000000..98cdb0dc0
--- /dev/null
+++ b/app/status.go
@@ -0,0 +1,255 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ l4g "github.com/alecthomas/log4go"
+
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+ "github.com/mattermost/platform/utils"
+)
+
+var statusCache *utils.Cache = utils.NewLru(model.STATUS_CACHE_SIZE)
+
+func ClearStatusCache() {
+ statusCache.Purge()
+}
+
+func AddStatusCacheSkipClusterSend(status *model.Status) {
+ statusCache.Add(status.UserId, status)
+}
+
+func AddStatusCache(status *model.Status) {
+ AddStatusCacheSkipClusterSend(status)
+
+ if einterfaces.GetClusterInterface() != nil {
+ einterfaces.GetClusterInterface().UpdateStatus(status)
+ }
+}
+
+func GetAllStatuses() map[string]*model.Status {
+ userIds := statusCache.Keys()
+ statusMap := map[string]*model.Status{}
+
+ for _, userId := range userIds {
+ if id, ok := userId.(string); !ok {
+ continue
+ } else {
+ status := GetStatusFromCache(id)
+ if status != nil {
+ statusMap[id] = status
+ }
+ }
+ }
+
+ return statusMap
+}
+
+func GetStatusesByIds(userIds []string) (map[string]interface{}, *model.AppError) {
+ statusMap := map[string]interface{}{}
+ metrics := einterfaces.GetMetricsInterface()
+
+ missingUserIds := []string{}
+ for _, userId := range userIds {
+ if result, ok := statusCache.Get(userId); ok {
+ statusMap[userId] = result.(*model.Status).Status
+ if metrics != nil {
+ metrics.IncrementMemCacheHitCounter("Status")
+ }
+ } else {
+ missingUserIds = append(missingUserIds, userId)
+ if metrics != nil {
+ metrics.IncrementMemCacheMissCounter("Status")
+ }
+ }
+ }
+
+ if len(missingUserIds) > 0 {
+ if result := <-Srv.Store.Status().GetByIds(missingUserIds); result.Err != nil {
+ return nil, result.Err
+ } else {
+ statuses := result.Data.([]*model.Status)
+
+ for _, s := range statuses {
+ AddStatusCache(s)
+ statusMap[s.UserId] = s.Status
+ }
+ }
+ }
+
+ // For the case where the user does not have a row in the Status table and cache
+ for _, userId := range missingUserIds {
+ if _, ok := statusMap[userId]; !ok {
+ statusMap[userId] = model.STATUS_OFFLINE
+ }
+ }
+
+ return statusMap, nil
+}
+
+func SetStatusOnline(userId string, sessionId string, manual bool) {
+ broadcast := false
+
+ var oldStatus string = model.STATUS_OFFLINE
+ var oldTime int64 = 0
+ var oldManual bool = false
+ var status *model.Status
+ var err *model.AppError
+
+ if status, err = GetStatus(userId); err != nil {
+ status = &model.Status{userId, model.STATUS_ONLINE, false, model.GetMillis(), ""}
+ broadcast = true
+ } else {
+ if status.Manual && !manual {
+ return // manually set status always overrides non-manual one
+ }
+
+ if status.Status != model.STATUS_ONLINE {
+ broadcast = true
+ }
+
+ oldStatus = status.Status
+ oldTime = status.LastActivityAt
+ oldManual = status.Manual
+
+ status.Status = model.STATUS_ONLINE
+ status.Manual = false // for "online" there's no manual setting
+ status.LastActivityAt = model.GetMillis()
+ }
+
+ AddStatusCache(status)
+
+ // Only update the database if the status has changed, the status has been manually set,
+ // or enough time has passed since the previous action
+ if status.Status != oldStatus || status.Manual != oldManual || status.LastActivityAt-oldTime > model.STATUS_MIN_UPDATE_TIME {
+ achan := Srv.Store.Session().UpdateLastActivityAt(sessionId, status.LastActivityAt)
+
+ var schan store.StoreChannel
+ if broadcast {
+ schan = Srv.Store.Status().SaveOrUpdate(status)
+ } else {
+ schan = Srv.Store.Status().UpdateLastActivityAt(status.UserId, status.LastActivityAt)
+ }
+
+ if result := <-achan; result.Err != nil {
+ l4g.Error(utils.T("api.status.last_activity.error"), userId, sessionId, result.Err)
+ }
+
+ if result := <-schan; result.Err != nil {
+ l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
+ }
+ }
+
+ if broadcast {
+ event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil)
+ event.Add("status", model.STATUS_ONLINE)
+ event.Add("user_id", status.UserId)
+ go Publish(event)
+ }
+}
+
+func SetStatusOffline(userId string, manual bool) {
+ status, err := GetStatus(userId)
+ if err == nil && status.Manual && !manual {
+ return // manually set status always overrides non-manual one
+ }
+
+ status = &model.Status{userId, model.STATUS_OFFLINE, manual, model.GetMillis(), ""}
+
+ AddStatusCache(status)
+
+ if result := <-Srv.Store.Status().SaveOrUpdate(status); result.Err != nil {
+ l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
+ }
+
+ event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil)
+ event.Add("status", model.STATUS_OFFLINE)
+ event.Add("user_id", status.UserId)
+ go Publish(event)
+}
+
+func SetStatusAwayIfNeeded(userId string, manual bool) {
+ status, err := GetStatus(userId)
+
+ if err != nil {
+ status = &model.Status{userId, model.STATUS_OFFLINE, manual, 0, ""}
+ }
+
+ if !manual && status.Manual {
+ return // manually set status always overrides non-manual one
+ }
+
+ if !manual {
+ if status.Status == model.STATUS_AWAY {
+ return
+ }
+
+ if !IsUserAway(status.LastActivityAt) {
+ return
+ }
+ }
+
+ status.Status = model.STATUS_AWAY
+ status.Manual = manual
+ status.ActiveChannel = ""
+
+ AddStatusCache(status)
+
+ if result := <-Srv.Store.Status().SaveOrUpdate(status); result.Err != nil {
+ l4g.Error(utils.T("api.status.save_status.error"), userId, result.Err)
+ }
+
+ event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_STATUS_CHANGE, "", "", status.UserId, nil)
+ event.Add("status", model.STATUS_AWAY)
+ event.Add("user_id", status.UserId)
+ go Publish(event)
+}
+
+func GetStatusFromCache(userId string) *model.Status {
+ if result, ok := statusCache.Get(userId); ok {
+ status := result.(*model.Status)
+ statusCopy := &model.Status{}
+ *statusCopy = *status
+ return statusCopy
+ }
+
+ return nil
+}
+
+func GetStatus(userId string) (*model.Status, *model.AppError) {
+ status := GetStatusFromCache(userId)
+ if status != nil {
+ return status, nil
+ }
+
+ if result := <-Srv.Store.Status().Get(userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.Status), nil
+ }
+}
+
+func IsUserAway(lastActivityAt int64) bool {
+ return model.GetMillis()-lastActivityAt >= *utils.Cfg.TeamSettings.UserStatusAwayTimeout*1000
+}
+
+func DoesStatusAllowPushNotification(user *model.User, status *model.Status, channelId string) bool {
+ props := user.NotifyProps
+
+ if props["push"] == "none" {
+ return false
+ }
+
+ if pushStatus, ok := props["push_status"]; (pushStatus == model.STATUS_ONLINE || !ok) && (status.ActiveChannel != channelId || model.GetMillis()-status.LastActivityAt > model.STATUS_CHANNEL_TIMEOUT) {
+ return true
+ } else if pushStatus == model.STATUS_AWAY && (status.Status == model.STATUS_AWAY || status.Status == model.STATUS_OFFLINE) {
+ return true
+ } else if pushStatus == model.STATUS_OFFLINE && status.Status == model.STATUS_OFFLINE {
+ return true
+ }
+
+ return false
+}
diff --git a/app/team.go b/app/team.go
new file mode 100644
index 000000000..aabdc0bfd
--- /dev/null
+++ b/app/team.go
@@ -0,0 +1,563 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "fmt"
+ "net/http"
+ "strconv"
+ "strings"
+
+ l4g "github.com/alecthomas/log4go"
+
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func CreateTeam(team *model.Team) (*model.Team, *model.AppError) {
+ if result := <-Srv.Store.Team().Save(team); result.Err != nil {
+ return nil, result.Err
+ } else {
+ rteam := result.Data.(*model.Team)
+
+ if _, err := CreateDefaultChannels(rteam.Id); err != nil {
+ return nil, err
+ }
+
+ return rteam, nil
+ }
+}
+
+func CreateTeamWithUser(team *model.Team, userId string) (*model.Team, *model.AppError) {
+ var user *model.User
+ var err *model.AppError
+ if user, err = GetUser(userId); err != nil {
+ return nil, err
+ } else {
+ team.Email = user.Email
+ }
+
+ if !isTeamEmailAllowed(user) {
+ return nil, model.NewLocAppError("isTeamEmailAllowed", "api.team.is_team_creation_allowed.domain.app_error", nil, "")
+ }
+
+ var rteam *model.Team
+ if rteam, err = CreateTeam(team); err != nil {
+ return nil, err
+ }
+
+ if err = JoinUserToTeam(rteam, user); err != nil {
+ return nil, err
+ }
+
+ return rteam, nil
+}
+
+func isTeamEmailAllowed(user *model.User) bool {
+ email := strings.ToLower(user.Email)
+
+ if len(user.AuthService) > 0 && len(*user.AuthData) > 0 {
+ return true
+ }
+
+ // commas and @ signs are optional
+ // can be in the form of "@corp.mattermost.com, mattermost.com mattermost.org" -> corp.mattermost.com mattermost.com mattermost.org
+ domains := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(utils.Cfg.TeamSettings.RestrictCreationToDomains, "@", " ", -1), ",", " ", -1))))
+
+ matched := false
+ for _, d := range domains {
+ if strings.HasSuffix(email, "@"+d) {
+ matched = true
+ break
+ }
+ }
+
+ if len(utils.Cfg.TeamSettings.RestrictCreationToDomains) > 0 && !matched {
+ return false
+ }
+
+ return true
+}
+
+func UpdateTeam(team *model.Team) (*model.Team, *model.AppError) {
+ var oldTeam *model.Team
+ var err *model.AppError
+ if oldTeam, err = GetTeam(team.Id); err != nil {
+ return nil, err
+ }
+
+ oldTeam.DisplayName = team.DisplayName
+ oldTeam.Description = team.Description
+ oldTeam.InviteId = team.InviteId
+ oldTeam.AllowOpenInvite = team.AllowOpenInvite
+ oldTeam.CompanyName = team.CompanyName
+ oldTeam.AllowedDomains = team.AllowedDomains
+
+ if result := <-Srv.Store.Team().Update(oldTeam); result.Err != nil {
+ return nil, result.Err
+ }
+
+ oldTeam.Sanitize()
+
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_UPDATE_TEAM, "", "", "", nil)
+ message.Add("team", oldTeam.ToJson())
+ go Publish(message)
+
+ return oldTeam, nil
+}
+
+func UpdateTeamMemberRoles(teamId string, userId string, newRoles string) (*model.TeamMember, *model.AppError) {
+ var member *model.TeamMember
+ if result := <-Srv.Store.Team().GetTeamsForUser(userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ members := result.Data.([]*model.TeamMember)
+ for _, m := range members {
+ if m.TeamId == teamId {
+ member = m
+ }
+ }
+ }
+
+ if member == nil {
+ err := model.NewLocAppError("UpdateTeamMemberRoles", "api.team.update_member_roles.not_a_member", nil, "userId="+userId+" teamId="+teamId)
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ member.Roles = newRoles
+
+ if result := <-Srv.Store.Team().UpdateMember(member); result.Err != nil {
+ return nil, result.Err
+ }
+
+ ClearSessionCacheForUser(userId)
+
+ return member, nil
+}
+
+func AddUserToTeam(teamId string, userId string) (*model.Team, *model.AppError) {
+ tchan := Srv.Store.Team().Get(teamId)
+ uchan := Srv.Store.User().Get(userId)
+
+ var team *model.Team
+ if result := <-tchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ var user *model.User
+ if result := <-uchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ if err := JoinUserToTeam(team, user); err != nil {
+ return nil, err
+ }
+
+ return team, nil
+}
+
+func AddUserToTeamByTeamId(teamId string, user *model.User) *model.AppError {
+ if result := <-Srv.Store.Team().Get(teamId); result.Err != nil {
+ return result.Err
+ } else {
+ return JoinUserToTeam(result.Data.(*model.Team), user)
+ }
+}
+
+func AddUserToTeamByHash(userId string, hash string, data string) (*model.Team, *model.AppError) {
+ props := model.MapFromJson(strings.NewReader(data))
+
+ if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
+ return nil, model.NewLocAppError("JoinUserToTeamByHash", "api.user.create_user.signup_link_invalid.app_error", nil, "")
+ }
+
+ t, timeErr := strconv.ParseInt(props["time"], 10, 64)
+ if timeErr != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours
+ return nil, model.NewLocAppError("JoinUserToTeamByHash", "api.user.create_user.signup_link_expired.app_error", nil, "")
+ }
+
+ tchan := Srv.Store.Team().Get(props["id"])
+ uchan := Srv.Store.User().Get(userId)
+
+ var team *model.Team
+ if result := <-tchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ var user *model.User
+ if result := <-uchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ if err := JoinUserToTeam(team, user); err != nil {
+ return nil, err
+ }
+
+ return team, nil
+}
+
+func AddUserToTeamByInviteId(inviteId string, userId string) (*model.Team, *model.AppError) {
+ tchan := Srv.Store.Team().GetByInviteId(inviteId)
+ uchan := Srv.Store.User().Get(userId)
+
+ var team *model.Team
+ if result := <-tchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ var user *model.User
+ if result := <-uchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ if err := JoinUserToTeam(team, user); err != nil {
+ return nil, err
+ }
+
+ return team, nil
+}
+
+func JoinUserToTeam(team *model.Team, user *model.User) *model.AppError {
+
+ tm := &model.TeamMember{
+ TeamId: team.Id,
+ UserId: user.Id,
+ Roles: model.ROLE_TEAM_USER.Id,
+ }
+
+ channelRole := model.ROLE_CHANNEL_USER.Id
+
+ if team.Email == user.Email {
+ tm.Roles = model.ROLE_TEAM_USER.Id + " " + model.ROLE_TEAM_ADMIN.Id
+ channelRole = model.ROLE_CHANNEL_USER.Id + " " + model.ROLE_CHANNEL_ADMIN.Id
+ }
+
+ if etmr := <-Srv.Store.Team().GetMember(team.Id, user.Id); etmr.Err == nil {
+ // Membership alredy exists. Check if deleted and and update, otherwise do nothing
+ rtm := etmr.Data.(*model.TeamMember)
+
+ // Do nothing if already added
+ if rtm.DeleteAt == 0 {
+ return nil
+ }
+
+ if tmr := <-Srv.Store.Team().UpdateMember(tm); tmr.Err != nil {
+ return tmr.Err
+ }
+ } else {
+ // Membership appears to be missing. Lets try to add.
+ if tmr := <-Srv.Store.Team().SaveMember(tm); tmr.Err != nil {
+ return tmr.Err
+ }
+ }
+
+ if uua := <-Srv.Store.User().UpdateUpdateAt(user.Id); uua.Err != nil {
+ return uua.Err
+ }
+
+ // Soft error if there is an issue joining the default channels
+ if err := JoinDefaultChannels(team.Id, user, channelRole); err != nil {
+ l4g.Error(utils.T("api.user.create_user.joining.error"), user.Id, team.Id, err)
+ }
+
+ ClearSessionCacheForUser(user.Id)
+ InvalidateCacheForUser(user.Id)
+
+ return nil
+}
+
+func GetTeam(teamId string) (*model.Team, *model.AppError) {
+ if result := <-Srv.Store.Team().Get(teamId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.Team), nil
+ }
+}
+
+func GetTeamByName(name string) (*model.Team, *model.AppError) {
+ if result := <-Srv.Store.Team().GetByName(name); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.Team), nil
+ }
+}
+
+func GetTeamByInviteId(inviteId string) (*model.Team, *model.AppError) {
+ if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.Team), nil
+ }
+}
+
+func GetAllTeams() ([]*model.Team, *model.AppError) {
+ if result := <-Srv.Store.Team().GetAll(); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.Team), nil
+ }
+}
+
+func GetAllOpenTeams() ([]*model.Team, *model.AppError) {
+ if result := <-Srv.Store.Team().GetAllTeamListing(); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.Team), nil
+ }
+}
+
+func GetTeamsForUser(userId string) ([]*model.Team, *model.AppError) {
+ if result := <-Srv.Store.Team().GetTeamsByUserId(userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.Team), nil
+ }
+}
+
+func GetTeamMember(teamId, userId string) (*model.TeamMember, *model.AppError) {
+ if result := <-Srv.Store.Team().GetMember(teamId, userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.TeamMember), nil
+ }
+}
+
+func GetTeamMembersForUser(userId string) ([]*model.TeamMember, *model.AppError) {
+ if result := <-Srv.Store.Team().GetTeamsForUser(userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.TeamMember), nil
+ }
+}
+
+func GetTeamMembers(teamId string, offset int, limit int) ([]*model.TeamMember, *model.AppError) {
+ if result := <-Srv.Store.Team().GetMembers(teamId, offset, limit); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.TeamMember), nil
+ }
+}
+
+func GetTeamMembersByIds(teamId string, userIds []string) ([]*model.TeamMember, *model.AppError) {
+ if result := <-Srv.Store.Team().GetMembersByIds(teamId, userIds); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.TeamMember), nil
+ }
+}
+
+func RemoveUserFromTeam(teamId string, userId string) *model.AppError {
+ tchan := Srv.Store.Team().Get(teamId)
+ uchan := Srv.Store.User().Get(userId)
+
+ var team *model.Team
+ if result := <-tchan; result.Err != nil {
+ return result.Err
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ var user *model.User
+ if result := <-uchan; result.Err != nil {
+ return result.Err
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ if err := LeaveTeam(team, user); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func LeaveTeam(team *model.Team, user *model.User) *model.AppError {
+ var teamMember *model.TeamMember
+ var err *model.AppError
+
+ if teamMember, err = GetTeamMember(team.Id, user.Id); err != nil {
+ return model.NewLocAppError("LeaveTeam", "api.team.remove_user_from_team.missing.app_error", nil, err.Error())
+ }
+
+ var channelList *model.ChannelList
+
+ if result := <-Srv.Store.Channel().GetChannels(team.Id, user.Id); result.Err != nil {
+ if result.Err.Id == "store.sql_channel.get_channels.not_found.app_error" {
+ channelList = &model.ChannelList{}
+ } else {
+ return result.Err
+ }
+
+ } else {
+ channelList = result.Data.(*model.ChannelList)
+ }
+
+ for _, channel := range *channelList {
+ if channel.Type != model.CHANNEL_DIRECT {
+ InvalidateCacheForChannel(channel.Id)
+ if result := <-Srv.Store.Channel().RemoveMember(channel.Id, user.Id); result.Err != nil {
+ return result.Err
+ }
+ }
+ }
+
+ // Send the websocket message before we actually do the remove so the user being removed gets it.
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_LEAVE_TEAM, team.Id, "", "", nil)
+ message.Add("user_id", user.Id)
+ message.Add("team_id", team.Id)
+ Publish(message)
+
+ teamMember.Roles = ""
+ teamMember.DeleteAt = model.GetMillis()
+
+ if result := <-Srv.Store.Team().UpdateMember(teamMember); result.Err != nil {
+ return result.Err
+ }
+
+ if uua := <-Srv.Store.User().UpdateUpdateAt(user.Id); uua.Err != nil {
+ return uua.Err
+ }
+
+ // delete the preferences that set the last channel used in the team and other team specific preferences
+ if result := <-Srv.Store.Preference().DeleteCategory(user.Id, team.Id); result.Err != nil {
+ return result.Err
+ }
+
+ ClearSessionCacheForUser(user.Id)
+ InvalidateCacheForUser(user.Id)
+
+ return nil
+}
+
+func InviteNewUsersToTeam(emailList []string, teamId, senderId, siteURL string) *model.AppError {
+ if len(emailList) == 0 {
+ err := model.NewLocAppError("InviteNewUsersToTeam", "api.team.invite_members.no_one.app_error", nil, "")
+ err.StatusCode = http.StatusBadRequest
+ return err
+ }
+
+ tchan := Srv.Store.Team().Get(teamId)
+ uchan := Srv.Store.User().Get(senderId)
+
+ var team *model.Team
+ if result := <-tchan; result.Err != nil {
+ return result.Err
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ var user *model.User
+ if result := <-uchan; result.Err != nil {
+ return result.Err
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ SendInviteEmails(team, user.GetDisplayName(), emailList, siteURL)
+
+ return nil
+}
+
+func FindTeamByName(name string) bool {
+ if result := <-Srv.Store.Team().GetByName(name); result.Err != nil {
+ return false
+ } else {
+ return true
+ }
+}
+
+func GetTeamsUnreadForUser(teamId string, userId string) ([]*model.TeamUnread, *model.AppError) {
+ if result := <-Srv.Store.Team().GetTeamsUnreadForUser(teamId, userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ data := result.Data.([]*model.ChannelUnread)
+ var members []*model.TeamUnread
+ membersMap := make(map[string]*model.TeamUnread)
+
+ unreads := func(cu *model.ChannelUnread, tu *model.TeamUnread) *model.TeamUnread {
+ tu.MentionCount += cu.MentionCount
+
+ if cu.NotifyProps["mark_unread"] != model.CHANNEL_MARK_UNREAD_MENTION {
+ tu.MsgCount += (cu.TotalMsgCount - cu.MsgCount)
+ }
+
+ return tu
+ }
+
+ for i := range data {
+ id := data[i].TeamId
+ if mu, ok := membersMap[id]; ok {
+ membersMap[id] = unreads(data[i], mu)
+ } else {
+ membersMap[id] = unreads(data[i], &model.TeamUnread{
+ MsgCount: 0,
+ MentionCount: 0,
+ TeamId: id,
+ })
+ }
+ }
+
+ for _, val := range membersMap {
+ members = append(members, val)
+ }
+
+ return members, nil
+ }
+}
+
+func PermanentDeleteTeam(team *model.Team) *model.AppError {
+ team.DeleteAt = model.GetMillis()
+ if result := <-Srv.Store.Team().Update(team); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Channel().PermanentDeleteByTeam(team.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Team().RemoveAllMembersByTeam(team.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Team().PermanentDelete(team.Id); result.Err != nil {
+ return result.Err
+ }
+
+ return nil
+}
+
+func GetTeamStats(teamId string) (*model.TeamStats, *model.AppError) {
+ tchan := Srv.Store.Team().GetTotalMemberCount(teamId)
+ achan := Srv.Store.Team().GetActiveMemberCount(teamId)
+
+ stats := &model.TeamStats{}
+ stats.TeamId = teamId
+
+ if result := <-tchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ stats.TotalMemberCount = result.Data.(int64)
+ }
+
+ if result := <-achan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ stats.ActiveMemberCount = result.Data.(int64)
+ }
+
+ return stats, nil
+}
diff --git a/app/user.go b/app/user.go
new file mode 100644
index 000000000..8fbed301d
--- /dev/null
+++ b/app/user.go
@@ -0,0 +1,982 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "bytes"
+ "fmt"
+ "hash/fnv"
+ "html/template"
+ "image"
+ "image/color"
+ "image/draw"
+ _ "image/gif"
+ _ "image/jpeg"
+ "image/png"
+ "io"
+ "io/ioutil"
+ "mime/multipart"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/disintegration/imaging"
+ "github.com/golang/freetype"
+ "github.com/mattermost/platform/einterfaces"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func CreateUserWithHash(user *model.User, hash string, data string) (*model.User, *model.AppError) {
+ props := model.MapFromJson(strings.NewReader(data))
+
+ if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
+ return nil, model.NewLocAppError("CreateUserWithHash", "api.user.create_user.signup_link_invalid.app_error", nil, "")
+ }
+
+ if t, err := strconv.ParseInt(props["time"], 10, 64); err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours
+ return nil, model.NewLocAppError("CreateUserWithHash", "api.user.create_user.signup_link_expired.app_error", nil, "")
+ }
+
+ teamId := props["id"]
+
+ var team *model.Team
+ if result := <-Srv.Store.Team().Get(teamId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ user.Email = props["email"]
+ user.EmailVerified = true
+
+ var ruser *model.User
+ var err *model.AppError
+ if ruser, err = CreateUser(user); err != nil {
+ return nil, err
+ }
+
+ if err := JoinUserToTeam(team, ruser); err != nil {
+ return nil, err
+ }
+
+ AddDirectChannels(team.Id, ruser)
+
+ return ruser, nil
+}
+
+func CreateUserWithInviteId(user *model.User, inviteId string, siteURL string) (*model.User, *model.AppError) {
+ var team *model.Team
+ if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ team = result.Data.(*model.Team)
+ }
+
+ var ruser *model.User
+ var err *model.AppError
+ if ruser, err = CreateUser(user); err != nil {
+ return nil, err
+ }
+
+ if err := JoinUserToTeam(team, ruser); err != nil {
+ return nil, err
+ }
+
+ AddDirectChannels(team.Id, ruser)
+
+ if err := SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, siteURL); err != nil {
+ l4g.Error(err.Error())
+ }
+
+ return ruser, nil
+}
+
+func IsFirstUserAccount() bool {
+ if SessionCacheLength() == 0 {
+ if cr := <-Srv.Store.User().GetTotalUsersCount(); cr.Err != nil {
+ l4g.Error(cr.Err)
+ return false
+ } else {
+ count := cr.Data.(int64)
+ if count <= 0 {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
+func CreateUser(user *model.User) (*model.User, *model.AppError) {
+ if !user.IsSSOUser() && !CheckUserDomain(user, utils.Cfg.TeamSettings.RestrictCreationToDomains) {
+ return nil, model.NewLocAppError("CreateUser", "api.user.create_user.accepted_domain.app_error", nil, "")
+ }
+
+ user.Roles = model.ROLE_SYSTEM_USER.Id
+
+ // Below is a special case where the first user in the entire
+ // system is granted the system_admin role
+ if result := <-Srv.Store.User().GetTotalUsersCount(); result.Err != nil {
+ return nil, result.Err
+ } else {
+ count := result.Data.(int64)
+ if count <= 0 {
+ user.Roles = model.ROLE_SYSTEM_ADMIN.Id + " " + model.ROLE_SYSTEM_USER.Id
+ }
+ }
+
+ user.MakeNonNil()
+ user.Locale = *utils.Cfg.LocalizationSettings.DefaultClientLocale
+
+ if err := utils.IsPasswordValid(user.Password); user.AuthService == "" && err != nil {
+ return nil, err
+ }
+
+ if result := <-Srv.Store.User().Save(user); result.Err != nil {
+ l4g.Error(utils.T("api.user.create_user.save.error"), result.Err)
+ return nil, result.Err
+ } else {
+ ruser := result.Data.(*model.User)
+
+ if user.EmailVerified {
+ if err := VerifyUserEmail(ruser.Id); err != nil {
+ l4g.Error(utils.T("api.user.create_user.verified.error"), err)
+ }
+ }
+
+ pref := model.Preference{UserId: ruser.Id, Category: model.PREFERENCE_CATEGORY_TUTORIAL_STEPS, Name: ruser.Id, Value: "0"}
+ if presult := <-Srv.Store.Preference().Save(&model.Preferences{pref}); presult.Err != nil {
+ l4g.Error(utils.T("api.user.create_user.tutorial.error"), presult.Err.Message)
+ }
+
+ ruser.Sanitize(map[string]bool{})
+
+ // This message goes to everyone, so the teamId, channelId and userId are irrelevant
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_NEW_USER, "", "", "", nil)
+ message.Add("user_id", ruser.Id)
+ go Publish(message)
+
+ return ruser, nil
+ }
+}
+
+func CreateOAuthUser(service string, userData io.Reader, teamId string) (*model.User, *model.AppError) {
+ var user *model.User
+ provider := einterfaces.GetOauthProvider(service)
+ if provider == nil {
+ return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.not_available.app_error", map[string]interface{}{"Service": strings.Title(service)}, "")
+ } else {
+ user = provider.GetUserFromJson(userData)
+ }
+
+ if user == nil {
+ return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.create.app_error", map[string]interface{}{"Service": service}, "")
+ }
+
+ suchan := Srv.Store.User().GetByAuth(user.AuthData, service)
+ euchan := Srv.Store.User().GetByEmail(user.Email)
+
+ found := true
+ count := 0
+ for found {
+ if found = IsUsernameTaken(user.Username); found {
+ user.Username = user.Username + strconv.Itoa(count)
+ count += 1
+ }
+ }
+
+ if result := <-suchan; result.Err == nil {
+ return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_used.app_error", map[string]interface{}{"Service": service}, "email="+user.Email)
+ }
+
+ if result := <-euchan; result.Err == nil {
+ authService := result.Data.(*model.User).AuthService
+ if authService == "" {
+ return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error",
+ map[string]interface{}{"Service": service, "Auth": model.USER_AUTH_SERVICE_EMAIL}, "email="+user.Email)
+ } else {
+ return nil, model.NewLocAppError("CreateOAuthUser", "api.user.create_oauth_user.already_attached.app_error",
+ map[string]interface{}{"Service": service, "Auth": authService}, "email="+user.Email)
+ }
+ }
+
+ user.EmailVerified = true
+
+ ruser, err := CreateUser(user)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(teamId) > 0 {
+ err = AddUserToTeamByTeamId(teamId, user)
+ if err != nil {
+ return nil, err
+ }
+
+ err = AddDirectChannels(teamId, user)
+ if err != nil {
+ l4g.Error(err.Error())
+ }
+ }
+
+ return ruser, nil
+}
+
+// Check that a user's email domain matches a list of space-delimited domains as a string.
+func CheckUserDomain(user *model.User, domains string) bool {
+ if len(domains) == 0 {
+ return true
+ }
+
+ domainArray := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(domains, "@", " ", -1), ",", " ", -1))))
+
+ matched := false
+ for _, d := range domainArray {
+ if strings.HasSuffix(strings.ToLower(user.Email), "@"+d) {
+ matched = true
+ break
+ }
+ }
+
+ return matched
+}
+
+// Check if the username is already used by another user. Return false if the username is invalid.
+func IsUsernameTaken(name string) bool {
+
+ if !model.IsValidUsername(name) {
+ return false
+ }
+
+ if result := <-Srv.Store.User().GetByUsername(name); result.Err != nil {
+ return false
+ } else {
+ return true
+ }
+
+ return false
+}
+
+func GetUser(userId string) (*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().Get(userId); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.User), nil
+ }
+}
+
+func GetUserByUsername(username string) (*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().GetByUsername(username); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.User), nil
+ }
+}
+
+func GetUserByEmail(email string) (*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().GetByEmail(email); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.User), nil
+ }
+}
+
+func GetUserByAuth(authData *string, authService string) (*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().GetByAuth(authData, authService); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(*model.User), nil
+ }
+}
+
+func GetUserForLogin(loginId string, onlyLdap bool) (*model.User, *model.AppError) {
+ ldapAvailable := *utils.Cfg.LdapSettings.Enable && einterfaces.GetLdapInterface() != nil && utils.IsLicensed && *utils.License.Features.LDAP
+
+ if result := <-Srv.Store.User().GetForLogin(
+ loginId,
+ *utils.Cfg.EmailSettings.EnableSignInWithUsername && !onlyLdap,
+ *utils.Cfg.EmailSettings.EnableSignInWithEmail && !onlyLdap,
+ ldapAvailable,
+ ); result.Err != nil && result.Err.Id == "store.sql_user.get_for_login.multiple_users" {
+ // don't fall back to LDAP in this case since we already know there's an LDAP user, but that it shouldn't work
+ result.Err.StatusCode = http.StatusBadRequest
+ return nil, result.Err
+ } else if result.Err != nil {
+ if !ldapAvailable {
+ // failed to find user and no LDAP server to fall back on
+ result.Err.StatusCode = http.StatusBadRequest
+ return nil, result.Err
+ }
+
+ // fall back to LDAP server to see if we can find a user
+ if ldapUser, ldapErr := einterfaces.GetLdapInterface().GetUser(loginId); ldapErr != nil {
+ ldapErr.StatusCode = http.StatusBadRequest
+ return nil, ldapErr
+ } else {
+ return ldapUser, nil
+ }
+ } else {
+ return result.Data.(*model.User), nil
+ }
+}
+
+func GetUsers(offset int, limit int) (map[string]*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().GetAllProfiles(offset, limit); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(map[string]*model.User), nil
+ }
+}
+
+func GetUsersEtag() string {
+ return (<-Srv.Store.User().GetEtagForAllProfiles()).Data.(string)
+}
+
+func GetUsersInTeam(teamId string, offset int, limit int) (map[string]*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().GetProfiles(teamId, offset, limit); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(map[string]*model.User), nil
+ }
+}
+
+func GetUsersInTeamEtag(teamId string) string {
+ return (<-Srv.Store.User().GetEtagForProfiles(teamId)).Data.(string)
+}
+
+func GetUsersInChannel(channelId string, offset int, limit int) (map[string]*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().GetProfilesInChannel(channelId, offset, limit, false); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(map[string]*model.User), nil
+ }
+}
+
+func GetUsersNotInChannel(teamId string, channelId string, offset int, limit int) (map[string]*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().GetProfilesNotInChannel(teamId, channelId, offset, limit); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(map[string]*model.User), nil
+ }
+}
+
+func GetUsersByIds(userIds []string) (map[string]*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().GetProfileByIds(userIds, true); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.(map[string]*model.User), nil
+ }
+}
+
+func ActivateMfa(userId, token string) *model.AppError {
+ mfaInterface := einterfaces.GetMfaInterface()
+ if mfaInterface == nil {
+ err := model.NewLocAppError("ActivateMfa", "api.user.update_mfa.not_available.app_error", nil, "")
+ err.StatusCode = http.StatusNotImplemented
+ return err
+ }
+
+ var user *model.User
+ if result := <-Srv.Store.User().Get(userId); result.Err != nil {
+ return result.Err
+ } else {
+ user = result.Data.(*model.User)
+ }
+
+ if len(user.AuthService) > 0 && user.AuthService != model.USER_AUTH_SERVICE_LDAP {
+ return model.NewLocAppError("ActivateMfa", "api.user.activate_mfa.email_and_ldap_only.app_error", nil, "")
+ }
+
+ if err := mfaInterface.Activate(user, token); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func DeactivateMfa(userId string) *model.AppError {
+ mfaInterface := einterfaces.GetMfaInterface()
+ if mfaInterface == nil {
+ err := model.NewLocAppError("DeactivateMfa", "api.user.update_mfa.not_available.app_error", nil, "")
+ err.StatusCode = http.StatusNotImplemented
+ return err
+ }
+
+ if err := mfaInterface.Deactivate(userId); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func CreateProfileImage(username string, userId string) ([]byte, *model.AppError) {
+ colors := []color.NRGBA{
+ {197, 8, 126, 255},
+ {227, 207, 18, 255},
+ {28, 181, 105, 255},
+ {35, 188, 224, 255},
+ {116, 49, 196, 255},
+ {197, 8, 126, 255},
+ {197, 19, 19, 255},
+ {250, 134, 6, 255},
+ {227, 207, 18, 255},
+ {123, 201, 71, 255},
+ {28, 181, 105, 255},
+ {35, 188, 224, 255},
+ {116, 49, 196, 255},
+ {197, 8, 126, 255},
+ {197, 19, 19, 255},
+ {250, 134, 6, 255},
+ {227, 207, 18, 255},
+ {123, 201, 71, 255},
+ {28, 181, 105, 255},
+ {35, 188, 224, 255},
+ {116, 49, 196, 255},
+ {197, 8, 126, 255},
+ {197, 19, 19, 255},
+ {250, 134, 6, 255},
+ {227, 207, 18, 255},
+ {123, 201, 71, 255},
+ }
+
+ h := fnv.New32a()
+ h.Write([]byte(userId))
+ seed := h.Sum32()
+
+ initial := string(strings.ToUpper(username)[0])
+
+ fontBytes, err := ioutil.ReadFile(utils.FindDir("fonts") + utils.Cfg.FileSettings.InitialFont)
+ if err != nil {
+ return nil, model.NewLocAppError("CreateProfileImage", "api.user.create_profile_image.default_font.app_error", nil, err.Error())
+ }
+ font, err := freetype.ParseFont(fontBytes)
+ if err != nil {
+ return nil, model.NewLocAppError("CreateProfileImage", "api.user.create_profile_image.default_font.app_error", nil, err.Error())
+ }
+
+ width := int(utils.Cfg.FileSettings.ProfileWidth)
+ height := int(utils.Cfg.FileSettings.ProfileHeight)
+ color := colors[int64(seed)%int64(len(colors))]
+ dstImg := image.NewRGBA(image.Rect(0, 0, width, height))
+ srcImg := image.White
+ draw.Draw(dstImg, dstImg.Bounds(), &image.Uniform{color}, image.ZP, draw.Src)
+ size := float64((width + height) / 4)
+
+ c := freetype.NewContext()
+ c.SetFont(font)
+ c.SetFontSize(size)
+ c.SetClip(dstImg.Bounds())
+ c.SetDst(dstImg)
+ c.SetSrc(srcImg)
+
+ pt := freetype.Pt(width/6, height*2/3)
+ _, err = c.DrawString(initial, pt)
+ if err != nil {
+ return nil, model.NewLocAppError("CreateProfileImage", "api.user.create_profile_image.initial.app_error", nil, err.Error())
+ }
+
+ buf := new(bytes.Buffer)
+
+ if imgErr := png.Encode(buf, dstImg); imgErr != nil {
+ return nil, model.NewLocAppError("CreateProfileImage", "api.user.create_profile_image.encode.app_error", nil, imgErr.Error())
+ } else {
+ return buf.Bytes(), nil
+ }
+}
+
+func GetProfileImage(user *model.User) ([]byte, *model.AppError) {
+ var img []byte
+
+ if len(utils.Cfg.FileSettings.DriverName) == 0 {
+ var err *model.AppError
+ if img, err = CreateProfileImage(user.Username, user.Id); err != nil {
+ return nil, err
+ }
+ } else {
+ path := "users/" + user.Id + "/profile.png"
+
+ if data, err := ReadFile(path); err != nil {
+ if img, err = CreateProfileImage(user.Username, user.Id); err != nil {
+ return nil, err
+ }
+
+ if user.LastPictureUpdate == 0 {
+ if err := WriteFile(img, path); err != nil {
+ return nil, err
+ }
+ }
+
+ } else {
+ img = data
+ }
+ }
+
+ return img, nil
+}
+
+func SetProfileImage(userId string, imageData *multipart.FileHeader) *model.AppError {
+ file, err := imageData.Open()
+ defer file.Close()
+ if err != nil {
+ return model.NewLocAppError("SetProfileImage", "api.user.upload_profile_user.open.app_error", nil, err.Error())
+ }
+
+ // Decode image config first to check dimensions before loading the whole thing into memory later on
+ config, _, err := image.DecodeConfig(file)
+ if err != nil {
+ return model.NewLocAppError("SetProfileImage", "api.user.upload_profile_user.decode_config.app_error", nil, err.Error())
+ } else if config.Width*config.Height > model.MaxImageSize {
+ return model.NewLocAppError("SetProfileImage", "api.user.upload_profile_user.too_large.app_error", nil, err.Error())
+ }
+
+ file.Seek(0, 0)
+
+ // Decode image into Image object
+ img, _, err := image.Decode(file)
+ if err != nil {
+ return model.NewLocAppError("SetProfileImage", "api.user.upload_profile_user.decode.app_error", nil, err.Error())
+ }
+
+ // Scale profile image
+ img = imaging.Resize(img, utils.Cfg.FileSettings.ProfileWidth, utils.Cfg.FileSettings.ProfileHeight, imaging.Lanczos)
+
+ buf := new(bytes.Buffer)
+ err = png.Encode(buf, img)
+ if err != nil {
+ return model.NewLocAppError("SetProfileImage", "api.user.upload_profile_user.encode.app_error", nil, err.Error())
+ }
+
+ path := "users/" + userId + "/profile.png"
+
+ if err := WriteFile(buf.Bytes(), path); err != nil {
+ return model.NewLocAppError("SetProfileImage", "api.user.upload_profile_user.upload_profile.app_error", nil, "")
+ }
+
+ Srv.Store.User().UpdateLastPictureUpdate(userId)
+
+ if user, err := GetUser(userId); err != nil {
+ l4g.Error(utils.T("api.user.get_me.getting.error"), userId)
+ } else {
+ options := utils.Cfg.GetSanitizeOptions()
+ user.SanitizeProfile(options)
+
+ omitUsers := make(map[string]bool, 1)
+ omitUsers[userId] = true
+ message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", omitUsers)
+ message.Add("user", user)
+
+ Publish(message)
+ }
+
+ return nil
+}
+
+func UpdateActiveNoLdap(userId string, active bool) (*model.User, *model.AppError) {
+ var user *model.User
+ var err *model.AppError
+ if user, err = GetUser(userId); err != nil {
+ return nil, err
+ }
+
+ if user.IsLDAPUser() {
+ err := model.NewLocAppError("UpdateActive", "api.user.update_active.no_deactivate_ldap.app_error", nil, "userId="+user.Id)
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ return UpdateActive(user, active)
+}
+
+func UpdateActive(user *model.User, active bool) (*model.User, *model.AppError) {
+ if active {
+ user.DeleteAt = 0
+ } else {
+ user.DeleteAt = model.GetMillis()
+ }
+
+ if result := <-Srv.Store.User().Update(user, true); result.Err != nil {
+ return nil, result.Err
+ } else {
+ if user.DeleteAt > 0 {
+ if err := RevokeAllSessions(user.Id); err != nil {
+ return nil, err
+ }
+ }
+
+ if extra := <-Srv.Store.Channel().ExtraUpdateByUser(user.Id, model.GetMillis()); extra.Err != nil {
+ return nil, extra.Err
+ }
+
+ ruser := result.Data.([2]*model.User)[0]
+ options := utils.Cfg.GetSanitizeOptions()
+ options["passwordupdate"] = false
+ ruser.Sanitize(options)
+
+ if !active {
+ SetStatusOffline(ruser.Id, false)
+ }
+
+ return ruser, nil
+ }
+}
+
+func UpdateUser(user *model.User, siteURL string) (*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().Update(user, false); result.Err != nil {
+ return nil, result.Err
+ } else {
+ rusers := result.Data.([2]*model.User)
+
+ if rusers[0].Email != rusers[1].Email {
+ go func() {
+ if err := SendEmailChangeEmail(rusers[1].Email, rusers[0].Email, rusers[0].Locale, siteURL); err != nil {
+ l4g.Error(err.Error())
+ }
+ }()
+
+ if utils.Cfg.EmailSettings.RequireEmailVerification {
+ go func() {
+ if err := SendEmailChangeVerifyEmail(rusers[0].Id, rusers[0].Email, rusers[0].Locale, siteURL); err != nil {
+ l4g.Error(err.Error())
+ }
+ }()
+ }
+ }
+
+ if rusers[0].Username != rusers[1].Username {
+ go func() {
+ if err := SendChangeUsernameEmail(rusers[1].Username, rusers[0].Username, rusers[0].Email, rusers[0].Locale, siteURL); err != nil {
+ l4g.Error(err.Error())
+ }
+ }()
+ }
+
+ InvalidateCacheForUser(user.Id)
+
+ return rusers[0], nil
+ }
+}
+
+func UpdateUserNotifyProps(userId string, props map[string]string, siteURL string) (*model.User, *model.AppError) {
+ var user *model.User
+ var err *model.AppError
+ if user, err = GetUser(userId); err != nil {
+ return nil, err
+ }
+
+ user.NotifyProps = props
+
+ var ruser *model.User
+ if ruser, err = UpdateUser(user, siteURL); err != nil {
+ return nil, err
+ }
+
+ return ruser, nil
+}
+
+func UpdatePasswordByUserIdSendEmail(userId, newPassword, method, siteURL string) *model.AppError {
+ var user *model.User
+ var err *model.AppError
+ if user, err = GetUser(userId); err != nil {
+ return err
+ }
+
+ return UpdatePasswordSendEmail(user, newPassword, method, siteURL)
+}
+
+func UpdatePassword(user *model.User, newPassword string) *model.AppError {
+ if err := utils.IsPasswordValid(newPassword); err != nil {
+ return err
+ }
+
+ hashedPassword := model.HashPassword(newPassword)
+
+ if result := <-Srv.Store.User().UpdatePassword(user.Id, hashedPassword); result.Err != nil {
+ return model.NewLocAppError("UpdatePassword", "api.user.update_password.failed.app_error", nil, result.Err.Error())
+ }
+
+ return nil
+}
+
+func UpdatePasswordSendEmail(user *model.User, newPassword, method, siteURL string) *model.AppError {
+ if err := UpdatePassword(user, newPassword); err != nil {
+ return err
+ }
+
+ go func() {
+ if err := SendPasswordChangeEmail(user.Email, method, user.Locale, siteURL); err != nil {
+ l4g.Error(err.Error())
+ }
+ }()
+
+ return nil
+}
+
+func SendPasswordReset(email string, siteURL string) (bool, *model.AppError) {
+ var user *model.User
+ var err *model.AppError
+ if user, err = GetUserByEmail(email); err != nil {
+ return false, nil
+ }
+
+ if user.AuthData != nil && len(*user.AuthData) != 0 {
+ return false, model.NewLocAppError("SendPasswordReset", "api.user.send_password_reset.sso.app_error", nil, "userId="+user.Id)
+ }
+
+ var recovery *model.PasswordRecovery
+ if recovery, err = CreatePasswordRecovery(user.Id); err != nil {
+ return false, err
+ }
+
+ T := utils.GetUserTranslations(user.Locale)
+
+ link := fmt.Sprintf("%s/reset_password_complete?code=%s", siteURL, url.QueryEscape(recovery.Code))
+
+ subject := T("api.templates.reset_subject")
+
+ bodyPage := utils.NewHTMLTemplate("reset_body", user.Locale)
+ bodyPage.Props["SiteURL"] = siteURL
+ bodyPage.Props["Title"] = T("api.templates.reset_body.title")
+ bodyPage.Html["Info"] = template.HTML(T("api.templates.reset_body.info"))
+ bodyPage.Props["ResetUrl"] = link
+ bodyPage.Props["Button"] = T("api.templates.reset_body.button")
+
+ if err := utils.SendMail(email, subject, bodyPage.Render()); err != nil {
+ return false, model.NewLocAppError("SendPasswordReset", "api.user.send_password_reset.send.app_error", nil, "err="+err.Message)
+ }
+
+ return true, nil
+}
+
+func ResetPasswordFromCode(code, newPassword, siteURL string) *model.AppError {
+ var recovery *model.PasswordRecovery
+ var err *model.AppError
+ if recovery, err = GetPasswordRecovery(code); err != nil {
+ return err
+ } else {
+ if model.GetMillis()-recovery.CreateAt >= model.PASSWORD_RECOVER_EXPIRY_TIME {
+ return model.NewLocAppError("resetPassword", "api.user.reset_password.link_expired.app_error", nil, "")
+ }
+ }
+
+ var user *model.User
+ if user, err = GetUser(recovery.UserId); err != nil {
+ return err
+ }
+
+ if user.IsSSOUser() {
+ return model.NewLocAppError("ResetPasswordFromCode", "api.user.reset_password.sso.app_error", nil, "userId="+user.Id)
+ }
+
+ T := utils.GetUserTranslations(user.Locale)
+
+ if err := UpdatePasswordSendEmail(user, newPassword, T("api.user.reset_password.method"), siteURL); err != nil {
+ return err
+ }
+
+ if err := DeletePasswordRecoveryForUser(recovery.UserId); err != nil {
+ l4g.Error(err.Error())
+ }
+
+ return nil
+}
+
+func CreatePasswordRecovery(userId string) (*model.PasswordRecovery, *model.AppError) {
+ recovery := &model.PasswordRecovery{}
+ recovery.UserId = userId
+
+ if result := <-Srv.Store.PasswordRecovery().SaveOrUpdate(recovery); result.Err != nil {
+ return nil, result.Err
+ }
+
+ return recovery, nil
+}
+
+func GetPasswordRecovery(code string) (*model.PasswordRecovery, *model.AppError) {
+ if result := <-Srv.Store.PasswordRecovery().GetByCode(code); result.Err != nil {
+ return nil, model.NewLocAppError("GetPasswordRecovery", "api.user.reset_password.invalid_link.app_error", nil, result.Err.Error())
+ } else {
+ return result.Data.(*model.PasswordRecovery), nil
+ }
+}
+
+func DeletePasswordRecoveryForUser(userId string) *model.AppError {
+ if result := <-Srv.Store.PasswordRecovery().Delete(userId); result.Err != nil {
+ return result.Err
+ }
+
+ return nil
+}
+
+func UpdateUserRoles(userId string, newRoles string) (*model.User, *model.AppError) {
+ var user *model.User
+ var err *model.AppError
+ if user, err = GetUser(userId); err != nil {
+ err.StatusCode = http.StatusBadRequest
+ return nil, err
+ }
+
+ user.Roles = newRoles
+ uchan := Srv.Store.User().Update(user, true)
+ schan := Srv.Store.Session().UpdateRoles(user.Id, newRoles)
+
+ var ruser *model.User
+ if result := <-uchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ ruser = result.Data.([2]*model.User)[0]
+ }
+
+ if result := <-schan; result.Err != nil {
+ // soft error since the user roles were still updated
+ l4g.Error(result.Err)
+ }
+
+ ClearSessionCacheForUser(user.Id)
+
+ return ruser, nil
+}
+
+func PermanentDeleteUser(user *model.User) *model.AppError {
+ l4g.Warn(utils.T("api.user.permanent_delete_user.attempting.warn"), user.Email, user.Id)
+ if user.IsInRole(model.ROLE_SYSTEM_ADMIN.Id) {
+ l4g.Warn(utils.T("api.user.permanent_delete_user.system_admin.warn"), user.Email)
+ }
+
+ if _, err := UpdateActive(user, false); err != nil {
+ return err
+ }
+
+ if result := <-Srv.Store.Session().PermanentDeleteSessionsByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.OAuth().PermanentDeleteAuthDataByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Webhook().PermanentDeleteIncomingByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Webhook().PermanentDeleteOutgoingByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Command().PermanentDeleteByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Preference().PermanentDeleteByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Channel().PermanentDeleteMembersByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Post().PermanentDeleteByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.User().PermanentDelete(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Audit().PermanentDeleteByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.Team().RemoveAllMembersByUser(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ if result := <-Srv.Store.PasswordRecovery().Delete(user.Id); result.Err != nil {
+ return result.Err
+ }
+
+ l4g.Warn(utils.T("api.user.permanent_delete_user.deleted.warn"), user.Email, user.Id)
+
+ return nil
+}
+
+func PermanentDeleteAllUsers() *model.AppError {
+ if result := <-Srv.Store.User().GetAll(); result.Err != nil {
+ return result.Err
+ } else {
+ users := result.Data.([]*model.User)
+ for _, user := range users {
+ PermanentDeleteUser(user)
+ }
+ }
+
+ return nil
+}
+
+func VerifyUserEmail(userId string) *model.AppError {
+ if err := (<-Srv.Store.User().VerifyEmail(userId)).Err; err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func SearchUsersInChannel(channelId string, term string, searchOptions map[string]bool) ([]*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().SearchInChannel(channelId, term, searchOptions); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.User), nil
+ }
+}
+
+func SearchUsersNotInChannel(teamId string, channelId string, term string, searchOptions map[string]bool) ([]*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().SearchNotInChannel(teamId, channelId, term, searchOptions); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.User), nil
+ }
+}
+
+func SearchUsersInTeam(teamId string, term string, searchOptions map[string]bool) ([]*model.User, *model.AppError) {
+ if result := <-Srv.Store.User().Search(teamId, term, searchOptions); result.Err != nil {
+ return nil, result.Err
+ } else {
+ return result.Data.([]*model.User), nil
+ }
+}
+
+func AutocompleteUsersInChannel(teamId string, channelId string, term string, searchOptions map[string]bool) (*model.UserAutocompleteInChannel, *model.AppError) {
+ uchan := Srv.Store.User().SearchInChannel(channelId, term, searchOptions)
+ nuchan := Srv.Store.User().SearchNotInChannel(teamId, channelId, term, searchOptions)
+
+ autocomplete := &model.UserAutocompleteInChannel{}
+
+ if result := <-uchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ autocomplete.InChannel = result.Data.([]*model.User)
+ }
+
+ if result := <-nuchan; result.Err != nil {
+ return nil, result.Err
+ } else {
+ autocomplete.OutOfChannel = result.Data.([]*model.User)
+ }
+
+ return autocomplete, nil
+}
+
+func AutocompleteUsersInTeam(teamId string, term string, searchOptions map[string]bool) (*model.UserAutocompleteInTeam, *model.AppError) {
+ autocomplete := &model.UserAutocompleteInTeam{}
+
+ if result := <-Srv.Store.User().Search(teamId, term, searchOptions); result.Err != nil {
+ return nil, result.Err
+ } else {
+ autocomplete.InTeam = result.Data.([]*model.User)
+ }
+
+ return autocomplete, nil
+}
diff --git a/app/user_test.go b/app/user_test.go
new file mode 100644
index 000000000..5b994d219
--- /dev/null
+++ b/app/user_test.go
@@ -0,0 +1,53 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "testing"
+)
+
+func TestIsUsernameTaken(t *testing.T) {
+ th := Setup().InitBasic()
+ user := th.BasicUser
+ taken := IsUsernameTaken(user.Username)
+
+ if !taken {
+ t.Logf("the username '%v' should be taken", user.Username)
+ t.FailNow()
+ }
+
+ newUsername := "randomUsername"
+ taken = IsUsernameTaken(newUsername)
+
+ if taken {
+ t.Logf("the username '%v' should not be taken", newUsername)
+ t.FailNow()
+ }
+}
+
+func TestCheckUserDomain(t *testing.T) {
+ th := Setup().InitBasic()
+ user := th.BasicUser
+
+ cases := []struct {
+ domains string
+ matched bool
+ }{
+ {"simulator.amazonses.com", true},
+ {"gmail.com", false},
+ {"", true},
+ {"gmail.com simulator.amazonses.com", true},
+ }
+ for _, c := range cases {
+ matched := CheckUserDomain(user, c.domains)
+ if matched != c.matched {
+ if c.matched {
+ t.Logf("'%v' should have matched '%v'", user.Email, c.domains)
+ } else {
+ t.Logf("'%v' should not have matched '%v'", user.Email, c.domains)
+ }
+ t.FailNow()
+ }
+ }
+}
diff --git a/api/web_conn.go b/app/web_conn.go
index 2f5036922..02c3b2642 100644
--- a/api/web_conn.go
+++ b/app/web_conn.go
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package api
+package app
import (
"fmt"
@@ -35,32 +35,32 @@ type WebConn struct {
LastAllChannelMembersTime int64
}
-func NewWebConn(c *Context, ws *websocket.Conn) *WebConn {
- if len(c.Session.UserId) > 0 {
- go SetStatusOnline(c.Session.UserId, c.Session.Id, false)
+func NewWebConn(ws *websocket.Conn, session model.Session, t goi18n.TranslateFunc, locale string) *WebConn {
+ if len(session.UserId) > 0 {
+ go SetStatusOnline(session.UserId, session.Id, false)
}
return &WebConn{
Send: make(chan model.WebSocketMessage, 256),
WebSocket: ws,
- UserId: c.Session.UserId,
- SessionToken: c.Session.Token,
- SessionExpiresAt: c.Session.ExpiresAt,
- T: c.T,
- Locale: c.Locale,
+ UserId: session.UserId,
+ SessionToken: session.Token,
+ SessionExpiresAt: session.ExpiresAt,
+ T: t,
+ Locale: locale,
}
}
-func (c *WebConn) readPump() {
+func (c *WebConn) ReadPump() {
defer func() {
HubUnregister(c)
c.WebSocket.Close()
}()
- c.WebSocket.SetReadLimit(SOCKET_MAX_MESSAGE_SIZE_KB)
+ c.WebSocket.SetReadLimit(model.SOCKET_MAX_MESSAGE_SIZE_KB)
c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT))
c.WebSocket.SetPongHandler(func(string) error {
c.WebSocket.SetReadDeadline(time.Now().Add(PONG_WAIT))
- if c.isAuthenticated() {
+ if c.IsAuthenticated() {
go SetStatusAwayIfNeeded(c.UserId, false)
}
return nil
@@ -78,12 +78,12 @@ func (c *WebConn) readPump() {
return
} else {
- BaseRoutes.WebSocket.ServeWebSocket(c, &req)
+ Srv.WebSocketRouter.ServeWebSocket(c, &req)
}
}
}
-func (c *WebConn) writePump() {
+func (c *WebConn) WritePump() {
ticker := time.NewTicker(PING_PERIOD)
authTicker := time.NewTicker(AUTH_TIMEOUT)
@@ -149,15 +149,16 @@ func (webCon *WebConn) InvalidateCache() {
webCon.SessionExpiresAt = 0
}
-func (webCon *WebConn) isAuthenticated() bool {
+func (webCon *WebConn) IsAuthenticated() bool {
// Check the expiry to see if we need to check for a new session
if webCon.SessionExpiresAt < model.GetMillis() {
if webCon.SessionToken == "" {
return false
}
- session := GetSession(webCon.SessionToken)
- if session == nil || session.IsExpired() {
+ session, err := GetSession(webCon.SessionToken)
+ if err != nil {
+ l4g.Error(utils.T("api.websocket.invalid_session.error"), err.Error())
webCon.SessionToken = ""
webCon.SessionExpiresAt = 0
return false
@@ -179,7 +180,7 @@ func (webCon *WebConn) SendHello() {
func (webCon *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool {
// IMPORTANT: Do not send event if WebConn does not have a session
- if !webCon.isAuthenticated() {
+ if !webCon.IsAuthenticated() {
return false
}
@@ -237,8 +238,9 @@ func (webCon *WebConn) ShouldSendEvent(msg *model.WebSocketEvent) bool {
}
func (webCon *WebConn) IsMemberOfTeam(teamId string) bool {
- session := GetSession(webCon.SessionToken)
- if session == nil {
+ session, err := GetSession(webCon.SessionToken)
+ if err != nil {
+ l4g.Error(utils.T("api.websocket.invalid_session.error"), err.Error())
return false
} else {
member := session.GetTeamByTeamId(teamId)
diff --git a/api/web_hub.go b/app/web_hub.go
index 076236dfb..28d2c0095 100644
--- a/api/web_hub.go
+++ b/app/web_hub.go
@@ -1,7 +1,7 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package api
+package app
import (
"fmt"
@@ -156,7 +156,7 @@ func InvalidateWebConnSessionCacheForUser(userId string) {
func (h *Hub) Register(webConn *WebConn) {
h.register <- webConn
- if webConn.isAuthenticated() {
+ if webConn.IsAuthenticated() {
webConn.SendHello()
}
}
diff --git a/app/webhook.go b/app/webhook.go
new file mode 100644
index 000000000..70ba1d07a
--- /dev/null
+++ b/app/webhook.go
@@ -0,0 +1,189 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "crypto/tls"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "regexp"
+ "strings"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+const (
+ TRIGGERWORDS_FULL = 0
+ TRIGGERWORDS_STARTSWITH = 1
+)
+
+func handleWebhookEvents(post *model.Post, team *model.Team, channel *model.Channel, user *model.User) *model.AppError {
+ if !utils.Cfg.ServiceSettings.EnableOutgoingWebhooks {
+ return nil
+ }
+
+ if channel.Type != model.CHANNEL_OPEN {
+ return nil
+ }
+
+ hchan := Srv.Store.Webhook().GetOutgoingByTeam(team.Id)
+ result := <-hchan
+ if result.Err != nil {
+ return result.Err
+ }
+
+ hooks := result.Data.([]*model.OutgoingWebhook)
+ if len(hooks) == 0 {
+ return nil
+ }
+
+ splitWords := strings.Fields(post.Message)
+ if len(splitWords) == 0 {
+ return nil
+ }
+ firstWord := splitWords[0]
+
+ relevantHooks := []*model.OutgoingWebhook{}
+ for _, hook := range hooks {
+ if hook.ChannelId == post.ChannelId || len(hook.ChannelId) == 0 {
+ if hook.ChannelId == post.ChannelId && len(hook.TriggerWords) == 0 {
+ relevantHooks = append(relevantHooks, hook)
+ } else if hook.TriggerWhen == TRIGGERWORDS_FULL && hook.HasTriggerWord(firstWord) {
+ relevantHooks = append(relevantHooks, hook)
+ } else if hook.TriggerWhen == TRIGGERWORDS_STARTSWITH && hook.TriggerWordStartsWith(firstWord) {
+ relevantHooks = append(relevantHooks, hook)
+ }
+ }
+ }
+
+ for _, hook := range relevantHooks {
+ go func(hook *model.OutgoingWebhook) {
+ payload := &model.OutgoingWebhookPayload{
+ Token: hook.Token,
+ TeamId: hook.TeamId,
+ TeamDomain: team.Name,
+ ChannelId: post.ChannelId,
+ ChannelName: channel.Name,
+ Timestamp: post.CreateAt,
+ UserId: post.UserId,
+ UserName: user.Username,
+ PostId: post.Id,
+ Text: post.Message,
+ TriggerWord: firstWord,
+ }
+ var body io.Reader
+ var contentType string
+ if hook.ContentType == "application/json" {
+ body = strings.NewReader(payload.ToJSON())
+ contentType = "application/json"
+ } else {
+ body = strings.NewReader(payload.ToFormValues())
+ contentType = "application/x-www-form-urlencoded"
+ }
+ tr := &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections},
+ }
+ client := &http.Client{Transport: tr}
+
+ for _, url := range hook.CallbackURLs {
+ go func(url string) {
+ req, _ := http.NewRequest("POST", url, body)
+ req.Header.Set("Content-Type", contentType)
+ req.Header.Set("Accept", "application/json")
+ if resp, err := client.Do(req); err != nil {
+ l4g.Error(utils.T("api.post.handle_webhook_events_and_forget.event_post.error"), err.Error())
+ } else {
+ defer func() {
+ ioutil.ReadAll(resp.Body)
+ resp.Body.Close()
+ }()
+ respProps := model.MapFromJson(resp.Body)
+
+ if text, ok := respProps["text"]; ok {
+ if _, err := CreateWebhookPost(hook.CreatorId, hook.TeamId, post.ChannelId, text, respProps["username"], respProps["icon_url"], post.Props, post.Type); err != nil {
+ l4g.Error(utils.T("api.post.handle_webhook_events_and_forget.create_post.error"), err)
+ }
+ }
+ }
+ }(url)
+ }
+
+ }(hook)
+ }
+
+ return nil
+}
+
+func CreateWebhookPost(userId, teamId, channelId, text, overrideUsername, overrideIconUrl string, props model.StringInterface, postType string) (*model.Post, *model.AppError) {
+ // parse links into Markdown format
+ linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
+ text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
+
+ post := &model.Post{UserId: userId, ChannelId: channelId, Message: text, Type: postType}
+ post.AddProp("from_webhook", "true")
+
+ if utils.Cfg.ServiceSettings.EnablePostUsernameOverride {
+ if len(overrideUsername) != 0 {
+ post.AddProp("override_username", overrideUsername)
+ } else {
+ post.AddProp("override_username", model.DEFAULT_WEBHOOK_USERNAME)
+ }
+ }
+
+ if utils.Cfg.ServiceSettings.EnablePostIconOverride {
+ if len(overrideIconUrl) != 0 {
+ post.AddProp("override_icon_url", overrideIconUrl)
+ }
+ }
+
+ if len(props) > 0 {
+ for key, val := range props {
+ if key == "attachments" {
+ if list, success := val.([]interface{}); success {
+ // parse attachment links into Markdown format
+ for i, aInt := range list {
+ attachment := aInt.(map[string]interface{})
+ if aText, ok := attachment["text"].(string); ok {
+ aText = linkWithTextRegex.ReplaceAllString(aText, "[${2}](${1})")
+ attachment["text"] = aText
+ list[i] = attachment
+ }
+ if aText, ok := attachment["pretext"].(string); ok {
+ aText = linkWithTextRegex.ReplaceAllString(aText, "[${2}](${1})")
+ attachment["pretext"] = aText
+ list[i] = attachment
+ }
+ if fVal, ok := attachment["fields"]; ok {
+ if fields, ok := fVal.([]interface{}); ok {
+ // parse attachment field links into Markdown format
+ for j, fInt := range fields {
+ field := fInt.(map[string]interface{})
+ if fValue, ok := field["value"].(string); ok {
+ fValue = linkWithTextRegex.ReplaceAllString(fValue, "[${2}](${1})")
+ field["value"] = fValue
+ fields[j] = field
+ }
+ }
+ attachment["fields"] = fields
+ list[i] = attachment
+ }
+ }
+ }
+ post.AddProp(key, list)
+ }
+ } else if key != "override_icon_url" && key != "override_username" && key != "from_webhook" {
+ post.AddProp(key, val)
+ }
+ }
+ }
+
+ if _, err := CreatePost(post, teamId, false); err != nil {
+ return nil, model.NewLocAppError("CreateWebhookPost", "api.post.create_webhook_post.creating.app_error", nil, "err="+err.Message)
+ }
+
+ return post, nil
+}
diff --git a/api/websocket_router.go b/app/websocket_router.go
index 989d41373..984b9d17e 100644
--- a/api/websocket_router.go
+++ b/app/websocket_router.go
@@ -1,7 +1,7 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-package api
+package app
import (
l4g "github.com/alecthomas/log4go"
@@ -10,30 +10,34 @@ import (
"github.com/mattermost/platform/utils"
)
+type webSocketHandler interface {
+ ServeWebSocket(*WebConn, *model.WebSocketRequest)
+}
+
type WebSocketRouter struct {
- handlers map[string]*webSocketHandler
+ handlers map[string]webSocketHandler
}
func NewWebSocketRouter() *WebSocketRouter {
router := &WebSocketRouter{}
- router.handlers = make(map[string]*webSocketHandler)
+ router.handlers = make(map[string]webSocketHandler)
return router
}
-func (wr *WebSocketRouter) Handle(action string, handler *webSocketHandler) {
+func (wr *WebSocketRouter) Handle(action string, handler webSocketHandler) {
wr.handlers[action] = handler
}
func (wr *WebSocketRouter) ServeWebSocket(conn *WebConn, r *model.WebSocketRequest) {
if r.Action == "" {
err := model.NewLocAppError("ServeWebSocket", "api.web_socket_router.no_action.app_error", nil, "")
- wr.ReturnWebSocketError(conn, r, err)
+ ReturnWebSocketError(conn, r, err)
return
}
if r.Seq <= 0 {
err := model.NewLocAppError("ServeWebSocket", "api.web_socket_router.bad_seq.app_error", nil, "")
- wr.ReturnWebSocketError(conn, r, err)
+ ReturnWebSocketError(conn, r, err)
return
}
@@ -44,9 +48,9 @@ func (wr *WebSocketRouter) ServeWebSocket(conn *WebConn, r *model.WebSocketReque
return
}
- session := GetSession(token)
+ session, err := GetSession(token)
- if session == nil || session.IsExpired() {
+ if err != nil {
conn.WebSocket.Close()
} else {
go SetStatusOnline(session.UserId, session.Id, false)
@@ -63,16 +67,16 @@ func (wr *WebSocketRouter) ServeWebSocket(conn *WebConn, r *model.WebSocketReque
return
}
- if !conn.isAuthenticated() {
+ if !conn.IsAuthenticated() {
err := model.NewLocAppError("ServeWebSocket", "api.web_socket_router.not_authenticated.app_error", nil, "")
- wr.ReturnWebSocketError(conn, r, err)
+ ReturnWebSocketError(conn, r, err)
return
}
- var handler *webSocketHandler
+ var handler webSocketHandler
if h, ok := wr.handlers[r.Action]; !ok {
err := model.NewLocAppError("ServeWebSocket", "api.web_socket_router.bad_action.app_error", nil, "")
- wr.ReturnWebSocketError(conn, r, err)
+ ReturnWebSocketError(conn, r, err)
return
} else {
handler = h
@@ -81,7 +85,7 @@ func (wr *WebSocketRouter) ServeWebSocket(conn *WebConn, r *model.WebSocketReque
handler.ServeWebSocket(conn, r)
}
-func (wr *WebSocketRouter) ReturnWebSocketError(conn *WebConn, r *model.WebSocketRequest, err *model.AppError) {
+func ReturnWebSocketError(conn *WebConn, r *model.WebSocketRequest, err *model.AppError) {
l4g.Error(utils.T("api.web_socket_router.log.error"), r.Seq, conn.UserId, err.SystemMessage(utils.T), err.DetailedError)
err.DetailedError = ""
diff --git a/app/webtrc.go b/app/webtrc.go
new file mode 100644
index 000000000..b526c96a6
--- /dev/null
+++ b/app/webtrc.go
@@ -0,0 +1,33 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package app
+
+import (
+ "crypto/tls"
+ "encoding/base64"
+ "net/http"
+ "strings"
+
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
+)
+
+func RevokeWebrtcToken(sessionId string) {
+ token := base64.StdEncoding.EncodeToString([]byte(sessionId))
+ data := make(map[string]string)
+ data["janus"] = "remove_token"
+ data["token"] = token
+ data["transaction"] = model.NewId()
+ data["admin_secret"] = *utils.Cfg.WebrtcSettings.GatewayAdminSecret
+
+ rq, _ := http.NewRequest("POST", *utils.Cfg.WebrtcSettings.GatewayAdminUrl, strings.NewReader(model.MapToJson(data)))
+ rq.Header.Set("Content-Type", "application/json")
+
+ // we do not care about the response
+ tr := &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: *utils.Cfg.ServiceSettings.EnableInsecureOutgoingConnections},
+ }
+ httpClient := &http.Client{Transport: tr}
+ httpClient.Do(rq)
+}
diff --git a/cmd/platform/channel.go b/cmd/platform/channel.go
index cf5ef61bc..fd2b097af 100644
--- a/cmd/platform/channel.go
+++ b/cmd/platform/channel.go
@@ -5,7 +5,7 @@ package main
import (
"errors"
- "github.com/mattermost/platform/api"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
"github.com/spf13/cobra"
@@ -117,6 +117,8 @@ func createChannelCmdF(cmd *cobra.Command, args []string) error {
team := getTeamFromTeamArg(teamArg)
+ c := getMockContext()
+
channel := &model.Channel{
TeamId: team.Id,
Name: name,
@@ -124,10 +126,10 @@ func createChannelCmdF(cmd *cobra.Command, args []string) error {
Header: header,
Purpose: purpose,
Type: channelType,
+ CreatorId: c.Session.UserId,
}
- c := getMockContext()
- if _, err := api.CreateChannel(c, channel, false); err != nil {
+ if _, err := app.CreateChannel(channel, false); err != nil {
return err
}
@@ -163,7 +165,7 @@ func removeUserFromChannel(channel *model.Channel, user *model.User, userArg str
CommandPrintErrorln("Can't find user '" + userArg + "'")
return
}
- if err := api.RemoveUserFromChannel(user.Id, "", channel); err != nil {
+ if err := app.RemoveUserFromChannel(user.Id, "", channel); err != nil {
CommandPrintErrorln("Unable to remove '" + userArg + "' from " + channel.Name + ". Error: " + err.Error())
}
}
@@ -197,7 +199,7 @@ func addUserToChannel(channel *model.Channel, user *model.User, userArg string)
CommandPrintErrorln("Can't find user '" + userArg + "'")
return
}
- if _, err := api.AddUserToChannel(user, channel); err != nil {
+ if _, err := app.AddUserToChannel(user, channel); err != nil {
CommandPrintErrorln("Unable to add '" + userArg + "' from " + channel.Name + ". Error: " + err.Error())
}
}
@@ -215,7 +217,7 @@ func deleteChannelsCmdF(cmd *cobra.Command, args []string) error {
CommandPrintErrorln("Unable to find channel '" + args[i] + "'")
continue
}
- if result := <-api.Srv.Store.Channel().Delete(channel.Id, model.GetMillis()); result.Err != nil {
+ if result := <-app.Srv.Store.Channel().Delete(channel.Id, model.GetMillis()); result.Err != nil {
CommandPrintErrorln("Unable to delete channel '" + channel.Name + "' error: " + result.Err.Error())
}
}
@@ -240,7 +242,7 @@ func listChannelsCmdF(cmd *cobra.Command, args []string) error {
CommandPrintErrorln("Unable to find team '" + args[i] + "'")
continue
}
- if result := <-api.Srv.Store.Channel().GetAll(team.Id); result.Err != nil {
+ if result := <-app.Srv.Store.Channel().GetAll(team.Id); result.Err != nil {
CommandPrintErrorln("Unable to list channels for '" + args[i] + "'")
} else {
channels := result.Data.([]*model.Channel)
@@ -275,7 +277,7 @@ func restoreChannelsCmdF(cmd *cobra.Command, args []string) error {
CommandPrintErrorln("Unable to find channel '" + args[i] + "'")
continue
}
- if result := <-api.Srv.Store.Channel().SetDeleteAt(channel.Id, 0, model.GetMillis()); result.Err != nil {
+ if result := <-app.Srv.Store.Channel().SetDeleteAt(channel.Id, 0, model.GetMillis()); result.Err != nil {
CommandPrintErrorln("Unable to restore channel '" + args[i] + "'")
}
}
diff --git a/cmd/platform/channelargs.go b/cmd/platform/channelargs.go
index b94bb6b70..ec697d86b 100644
--- a/cmd/platform/channelargs.go
+++ b/cmd/platform/channelargs.go
@@ -6,7 +6,7 @@ import (
"fmt"
"strings"
- "github.com/mattermost/platform/api"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
)
@@ -42,7 +42,7 @@ func getChannelFromChannelArg(channelArg string) *model.Channel {
return nil
}
- if result := <-api.Srv.Store.Channel().GetByNameIncludeDeleted(team.Id, channelPart); result.Err == nil {
+ if result := <-app.Srv.Store.Channel().GetByNameIncludeDeleted(team.Id, channelPart); result.Err == nil {
channel = result.Data.(*model.Channel)
} else {
fmt.Println(result.Err.Error())
@@ -50,7 +50,7 @@ func getChannelFromChannelArg(channelArg string) *model.Channel {
}
if channel == nil {
- if result := <-api.Srv.Store.Channel().Get(channelPart, true); result.Err == nil {
+ if result := <-app.Srv.Store.Channel().Get(channelPart, true); result.Err == nil {
channel = result.Data.(*model.Channel)
}
}
diff --git a/cmd/platform/import.go b/cmd/platform/import.go
index b482cda7e..09b135354 100644
--- a/cmd/platform/import.go
+++ b/cmd/platform/import.go
@@ -6,7 +6,7 @@ import (
"errors"
"os"
- "github.com/mattermost/platform/api"
+ "github.com/mattermost/platform/app"
"github.com/spf13/cobra"
)
@@ -54,7 +54,7 @@ func slackImportCmdF(cmd *cobra.Command, args []string) error {
CommandPrettyPrintln("Running Slack Import. This may take a long time for large teams or teams with many messages.")
- api.SlackImport(fileReader, fileInfo.Size(), team.Id)
+ app.SlackImport(fileReader, fileInfo.Size(), team.Id)
CommandPrettyPrintln("Finished Slack Import.")
diff --git a/cmd/platform/init.go b/cmd/platform/init.go
index 7cf941e9c..eb842d302 100644
--- a/cmd/platform/init.go
+++ b/cmd/platform/init.go
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/mattermost/platform/api"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
"github.com/spf13/cobra"
@@ -37,8 +38,8 @@ func initDBCommandContext(configFileLocation string) {
utils.ConfigureCmdLineLog()
- api.NewServer()
- api.InitStores()
+ app.NewServer()
+ app.InitStores()
if model.BuildEnterpriseReady == "true" {
api.LoadLicense()
}
diff --git a/cmd/platform/mattermost.go b/cmd/platform/mattermost.go
index b5224c403..2ee539980 100644
--- a/cmd/platform/mattermost.go
+++ b/cmd/platform/mattermost.go
@@ -9,7 +9,7 @@ import (
"fmt"
"os"
- "github.com/mattermost/platform/api"
+ "github.com/mattermost/platform/app"
"github.com/spf13/cobra"
// Plugins
@@ -79,7 +79,7 @@ func resetCmdF(cmd *cobra.Command, args []string) error {
}
}
- api.Srv.Store.DropAllTables()
+ app.Srv.Store.DropAllTables()
CommandPrettyPrintln("Database sucessfully reset")
return nil
diff --git a/cmd/platform/oldcommands.go b/cmd/platform/oldcommands.go
index a599fa47e..ee7f66567 100644
--- a/cmd/platform/oldcommands.go
+++ b/cmd/platform/oldcommands.go
@@ -13,6 +13,7 @@ import (
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/api"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
@@ -69,8 +70,8 @@ func doLegacyCommands() {
doLoadConfig(flagConfigFile)
utils.InitTranslations(utils.Cfg.LocalizationSettings)
utils.ConfigureCmdLineLog()
- api.NewServer()
- api.InitStores()
+ app.NewServer()
+ app.InitStores()
api.InitRouter()
api.InitApi()
web.InitWeb()
@@ -187,9 +188,9 @@ func runCmds() {
func cmdRunClientTests() {
if flagCmdRunWebClientTests {
setupClientTests()
- api.StartServer()
+ app.StartServer()
runWebClientTests()
- api.StopServer()
+ app.StopServer()
}
}
@@ -220,9 +221,8 @@ func cmdCreateTeam() {
team.Email = flagEmail
team.Type = model.TEAM_OPEN
- api.CreateTeam(c, team)
- if c.Err != nil {
- if c.Err.Id != "store.sql_team.save.domain_exists.app_error" {
+ if _, err := app.CreateTeam(team); err != nil {
+ if err.Id != "store.sql_team.save.domain_exists.app_error" {
l4g.Error("%v", c.Err)
flushLogAndExit(1)
}
@@ -257,7 +257,7 @@ func cmdCreateUser() {
}
if len(flagTeamName) > 0 {
- if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
+ if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
@@ -265,7 +265,7 @@ func cmdCreateUser() {
}
}
- ruser, err := api.CreateUser(user)
+ ruser, err := app.CreateUser(user)
if err != nil {
if err.Id != "store.sql_user.save.email_exists.app_error" {
l4g.Error("%v", err)
@@ -274,7 +274,7 @@ func cmdCreateUser() {
}
if team != nil {
- err = api.JoinUserToTeam(team, ruser)
+ err = app.JoinUserToTeam(team, ruser)
if err != nil {
l4g.Error("%v", err)
flushLogAndExit(1)
@@ -303,7 +303,7 @@ func cmdInviteUser() {
}
var team *model.Team
- if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
+ if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
@@ -311,7 +311,7 @@ func cmdInviteUser() {
}
var user *model.User
- if result := <-api.Srv.Store.User().GetByEmail(team.Email); result.Err != nil {
+ if result := <-app.Srv.Store.User().GetByEmail(team.Email); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
@@ -319,8 +319,7 @@ func cmdInviteUser() {
}
invites := []string{flagEmail}
- c := getMockContext()
- api.InviteMembers(team, user.GetDisplayName(), invites, c.GetSiteURL())
+ app.SendInviteEmails(team, user.GetDisplayName(), invites, *utils.Cfg.ServiceSettings.SiteURL)
os.Exit(0)
}
@@ -333,7 +332,7 @@ func cmdVersion() {
fmt.Fprintln(os.Stderr, "Build Date: "+model.BuildDate)
fmt.Fprintln(os.Stderr, "Build Hash: "+model.BuildHash)
fmt.Fprintln(os.Stderr, "Build Enterprise Ready: "+model.BuildEnterpriseReady)
- fmt.Fprintln(os.Stderr, "DB Version: "+api.Srv.Store.(*store.SqlStore).SchemaVersion)
+ fmt.Fprintln(os.Stderr, "DB Version: "+app.Srv.Store.(*store.SqlStore).SchemaVersion)
os.Exit(0)
}
@@ -361,7 +360,7 @@ func cmdAssignRole() {
}
var user *model.User
- if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
+ if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
@@ -369,7 +368,7 @@ func cmdAssignRole() {
}
if !user.IsInRole(flagRole) {
- api.UpdateUserRoles(user, flagRole)
+ app.UpdateUserRoles(user.Id, flagRole)
}
os.Exit(0)
@@ -404,7 +403,7 @@ func cmdCreateChannel() {
}
var team *model.Team
- if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
+ if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
l4g.Error("%v %v", utils.T(result.Err.Message), result.Err.DetailedError)
flushLogAndExit(1)
} else {
@@ -412,7 +411,7 @@ func cmdCreateChannel() {
}
var user *model.User
- if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
+ if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
l4g.Error("%v %v", utils.T(result.Err.Message), result.Err.DetailedError)
flushLogAndExit(1)
} else {
@@ -431,7 +430,7 @@ func cmdCreateChannel() {
channel.Header = flagChannelHeader
channel.Purpose = flagChannelPurpose
- if _, err := api.CreateChannel(c, channel, true); err != nil {
+ if _, err := app.CreateChannel(channel, true); err != nil {
l4g.Error("%v %v", utils.T(err.Message), err.DetailedError)
flushLogAndExit(1)
}
@@ -463,7 +462,7 @@ func cmdJoinChannel() {
}
var team *model.Team
- if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
+ if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
@@ -471,7 +470,7 @@ func cmdJoinChannel() {
}
var user *model.User
- if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
+ if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
@@ -479,14 +478,14 @@ func cmdJoinChannel() {
}
var channel *model.Channel
- if result := <-api.Srv.Store.Channel().GetByName(team.Id, flagChannelName); result.Err != nil {
+ if result := <-app.Srv.Store.Channel().GetByName(team.Id, flagChannelName); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
channel = result.Data.(*model.Channel)
}
- _, err := api.AddUserToChannel(user, channel)
+ _, err := app.AddUserToChannel(user, channel)
if err != nil {
l4g.Error("%v", err)
flushLogAndExit(1)
@@ -524,7 +523,7 @@ func cmdLeaveChannel() {
}
var team *model.Team
- if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
+ if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
@@ -532,7 +531,7 @@ func cmdLeaveChannel() {
}
var user *model.User
- if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
+ if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
@@ -540,14 +539,14 @@ func cmdLeaveChannel() {
}
var channel *model.Channel
- if result := <-api.Srv.Store.Channel().GetByName(team.Id, flagChannelName); result.Err != nil {
+ if result := <-app.Srv.Store.Channel().GetByName(team.Id, flagChannelName); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
channel = result.Data.(*model.Channel)
}
- err := api.RemoveUserFromChannel(user.Id, user.Id, channel)
+ err := app.RemoveUserFromChannel(user.Id, user.Id, channel)
if err != nil {
l4g.Error("%v", err)
flushLogAndExit(1)
@@ -570,14 +569,14 @@ func cmdListChannels() {
}
var team *model.Team
- if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
+ if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
team = result.Data.(*model.Team)
}
- if result := <-api.Srv.Store.Channel().GetAll(team.Id); result.Err != nil {
+ if result := <-app.Srv.Store.Channel().GetAll(team.Id); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
@@ -615,7 +614,7 @@ func cmdRestoreChannel() {
}
var team *model.Team
- if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
+ if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
@@ -623,7 +622,7 @@ func cmdRestoreChannel() {
}
var channel *model.Channel
- if result := <-api.Srv.Store.Channel().GetAll(team.Id); result.Err != nil {
+ if result := <-app.Srv.Store.Channel().GetAll(team.Id); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
@@ -637,7 +636,7 @@ func cmdRestoreChannel() {
}
}
- if result := <-api.Srv.Store.Channel().SetDeleteAt(channel.Id, 0, model.GetMillis()); result.Err != nil {
+ if result := <-app.Srv.Store.Channel().SetDeleteAt(channel.Id, 0, model.GetMillis()); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
}
@@ -659,7 +658,7 @@ func cmdJoinTeam() {
}
var team *model.Team
- if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
+ if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
@@ -667,14 +666,14 @@ func cmdJoinTeam() {
}
var user *model.User
- if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
+ if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
user = result.Data.(*model.User)
}
- err := api.JoinUserToTeam(team, user)
+ err := app.JoinUserToTeam(team, user)
if err != nil {
l4g.Error("%v", err)
flushLogAndExit(1)
@@ -697,7 +696,7 @@ func cmdLeaveTeam() {
}
var team *model.Team
- if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
+ if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
@@ -705,14 +704,14 @@ func cmdLeaveTeam() {
}
var user *model.User
- if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
+ if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
user = result.Data.(*model.User)
}
- err := api.LeaveTeam(team, user)
+ err := app.LeaveTeam(team, user)
if err != nil {
l4g.Error("%v", err)
@@ -741,14 +740,14 @@ func cmdResetPassword() {
}
var user *model.User
- if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
+ if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
user = result.Data.(*model.User)
}
- if result := <-api.Srv.Store.User().UpdatePassword(user.Id, model.HashPassword(flagPassword)); result.Err != nil {
+ if result := <-app.Srv.Store.User().UpdatePassword(user.Id, model.HashPassword(flagPassword)); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
}
@@ -766,14 +765,14 @@ func cmdResetMfa() {
var user *model.User
if len(flagEmail) > 0 {
- if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
+ if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
user = result.Data.(*model.User)
}
} else {
- if result := <-api.Srv.Store.User().GetByUsername(flagUsername); result.Err != nil {
+ if result := <-app.Srv.Store.User().GetByUsername(flagUsername); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
@@ -781,7 +780,7 @@ func cmdResetMfa() {
}
}
- if err := api.DeactivateMfa(user.Id); err != nil {
+ if err := app.DeactivateMfa(user.Id); err != nil {
l4g.Error("%v", err)
flushLogAndExit(1)
}
@@ -798,7 +797,7 @@ func cmdPermDeleteUser() {
}
var user *model.User
- if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
+ if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
@@ -823,7 +822,7 @@ func cmdPermDeleteUser() {
flushLogAndExit(1)
}
- if err := api.PermanentDeleteUser(user); err != nil {
+ if err := app.PermanentDeleteUser(user); err != nil {
l4g.Error("%v", err)
flushLogAndExit(1)
} else {
@@ -841,7 +840,7 @@ func cmdPermDeleteTeam() {
}
var team *model.Team
- if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
+ if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
@@ -866,7 +865,7 @@ func cmdPermDeleteTeam() {
flushLogAndExit(1)
}
- if err := api.PermanentDeleteTeam(team); err != nil {
+ if err := app.PermanentDeleteTeam(team); err != nil {
l4g.Error("%v", err)
flushLogAndExit(1)
} else {
@@ -896,7 +895,7 @@ func cmdPermDeleteAllUsers() {
flushLogAndExit(1)
}
- if err := api.PermanentDeleteAllUsers(); err != nil {
+ if err := app.PermanentDeleteAllUsers(); err != nil {
l4g.Error("%v", err)
flushLogAndExit(1)
} else {
@@ -927,7 +926,7 @@ func cmdResetDatabase() {
flushLogAndExit(1)
}
- api.Srv.Store.DropAllTables()
+ app.Srv.Store.DropAllTables()
fmt.Print("SUCCESS: Database reset.")
flushLogAndExit(0)
}
@@ -1022,7 +1021,7 @@ func cmdActivateUser() {
}
var user *model.User
- if result := <-api.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
+ if result := <-app.Srv.Store.User().GetByEmail(flagEmail); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
@@ -1033,7 +1032,7 @@ func cmdActivateUser() {
l4g.Error("%v", utils.T("api.user.update_active.no_deactivate_ldap.app_error"))
}
- if _, err := api.UpdateActive(user, !flagUserSetInactive); err != nil {
+ if _, err := app.UpdateActive(user, !flagUserSetInactive); err != nil {
l4g.Error("%v", err)
}
@@ -1054,7 +1053,7 @@ func cmdSlackImport() {
}
var team *model.Team
- if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
+ if result := <-app.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
@@ -1076,7 +1075,7 @@ func cmdSlackImport() {
fmt.Fprintln(os.Stdout, "Running Slack Import. This may take a long time for large teams or teams with many messages.")
- api.SlackImport(fileReader, fileInfo.Size(), team.Id)
+ app.SlackImport(fileReader, fileInfo.Size(), team.Id)
flushLogAndExit(0)
}
diff --git a/cmd/platform/roles.go b/cmd/platform/roles.go
index 7b635c5a3..a65eb2bd6 100644
--- a/cmd/platform/roles.go
+++ b/cmd/platform/roles.go
@@ -5,7 +5,7 @@ package main
import (
"errors"
- "github.com/mattermost/platform/api"
+ "github.com/mattermost/platform/app"
"github.com/spf13/cobra"
)
@@ -49,7 +49,7 @@ func makeSystemAdminCmdF(cmd *cobra.Command, args []string) error {
return errors.New("Unable to find user '" + args[i] + "'")
}
- if _, err := api.UpdateUserRoles(user, "system_admin system_user"); err != nil {
+ if _, err := app.UpdateUserRoles(user.Id, "system_admin system_user"); err != nil {
return err
}
}
@@ -69,7 +69,7 @@ func makeMemberCmdF(cmd *cobra.Command, args []string) error {
return errors.New("Unable to find user '" + args[i] + "'")
}
- if _, err := api.UpdateUserRoles(user, "system_user"); err != nil {
+ if _, err := app.UpdateUserRoles(user.Id, "system_user"); err != nil {
return err
}
}
diff --git a/cmd/platform/server.go b/cmd/platform/server.go
index 83185a45f..560403a6b 100644
--- a/cmd/platform/server.go
+++ b/cmd/platform/server.go
@@ -16,6 +16,7 @@ import (
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/api"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/manualtesting"
"github.com/mattermost/platform/model"
@@ -69,8 +70,8 @@ func runServer(configFileLocation string) {
cmdUpdateDb30()
- api.NewServer()
- api.InitStores()
+ app.NewServer()
+ app.InitStores()
api.InitRouter()
api.InitApi()
web.InitWeb()
@@ -90,7 +91,7 @@ func runServer(configFileLocation string) {
resetStatuses()
- api.StartServer()
+ app.StartServer()
// If we allow testing then listen for manual testing URL hits
if utils.Cfg.ServiceSettings.EnableTesting {
@@ -126,7 +127,7 @@ func runServer(configFileLocation string) {
einterfaces.GetMetricsInterface().StopServer()
}
- api.StopServer()
+ app.StopServer()
}
func runSecurityAndDiagnosticsJob() {
@@ -135,20 +136,20 @@ func runSecurityAndDiagnosticsJob() {
}
func resetStatuses() {
- if result := <-api.Srv.Store.Status().ResetAll(); result.Err != nil {
+ if result := <-app.Srv.Store.Status().ResetAll(); result.Err != nil {
l4g.Error(utils.T("mattermost.reset_status.error"), result.Err.Error())
}
}
func setDiagnosticId() {
- if result := <-api.Srv.Store.System().Get(); result.Err == nil {
+ if result := <-app.Srv.Store.System().Get(); result.Err == nil {
props := result.Data.(model.StringMap)
id := props[model.SYSTEM_DIAGNOSTIC_ID]
if len(id) == 0 {
id = model.NewId()
systemId := &model.System{Name: model.SYSTEM_DIAGNOSTIC_ID, Value: id}
- <-api.Srv.Store.System().Save(systemId)
+ <-app.Srv.Store.System().Save(systemId)
}
utils.CfgDiagnosticId = id
@@ -157,7 +158,7 @@ func setDiagnosticId() {
func doSecurityAndDiagnostics() {
if *utils.Cfg.ServiceSettings.EnableSecurityFixAlert {
- if result := <-api.Srv.Store.System().Get(); result.Err == nil {
+ if result := <-app.Srv.Store.System().Get(); result.Err == nil {
props := result.Data.(model.StringMap)
lastSecurityTime, _ := strconv.ParseInt(props[model.SYSTEM_LAST_SECURITY_TIME], 10, 0)
currentTime := model.GetMillis()
@@ -182,20 +183,20 @@ func doSecurityAndDiagnostics() {
systemSecurityLastTime := &model.System{Name: model.SYSTEM_LAST_SECURITY_TIME, Value: strconv.FormatInt(currentTime, 10)}
if lastSecurityTime == 0 {
- <-api.Srv.Store.System().Save(systemSecurityLastTime)
+ <-app.Srv.Store.System().Save(systemSecurityLastTime)
} else {
- <-api.Srv.Store.System().Update(systemSecurityLastTime)
+ <-app.Srv.Store.System().Update(systemSecurityLastTime)
}
- if ucr := <-api.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil {
+ if ucr := <-app.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil {
v.Set(utils.PROP_DIAGNOSTIC_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10))
}
- if ucr := <-api.Srv.Store.Status().GetTotalActiveUsersCount(); ucr.Err == nil {
+ if ucr := <-app.Srv.Store.Status().GetTotalActiveUsersCount(); ucr.Err == nil {
v.Set(utils.PROP_DIAGNOSTIC_ACTIVE_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10))
}
- if tcr := <-api.Srv.Store.Team().AnalyticsTeamCount(); tcr.Err == nil {
+ if tcr := <-app.Srv.Store.Team().AnalyticsTeamCount(); tcr.Err == nil {
v.Set(utils.PROP_DIAGNOSTIC_TEAM_COUNT, strconv.FormatInt(tcr.Data.(int64), 10))
}
@@ -212,7 +213,7 @@ func doSecurityAndDiagnostics() {
for _, bulletin := range bulletins {
if bulletin.AppliesToVersion == model.CurrentVersion {
if props["SecurityBulletin_"+bulletin.Id] == "" {
- if results := <-api.Srv.Store.User().GetSystemAdminProfiles(); results.Err != nil {
+ if results := <-app.Srv.Store.User().GetSystemAdminProfiles(); results.Err != nil {
l4g.Error(utils.T("mattermost.system_admins.error"))
return
} else {
@@ -238,7 +239,7 @@ func doSecurityAndDiagnostics() {
}
bulletinSeen := &model.System{Name: "SecurityBulletin_" + bulletin.Id, Value: bulletin.Id}
- <-api.Srv.Store.System().Save(bulletinSeen)
+ <-app.Srv.Store.System().Save(bulletinSeen)
}
}
}
@@ -257,15 +258,15 @@ func sendServerDiagnostics() {
var activeUserCount int64
var teamCount int64
- if ucr := <-api.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil {
+ if ucr := <-app.Srv.Store.User().GetTotalUsersCount(); ucr.Err == nil {
userCount = ucr.Data.(int64)
}
- if ucr := <-api.Srv.Store.Status().GetTotalActiveUsersCount(); ucr.Err == nil {
+ if ucr := <-app.Srv.Store.Status().GetTotalActiveUsersCount(); ucr.Err == nil {
activeUserCount = ucr.Data.(int64)
}
- if tcr := <-api.Srv.Store.Team().AnalyticsTeamCount(); tcr.Err == nil {
+ if tcr := <-app.Srv.Store.Team().AnalyticsTeamCount(); tcr.Err == nil {
teamCount = tcr.Data.(int64)
}
diff --git a/cmd/platform/team.go b/cmd/platform/team.go
index 8fecda6e1..1dc5d46eb 100644
--- a/cmd/platform/team.go
+++ b/cmd/platform/team.go
@@ -6,7 +6,7 @@ import (
"errors"
"fmt"
- "github.com/mattermost/platform/api"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/spf13/cobra"
)
@@ -92,10 +92,8 @@ func createTeamCmdF(cmd *cobra.Command, args []string) error {
Type: teamType,
}
- c := getMockContext()
- api.CreateTeam(c, team)
- if c.Err != nil {
- return errors.New("Team creation failed: " + c.Err.Error())
+ if _, err := app.CreateTeam(team); err != nil {
+ return errors.New("Team creation failed: " + err.Error())
}
return nil
@@ -126,7 +124,7 @@ func removeUserFromTeam(team *model.Team, user *model.User, userArg string) {
CommandPrintErrorln("Can't find user '" + userArg + "'")
return
}
- if err := api.LeaveTeam(team, user); err != nil {
+ if err := app.LeaveTeam(team, user); err != nil {
CommandPrintErrorln("Unable to remove '" + userArg + "' from " + team.Name + ". Error: " + err.Error())
}
}
@@ -156,7 +154,7 @@ func addUserToTeam(team *model.Team, user *model.User, userArg string) {
CommandPrintErrorln("Can't find user '" + userArg + "'")
return
}
- if err := api.JoinUserToTeam(team, user); err != nil {
+ if err := app.JoinUserToTeam(team, user); err != nil {
CommandPrintErrorln("Unable to add '" + userArg + "' to " + team.Name)
}
}
@@ -201,5 +199,5 @@ func deleteTeamsCmdF(cmd *cobra.Command, args []string) error {
}
func deleteTeam(team *model.Team) *model.AppError {
- return api.PermanentDeleteTeam(team)
+ return app.PermanentDeleteTeam(team)
}
diff --git a/cmd/platform/teamargs.go b/cmd/platform/teamargs.go
index 5ad56f5d9..506cc88ef 100644
--- a/cmd/platform/teamargs.go
+++ b/cmd/platform/teamargs.go
@@ -3,7 +3,7 @@
package main
import (
- "github.com/mattermost/platform/api"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
)
@@ -18,12 +18,12 @@ func getTeamsFromTeamArgs(teamArgs []string) []*model.Team {
func getTeamFromTeamArg(teamArg string) *model.Team {
var team *model.Team
- if result := <-api.Srv.Store.Team().GetByName(teamArg); result.Err == nil {
+ if result := <-app.Srv.Store.Team().GetByName(teamArg); result.Err == nil {
team = result.Data.(*model.Team)
}
if team == nil {
- if result := <-api.Srv.Store.Team().Get(teamArg); result.Err == nil {
+ if result := <-app.Srv.Store.Team().Get(teamArg); result.Err == nil {
team = result.Data.(*model.Team)
}
}
diff --git a/cmd/platform/test.go b/cmd/platform/test.go
index d82734c75..65a8528d0 100644
--- a/cmd/platform/test.go
+++ b/cmd/platform/test.go
@@ -10,6 +10,7 @@ import (
"os/exec"
"github.com/mattermost/platform/api"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/utils"
"github.com/spf13/cobra"
)
@@ -38,9 +39,9 @@ func webClientTestsCmdF(cmd *cobra.Command, args []string) error {
api.InitRouter()
api.InitApi()
setupClientTests()
- api.StartServer()
+ app.StartServer()
runWebClientTests()
- api.StopServer()
+ app.StopServer()
return nil
}
@@ -59,18 +60,34 @@ func executeTestCommand(cmd *exec.Cmd) {
cmdOutPipe, err := cmd.StdoutPipe()
if err != nil {
CommandPrintErrorln("Failed to run tests")
+ os.Exit(1)
+ return
+ }
+
+ cmdErrOutPipe, err := cmd.StderrPipe()
+ if err != nil {
+ CommandPrintErrorln("Failed to run tests")
+ os.Exit(1)
return
}
cmdOutReader := bufio.NewScanner(cmdOutPipe)
+ cmdErrOutReader := bufio.NewScanner(cmdErrOutPipe)
go func() {
for cmdOutReader.Scan() {
fmt.Println(cmdOutReader.Text())
}
}()
+ go func() {
+ for cmdErrOutReader.Scan() {
+ fmt.Println(cmdErrOutReader.Text())
+ }
+ }()
+
if err := cmd.Run(); err != nil {
CommandPrintErrorln("Client Tests failed")
+ os.Exit(1)
return
}
}
diff --git a/cmd/platform/user.go b/cmd/platform/user.go
index 373274241..fa93e418c 100644
--- a/cmd/platform/user.go
+++ b/cmd/platform/user.go
@@ -6,7 +6,7 @@ import (
"errors"
"fmt"
- "github.com/mattermost/platform/api"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
@@ -174,7 +174,7 @@ func changeUserActiveStatus(user *model.User, userArg string, activate bool) {
CommandPrintErrorln(utils.T("api.user.update_active.no_deactivate_ldap.app_error"))
return
}
- if _, err := api.UpdateActive(user, activate); err != nil {
+ if _, err := app.UpdateActive(user, activate); err != nil {
CommandPrintErrorln("Unable to change activation status of user: " + userArg)
}
}
@@ -220,13 +220,13 @@ func userCreateCmdF(cmd *cobra.Command, args []string) error {
Locale: locale,
}
- ruser, err := api.CreateUser(user)
+ ruser, err := app.CreateUser(user)
if err != nil {
return errors.New("Unable to create user. Error: " + err.Error())
}
if system_admin {
- api.UpdateUserRoles(ruser, "system_user system_admin")
+ app.UpdateUserRoles(ruser.Id, "system_user system_admin")
}
CommandPrettyPrintln("Created User")
@@ -261,7 +261,7 @@ func inviteUser(email string, team *model.Team, teamArg string) {
CommandPrintErrorln("Can't find team '" + teamArg + "'")
return
}
- api.InviteMembers(team, "Administrator", invites, *utils.Cfg.ServiceSettings.SiteURL)
+ app.SendInviteEmails(team, "Administrator", invites, *utils.Cfg.ServiceSettings.SiteURL)
CommandPrettyPrintln("Invites may or may not have been sent.")
}
@@ -277,7 +277,7 @@ func resetUserPasswordCmdF(cmd *cobra.Command, args []string) error {
}
password := args[1]
- if result := <-api.Srv.Store.User().UpdatePassword(user.Id, model.HashPassword(password)); result.Err != nil {
+ if result := <-app.Srv.Store.User().UpdatePassword(user.Id, model.HashPassword(password)); result.Err != nil {
return result.Err
}
@@ -297,7 +297,7 @@ func resetUserMfaCmdF(cmd *cobra.Command, args []string) error {
return errors.New("Unable to find user '" + args[i] + "'")
}
- if err := api.DeactivateMfa(user.Id); err != nil {
+ if err := app.DeactivateMfa(user.Id); err != nil {
return err
}
}
@@ -334,7 +334,7 @@ func deleteUserCmdF(cmd *cobra.Command, args []string) error {
return errors.New("Unable to find user '" + args[i] + "'")
}
- if err := api.PermanentDeleteUser(user); err != nil {
+ if err := app.PermanentDeleteUser(user); err != nil {
return err
}
}
@@ -364,7 +364,7 @@ func deleteAllUsersCommandF(cmd *cobra.Command, args []string) error {
}
}
- if err := api.PermanentDeleteAllUsers(); err != nil {
+ if err := app.PermanentDeleteAllUsers(); err != nil {
return err
} else {
CommandPrettyPrintln("Sucsessfull. All users deleted.")
@@ -423,7 +423,7 @@ func verifyUserCmdF(cmd *cobra.Command, args []string) error {
if user == nil {
CommandPrintErrorln("Unable to find user '" + args[i] + "'")
}
- if cresult := <-api.Srv.Store.User().VerifyEmail(user.Id); cresult.Err != nil {
+ if cresult := <-app.Srv.Store.User().VerifyEmail(user.Id); cresult.Err != nil {
CommandPrintErrorln("Unable to verify '" + args[i] + "' email. Error: " + cresult.Err.Error())
}
}
diff --git a/cmd/platform/userargs.go b/cmd/platform/userargs.go
index 9ac00ae70..31ae3c251 100644
--- a/cmd/platform/userargs.go
+++ b/cmd/platform/userargs.go
@@ -3,7 +3,7 @@
package main
import (
- "github.com/mattermost/platform/api"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
)
@@ -18,18 +18,18 @@ func getUsersFromUserArgs(userArgs []string) []*model.User {
func getUserFromUserArg(userArg string) *model.User {
var user *model.User
- if result := <-api.Srv.Store.User().GetByEmail(userArg); result.Err == nil {
+ if result := <-app.Srv.Store.User().GetByEmail(userArg); result.Err == nil {
user = result.Data.(*model.User)
}
if user == nil {
- if result := <-api.Srv.Store.User().GetByUsername(userArg); result.Err == nil {
+ if result := <-app.Srv.Store.User().GetByUsername(userArg); result.Err == nil {
user = result.Data.(*model.User)
}
}
if user == nil {
- if result := <-api.Srv.Store.User().Get(userArg); result.Err == nil {
+ if result := <-app.Srv.Store.User().Get(userArg); result.Err == nil {
user = result.Data.(*model.User)
}
}
diff --git a/cmd/platform/version.go b/cmd/platform/version.go
index 8da34e3a9..8978aa841 100644
--- a/cmd/platform/version.go
+++ b/cmd/platform/version.go
@@ -3,7 +3,7 @@
package main
import (
- "github.com/mattermost/platform/api"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/spf13/cobra"
@@ -26,5 +26,5 @@ func printVersion() {
CommandPrintln("Build Date: " + model.BuildDate)
CommandPrintln("Build Hash: " + model.BuildHash)
CommandPrintln("Build Enterprise Ready: " + model.BuildEnterpriseReady)
- CommandPrintln("DB Version: " + api.Srv.Store.(*store.SqlStore).SchemaVersion)
+ CommandPrintln("DB Version: " + app.Srv.Store.(*store.SqlStore).SchemaVersion)
}
diff --git a/config/config.json b/config/config.json
index c979c778b..f538e9686 100644
--- a/config/config.json
+++ b/config/config.json
@@ -35,7 +35,10 @@
"WebsocketPort": 80,
"WebserverMode": "gzip",
"EnableCustomEmoji": false,
- "RestrictCustomEmojiCreation": "all"
+ "RestrictCustomEmojiCreation": "all",
+ "RestrictPostDelete": "all",
+ "AllowEditPost": "always",
+ "PostEditTimeLimit": 300
},
"TeamSettings": {
"SiteName": "Mattermost",
@@ -109,15 +112,15 @@
"EnableSignUpWithEmail": true,
"EnableSignInWithEmail": true,
"EnableSignInWithUsername": true,
- "SendEmailNotifications": false,
+ "SendEmailNotifications": true,
"RequireEmailVerification": false,
"FeedbackName": "",
- "FeedbackEmail": "",
+ "FeedbackEmail": "test@example.com",
"FeedbackOrganization": "",
"SMTPUsername": "",
"SMTPPassword": "",
- "SMTPServer": "",
- "SMTPPort": "",
+ "SMTPServer": "dockerhost",
+ "SMTPPort": "2500",
"ConnectionSecurity": "",
"InviteSalt": "",
"PasswordResetSalt": "",
diff --git a/einterfaces/cluster.go b/einterfaces/cluster.go
index 0d7bf7e86..6cf57308c 100644
--- a/einterfaces/cluster.go
+++ b/einterfaces/cluster.go
@@ -12,7 +12,7 @@ type ClusterInterface interface {
StopInterNodeCommunication()
GetClusterInfos() []*model.ClusterInfo
GetClusterStats() ([]*model.ClusterStats, *model.AppError)
- RemoveAllSessionsForUserId(userId string)
+ ClearSessionCacheForUser(userId string)
InvalidateCacheForUser(userId string)
InvalidateCacheForChannel(channelId string)
InvalidateCacheForChannelPosts(channelId string)
diff --git a/glide.lock b/glide.lock
index 35dfce6bb..d91c3c347 100644
--- a/glide.lock
+++ b/glide.lock
@@ -1,5 +1,5 @@
-hash: a8fe20af467bdf57b944108bb00f8b410d73ad13a2cc5616ae2c410d510e5d90
-updated: 2016-11-23T19:38:17.926223307-05:00
+hash: 67ac70374ac7d1acb02736f628409e406cb55f343a260ccf78c4f324ec2df45a
+updated: 2016-12-25T21:59:49.671116871+05:30
imports:
- name: github.com/alecthomas/log4go
version: e5dc62318d9bd58682f1dceb53a4b24e8253682f
@@ -11,6 +11,10 @@ imports:
version: 96977cbd42e27be71f9f731db6634123de7e861a
- name: github.com/disintegration/imaging
version: 5b7e22645c93e3f3911b36b7d66bf8799f3eddfd
+- name: github.com/dyatlov/go-opengraph
+ version: 41a3523719dfbe7e8f853fbd4061867543db5270
+ subpackages:
+ - opengraph
- name: github.com/go-gorp/gorp
version: 0c9bc0918534d133cedb439a24adc7cbe66e4a9d
- name: github.com/go-ldap/ldap
@@ -136,6 +140,8 @@ imports:
version: 4971afdc2f162e82d185353533d3cf16188a9f4e
subpackages:
- context
+ - html
+ - html/atom
- publicsuffix
- name: golang.org/x/sys
version: 30237cf4eefd639b184d1f2cb77a581ea0be8947
diff --git a/glide.yaml b/glide.yaml
index 8846dccdb..ae0560199 100644
--- a/glide.yaml
+++ b/glide.yaml
@@ -88,3 +88,7 @@ import:
- package: github.com/prometheus/procfs
- package: github.com/spf13/cobra
- package: github.com/spf13/pflag
+- package: github.com/dyatlov/go-opengraph
+ version: 41a3523719dfbe7e8f853fbd4061867543db5270
+ subpackages:
+ - opengraph
diff --git a/i18n/de.json b/i18n/de.json
index 311798c74..8acaec164 100644
--- a/i18n/de.json
+++ b/i18n/de.json
@@ -305,15 +305,15 @@
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error",
- "translation": "Fehler beim Abrufen des Nutzers während des Sicherns der Mitteilung des Kanalanzeigennamen %v"
+ "translation": "Fehler beim Abrufen des Nutzers während des Sicherns der Kanalüberschrift %v"
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from",
"translation": "%s hat den Kanal-Anzeigenamen von %s nach %s aktualisiert"
},
{
- "id": "api.channel.post_update_channel_header_message_and_forget.join_leave.error",
- "translation": "Die Begrüßungs-/Abschiedsnachricht %v konnte nicht gesendet werden"
+ "id": "api.channel.post_update_channel_header_message_and_forget.post.error",
+ "translation": "Failed to post update channel header message"
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.removed",
@@ -800,12 +800,16 @@
"translation": "Ungültiger {{.Name}} Parameter"
},
{
+ "id": "api.context.invalid_session.error",
+ "translation": "Invalid session err=%v"
+ },
+ {
"id": "api.context.invalid_team_url.debug",
"translation": "Es wurde an einer Stelle auf die Team-URL zugegriffen, an der dies nicht erlaubt ist. Die Team-URL sollte nicht in API- oder in sonstigen Funktionen genutzt werden, die teamunabhängig sind"
},
{
"id": "api.context.invalid_token.error",
- "translation": "Ungültiger Session token=%v, err=%v"
+ "translation": "Invalid session token={{.Token}}, err={{.Error}}"
},
{
"id": "api.context.invalidate_all_caches",
@@ -1419,7 +1423,7 @@
},
{
"id": "api.post.delete_flagged_post.app_error.warn",
- "translation": "Konnte markierte Nachrichten Einstellungen beim Entfernen der Nachricht nicht löschen, err=%v"
+ "translation": "Konnte Nachrichten-Markierungseinstellung beim Entfernen der Nachricht nicht löschen, err=%v"
},
{
"id": "api.post.delete_post.permissions.app_error",
@@ -1468,26 +1472,10 @@
"translation": "Sie haben nicht die erforderlichen Zugriffsrechte"
},
{
- "id": "api.post.handle_post_events_and_forget.channel.error",
- "translation": "Fehler beim Abruf des Kanals festgestellt, channel_id=%s, err=%v"
- },
- {
"id": "api.post.handle_post_events_and_forget.members.error",
"translation": "Fehler beim Abruf der Kanalmitglieder, channel_id=%v, err=%v"
},
{
- "id": "api.post.handle_post_events_and_forget.profiles.error",
- "translation": "Fehler beim Abruf des Benutzer-Profils, team_id=%v, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.team.error",
- "translation": "Fehler beim Abruf des Teams, team_id=%s, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.user.error",
- "translation": "Fehler beim Abruf des Benutzers, user_id=%s, err=%v"
- },
- {
"id": "api.post.handle_webhook_events_and_forget.create_post.error",
"translation": "Fehler beim Erstellen der Antwort, Fehler=%v"
},
@@ -1496,10 +1484,6 @@
"translation": "Event POST fehlgeschlagen, Fehler=%s"
},
{
- "id": "api.post.handle_webhook_events_and_forget.getting.error",
- "translation": "Fehler beim Abruf der Webhooks pro Team festgestellt, Fehler=%v"
- },
- {
"id": "api.post.init.debug",
"translation": "Initialisiere API-Routen für Nachrichten"
},
@@ -1520,20 +1504,16 @@
"translation": "Fehler beim Speichern der Direktkanal-Einstellung user_id=%v other_user_id=%v err=%v"
},
{
- "id": "api.post.notification.here.warn",
- "translation": "Konnte keine Benachrichtigung an Onlinebenutzer senden mit @here, err=%v"
- },
- {
"id": "api.post.notification.member_profile.warn",
"translation": "Konnte Profil für Kanalmitglied laden, user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
- "translation": "Leere Push-Mitteilung an %v mit channel_id %v"
+ "id": "api.post.send_notifications.user_id.debug",
+ "translation": "Post creator not in channel for the post, no notification sent post_id=%v channel_id=%v user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.comment_thread.error",
- "translation": "Fehler beim Abrufen der Nachrichten des Kommentarthreads in Benachrichtigung root_post_id=%v, err=%v"
+ "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
+ "translation": "Leere Push-Mitteilung an %v mit channel_id %v"
},
{
"id": "api.post.send_notifications_and_forget.files.error",
@@ -1581,25 +1561,13 @@
},
{
"id": "api.post.send_notifications_and_forget.push_notification.error",
- "translation": "Fehler beim Senden der Push-Mitteilung device_id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.send.error",
- "translation": "Die E-Mail konnte nicht gesendet werden. email=%v err=%v"
+ "translation": "Failed to send push device_id={{.DeviceId}}, err={{.Error}}"
},
{
"id": "api.post.send_notifications_and_forget.sent",
"translation": "{{.Prefix}} {{.Filenames}} gesendet"
},
{
- "id": "api.post.send_notifications_and_forget.sessions.error",
- "translation": "Fehler beim Laden der Sitzungen in Benachrichtigungen id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.user_id.error",
- "translation": "Nachricht user_id nicht wiedergegeben durch GetProfiles user_id=%v"
- },
- {
"id": "api.post.update_mention_count_and_forget.update_error",
"translation": "Fehler beim Aktualisieren der Erwähnungsanzahl, post_id=%v channel_id=%v err=%v"
},
@@ -1612,10 +1580,18 @@
"translation": "Sie haben nicht die erforderlichen Zugriffsrechte"
},
{
+ "id": "api.post.update_post.permissions_denied.app_error",
+ "translation": "Teamerstellung wurde deaktiviert. Bitte fragen Sie Ihren Administrator, um Details zu erfahren."
+ },
+ {
"id": "api.post.update_post.permissions_details.app_error",
"translation": "{{.PostId}} wurde bereits gelöscht"
},
{
+ "id": "api.post.update_post.permissions_time_limit.app_error",
+ "translation": "Post edit is only allowed for {{.timeLimit}} seconds. Please ask your systems administrator for details."
+ },
+ {
"id": "api.post.update_post.system_message.app_error",
"translation": "Konnte die Systemnachricht nicht aktualisieren"
},
@@ -1717,11 +1693,11 @@
},
{
"id": "api.slackimport.slack_add_bot_user.email_pwd",
- "translation": "Slack Bot/Integration Nachrichten Import Benutzer: E-Mail, Passwort: {{.Email}}, {{.Password}}\r\n"
+ "translation": "Slack-Bot-/Integrationsnachrichten-Importbenutzer: E-Mail, Passwort: {{.Email}}, {{.Password}}\r\n"
},
{
"id": "api.slackimport.slack_add_bot_user.unable_import",
- "translation": "Konnte Slack Bot/Integration Nachrichten Import Benutzer nicht importieren: {{.Username}}\r\n"
+ "translation": "Konnte Slack-Bot-/Integrationsnachrichten-Importbenutzer nicht importieren: {{.Username}}\r\n"
},
{
"id": "api.slackimport.slack_add_channels.added",
@@ -2273,7 +2249,7 @@
},
{
"id": "api.user.add_direct_channels_and_forget.failed.error",
- "translation": "Fehler beim Hinzufügen von Kanaleinstellungen für Benutzer user_id=%s, team_id=%s, err=%v"
+ "translation": "Failed to add direct channel preferences for user user_id={{.UserId}}, team_id={{.TeamId}}, err={{.Error}}"
},
{
"id": "api.user.authorize_oauth_user.bad_response.app_error",
@@ -2776,10 +2752,50 @@
"translation": "Es trat ein Fehler auf beim Versuch, den WebRTC-Token zu registrieren"
},
{
+ "id": "api.websocket.invalid_session.error",
+ "translation": "Invalid session err=%v"
+ },
+ {
"id": "api.websocket_handler.invalid_param.app_error",
"translation": "Ungültiger {{.Name}} Parameter"
},
{
+ "id": "app.channel.post_update_channel_purpose_message.post.error",
+ "translation": "Failed to post channel purpose message"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.removed",
+ "translation": "Der Kanaltitel %s wurde entfernt (vorher: %s)"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error",
+ "translation": "Fehler beim Abrufen des Nutzers während des Sicherns der Kanalüberschrift %v"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_from",
+ "translation": "%s Der Kanaltitel wurde aktualisiert von %s nach %s"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_to",
+ "translation": "%s der Kanaltitel wurde aktualisiert in %s"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.description",
+ "translation": "Ability to create new teams"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.name",
+ "translation": "Create Teams"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.description",
+ "translation": "Ability to change the roles of a team member"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.name",
+ "translation": "Manage Team Roles"
+ },
+ {
"id": "authentication.permissions.team_invite_user.description",
"translation": "Die Möglichkeit, Benutzer in ein Team einzuladen"
},
diff --git a/i18n/en.json b/i18n/en.json
index 6d52036bf..6cc82bf37 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -301,19 +301,19 @@
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error",
- "translation": "Failed to post displayname update message %v"
+ "translation": "Failed to post displayname update message"
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error",
- "translation": "Failed to retrieve user while trying to save update channel displayname message %v"
+ "translation": "Failed to retrieve user while updating channel DisplayName field"
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from",
"translation": "%s updated the channel display name from: %s to: %s"
},
{
- "id": "api.channel.post_update_channel_header_message_and_forget.join_leave.error",
- "translation": "Failed to post join/leave message %v"
+ "id": "api.channel.post_update_channel_header_message_and_forget.post.error",
+ "translation": "Failed to post update channel header message"
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.removed",
@@ -321,7 +321,7 @@
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error",
- "translation": "Failed to retrieve user while trying to save update channel header message %v"
+ "translation": "Failed to retrieve user while updating channel header"
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.updated_from",
@@ -333,7 +333,7 @@
},
{
"id": "api.channel.post_user_add_remove_message_and_forget.error",
- "translation": "Failed to post join/leave message %v"
+ "translation": "Failed to post join/leave message"
},
{
"id": "api.channel.remove.default.app_error",
@@ -800,12 +800,16 @@
"translation": "Invalid {{.Name}} parameter"
},
{
+ "id": "api.context.invalid_session.error",
+ "translation": "Invalid session err=%v"
+ },
+ {
"id": "api.context.invalid_team_url.debug",
"translation": "Team URL accessed when not valid. Team URL should not be used in API functions or those that are team independent"
},
{
"id": "api.context.invalid_token.error",
- "translation": "Invalid session token=%v, err=%v"
+ "translation": "Invalid session token={{.Token}}, err={{.Error}}"
},
{
"id": "api.context.invalidate_all_caches",
@@ -1468,26 +1472,10 @@
"translation": "You do not have the appropriate permissions"
},
{
- "id": "api.post.handle_post_events_and_forget.channel.error",
- "translation": "Encountered error getting channel, channel_id=%s, err=%v"
- },
- {
"id": "api.post.handle_post_events_and_forget.members.error",
"translation": "Failed to get channel members channel_id=%v err=%v"
},
{
- "id": "api.post.handle_post_events_and_forget.profiles.error",
- "translation": "Failed to retrieve user profiles team_id=%v, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.team.error",
- "translation": "Encountered error getting team, team_id=%s, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.user.error",
- "translation": "Encountered error getting user, user_id=%s, err=%v"
- },
- {
"id": "api.post.handle_webhook_events_and_forget.create_post.error",
"translation": "Failed to create response post, err=%v"
},
@@ -1496,16 +1484,12 @@
"translation": "Event POST failed, err=%s"
},
{
- "id": "api.post.handle_webhook_events_and_forget.getting.error",
- "translation": "Encountered error getting webhooks by team, err=%v"
- },
- {
"id": "api.post.init.debug",
"translation": "Initializing post API routes"
},
{
"id": "api.post.make_direct_channel_visible.get_2_members.error",
- "translation": "Failed to get 2 members for a direct channel channel_id=%v"
+ "translation": "Failed to get 2 members for a direct channel channel_id={{.ChannelId}}"
},
{
"id": "api.post.make_direct_channel_visible.get_members.error",
@@ -1520,20 +1504,16 @@
"translation": "Failed to update direct channel preference user_id=%v other_user_id=%v err=%v"
},
{
- "id": "api.post.notification.here.warn",
- "translation": "Unable to send notification to online users with @here, err=%v"
- },
- {
"id": "api.post.notification.member_profile.warn",
"translation": "Unable to get profile for channel member, user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
- "translation": "Clearing push notification to %v with channel_id %v"
+ "id": "api.post.send_notifications.user_id.debug",
+ "translation": "Post creator not in channel for the post, no notification sent post_id=%v channel_id=%v user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.comment_thread.error",
- "translation": "Failed to retrieve comment thread posts in notifications root_post_id=%v, err=%v"
+ "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
+ "translation": "Clearing push notification to %v with channel_id %v"
},
{
"id": "api.post.send_notifications_and_forget.files.error",
@@ -1581,25 +1561,13 @@
},
{
"id": "api.post.send_notifications_and_forget.push_notification.error",
- "translation": "Failed to send push device_id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.send.error",
- "translation": "Failed to send mention email successfully email=%v err=%v"
+ "translation": "Failed to send push device_id={{.DeviceId}}, err={{.Error}}"
},
{
"id": "api.post.send_notifications_and_forget.sent",
"translation": "{{.Prefix}} {{.Filenames}} sent"
},
{
- "id": "api.post.send_notifications_and_forget.sessions.error",
- "translation": "Failed to retrieve sessions in notifications id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.user_id.error",
- "translation": "Post user_id not returned by GetProfiles user_id=%v"
- },
- {
"id": "api.post.update_mention_count_and_forget.update_error",
"translation": "Failed to update mention count, post_id=%v channel_id=%v err=%v"
},
@@ -1612,10 +1580,18 @@
"translation": "You do not have the appropriate permissions"
},
{
+ "id": "api.post.update_post.permissions_denied.app_error",
+ "translation": "Post edit has been disabled. Please ask your systems administrator for details."
+ },
+ {
"id": "api.post.update_post.permissions_details.app_error",
"translation": "Already deleted id={{.PostId}}"
},
{
+ "id": "api.post.update_post.permissions_time_limit.app_error",
+ "translation": "Post edit is only allowed for {{.timeLimit}} seconds. Please ask your systems administrator for details."
+ },
+ {
"id": "api.post.update_post.system_message.app_error",
"translation": "Unable to update system message"
},
@@ -2273,7 +2249,7 @@
},
{
"id": "api.user.add_direct_channels_and_forget.failed.error",
- "translation": "Failed to add direct channel preferences for user user_id=%s, team_id=%s, err=%v"
+ "translation": "Failed to add direct channel preferences for user user_id={{.UserId}}, team_id={{.TeamId}}, err={{.Error}}"
},
{
"id": "api.user.authorize_oauth_user.bad_response.app_error",
@@ -2525,19 +2501,19 @@
},
{
"id": "api.user.send_email_change_email_and_forget.error",
- "translation": "Failed to send email change notification email successfully err=%v"
+ "translation": "Failed to send email change notification email successfully"
},
{
"id": "api.user.send_email_change_username_and_forget.error",
- "translation": "Failed to send username change notification email successfully err=%v"
+ "translation": "Failed to send username change notification email successfully"
},
{
"id": "api.user.send_email_change_verify_email_and_forget.error",
- "translation": "Failed to send email change verification email successfully err=%v"
+ "translation": "Failed to send email change verification email successfully"
},
{
"id": "api.user.send_password_change_email_and_forget.error",
- "translation": "Failed to send update password email successfully err=%v"
+ "translation": "Failed to send update password email successfully"
},
{
"id": "api.user.send_password_reset.find.app_error",
@@ -2553,15 +2529,15 @@
},
{
"id": "api.user.send_sign_in_change_email_and_forget.error",
- "translation": "Failed to send update password email successfully err=%v"
+ "translation": "Failed to send update password email successfully"
},
{
"id": "api.user.send_verify_email_and_forget.failed.error",
- "translation": "Failed to send verification email successfully err=%v"
+ "translation": "Failed to send verification email successfully"
},
{
"id": "api.user.send_welcome_email_and_forget.failed.error",
- "translation": "Failed to send welcome email successfully err=%v"
+ "translation": "Failed to send welcome email successfully"
},
{
"id": "api.user.update_active.no_deactivate_ldap.app_error",
@@ -2733,7 +2709,7 @@
},
{
"id": "api.webhook.delete_outgoing.permissions.app_error",
- "translation": "Invalid permissions to delete outcoming webhook"
+ "translation": "Invalid permissions to delete outgoing webhook"
},
{
"id": "api.webhook.get_incoming.disabled.app_error",
@@ -2761,7 +2737,7 @@
},
{
"id": "api.webhook.regen_outgoing_token.permissions.app_error",
- "translation": "Invalid permissions to regenerate outcoming webhook token"
+ "translation": "Invalid permissions to regenerate outgoing webhook token"
},
{
"id": "api.webrtc.disabled.app_error",
@@ -2776,10 +2752,50 @@
"translation": "We encountered an error trying to register the WebRTC Token"
},
{
+ "id": "api.websocket.invalid_session.error",
+ "translation": "Invalid session err=%v"
+ },
+ {
"id": "api.websocket_handler.invalid_param.app_error",
"translation": "Invalid {{.Name}} parameter"
},
{
+ "id": "app.channel.post_update_channel_purpose_message.post.error",
+ "translation": "Failed to post channel purpose message"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.removed",
+ "translation": "%s removed the channel purpose (was: %s)"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error",
+ "translation": "Failed to retrieve user while updating channel purpose message %v"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_from",
+ "translation": "%s updated the channel purpose from: %s to: %s"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_to",
+ "translation": "%s updated the channel purpose to: %s"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.description",
+ "translation": "Ability to create new teams"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.name",
+ "translation": "Create Teams"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.description",
+ "translation": "Ability to change the roles of a team member"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.name",
+ "translation": "Manage Team Roles"
+ },
+ {
"id": "authentication.permissions.team_invite_user.description",
"translation": "Ability to invite users to a team"
},
@@ -4300,6 +4316,14 @@
"translation": "No channels were found"
},
{
+ "id": "store.sql_channel.get_deleted_by_name.existing.app_error",
+ "translation": "We couldn't find the existing deleted channel"
+ },
+ {
+ "id": "store.sql_channel.get_deleted_by_name.missing.app_error",
+ "translation": "No deleted channel exists with that name"
+ },
+ {
"id": "store.sql_channel.get_extra_members.app_error",
"translation": "We couldn't get the extra info for channel members"
},
diff --git a/i18n/es.json b/i18n/es.json
index 436788d4f..4944b41c3 100644
--- a/i18n/es.json
+++ b/i18n/es.json
@@ -301,19 +301,19 @@
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error",
- "translation": "No se pudo publicar el mensaje de actualización del nombre del canal %v"
+ "translation": "No se pudo publicar el mensaje de actualización del nombre del canal"
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error",
- "translation": "No se pudo recuperar de usuario al intentar guardar la actualización del nombre del canal %v"
+ "translation": "No se pudo recuperar el usuario al actualizar el campo DisplayName del canal"
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from",
"translation": "%s actualizado el nombre del canal de: %s a: %s"
},
{
- "id": "api.channel.post_update_channel_header_message_and_forget.join_leave.error",
- "translation": "Fallo al publicar el mensaje de unir/abandonar %v"
+ "id": "api.channel.post_update_channel_header_message_and_forget.post.error",
+ "translation": "Falla al publicar el mensaje de actualización del encabezado del canal"
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.removed",
@@ -321,7 +321,7 @@
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error",
- "translation": "No se pudo recuperar de usuario al intentar guardar la actualización del encabezado del canal %v"
+ "translation": "No se pudo recuperar el usuario al actualizar el mensaje del encabezado del canal %v"
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.updated_from",
@@ -333,7 +333,7 @@
},
{
"id": "api.channel.post_user_add_remove_message_and_forget.error",
- "translation": "Fallo al publicar el mensaje de unir/abandonar %v"
+ "translation": "Fallo al publicar el mensaje de unir/abandonar"
},
{
"id": "api.channel.remove.default.app_error",
@@ -385,7 +385,7 @@
},
{
"id": "api.command.disabled.app_error",
- "translation": "Los comandos han sido deshabilitados por el administrador de sistema."
+ "translation": "Los comandos han sido inhabilitados por el administrador de sistema."
},
{
"id": "api.command.duplicate_trigger.app_error",
@@ -800,12 +800,16 @@
"translation": "Parámetro {{.Name}} inválido"
},
{
+ "id": "api.context.invalid_session.error",
+ "translation": "Sesión no válida err=%v"
+ },
+ {
"id": "api.context.invalid_team_url.debug",
"translation": "El URL de Equipo fue accesado de forma inválida. El URL del Equipo no debe ser utilizado en funciones del API o aquellas funciones que son independientes del equipo"
},
{
"id": "api.context.invalid_token.error",
- "translation": "Token=%v de sesión inválido, err=%v"
+ "translation": "Token {{.Token}} de sesión no válido, err={{.Error}}"
},
{
"id": "api.context.invalidate_all_caches",
@@ -853,7 +857,7 @@
},
{
"id": "api.email_batching.add_notification_email_to_batch.disabled.app_error",
- "translation": "Los correos electrónicos por lotes han sido deshabilitados por el administrador de sistema"
+ "translation": "Los correos electrónicos por lotes han sido inhabilitados por el administrador de sistema"
},
{
"id": "api.email_batching.check_pending_emails.finished_running",
@@ -939,7 +943,7 @@
},
{
"id": "api.emoji.disabled.app_error",
- "translation": "Los emoticones personalizados han sido deshabilitados por el administrador del sistema."
+ "translation": "Los emoticones personalizados han sido inhabilitados por el administrador del sistema."
},
{
"id": "api.emoji.get_image.decode.app_error",
@@ -979,7 +983,7 @@
},
{
"id": "api.file.get_file.public_disabled.app_error",
- "translation": "Los enlaces públicos han sido deshabilitados por un administrador del sistema"
+ "translation": "Los enlaces públicos han sido inhabilitados por un administrador del sistema"
},
{
"id": "api.file.get_file.public_invalid.app_error",
@@ -1011,7 +1015,7 @@
},
{
"id": "api.file.get_public_link.disabled.app_error",
- "translation": "Los enlaces públicos han sido deshabilitados"
+ "translation": "Los enlaces públicos han sido inhabilitados"
},
{
"id": "api.file.get_public_link.no_post.app_error",
@@ -1367,7 +1371,7 @@
},
{
"id": "api.oauth.singup_with_oauth.disabled.app_error",
- "translation": "El registro de usuario está deshabilitado."
+ "translation": "El registro de usuario está inhabilitado."
},
{
"id": "api.oauth.singup_with_oauth.expired_link.app_error",
@@ -1468,26 +1472,10 @@
"translation": "No tienes los permisos apropiados"
},
{
- "id": "api.post.handle_post_events_and_forget.channel.error",
- "translation": "Se encontró un error obteniendo el canal, channel_id=%s, err=%v"
- },
- {
"id": "api.post.handle_post_events_and_forget.members.error",
"translation": "Falla al obtener los miembros del canal channel_id=%v err=%v"
},
{
- "id": "api.post.handle_post_events_and_forget.profiles.error",
- "translation": "Falla al recuperar los perfiles de usuario team_id=%v, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.team.error",
- "translation": "Se encontró un error obteniendo el equipo, team_id=%s, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.user.error",
- "translation": "Se encontró un error obteniendo el usuario, user_id=%s, err=%v"
- },
- {
"id": "api.post.handle_webhook_events_and_forget.create_post.error",
"translation": "Falla al crear la respuesta del mensaje, err=%v"
},
@@ -1496,16 +1484,12 @@
"translation": "Fallo el Evento POST, err=%s"
},
{
- "id": "api.post.handle_webhook_events_and_forget.getting.error",
- "translation": "Se encontró un error obteniendo los webhooks por equipo, err=%v"
- },
- {
"id": "api.post.init.debug",
"translation": "Inicializando rutas del API para los mensajes"
},
{
"id": "api.post.make_direct_channel_visible.get_2_members.error",
- "translation": "Falla al obtener 2 miembros para un canal directo channel_id=%v"
+ "translation": "Falla al obtener 2 miembros para un canal directo channel_id={{.ChannelId}}"
},
{
"id": "api.post.make_direct_channel_visible.get_members.error",
@@ -1520,20 +1504,16 @@
"translation": "Falla al actualizar las preferencias del canal directo user_id=%v other_user_id=%v err=%v"
},
{
- "id": "api.post.notification.here.warn",
- "translation": "No se pudo enviar notificaciones a los usuarios en línea con @here, err=%v"
- },
- {
"id": "api.post.notification.member_profile.warn",
"translation": "No se puede obtener el perfil del miembro del canal, user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
- "translation": "Limpiando notificaciones push a %v con el channel_id '%v'"
+ "id": "api.post.send_notifications.user_id.debug",
+ "translation": "El creador del mensaje no es parte del canal donde se publicó el mensaje, no se envió la notificación post_id=%v channel_id=%v user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.comment_thread.error",
- "translation": "No se pudo recuperar el hilo de comentarios en las notificaciones root_post_id=%v, err=%v"
+ "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
+ "translation": "Limpiando notificaciones push a %v con el channel_id '%v'"
},
{
"id": "api.post.send_notifications_and_forget.files.error",
@@ -1581,25 +1561,13 @@
},
{
"id": "api.post.send_notifications_and_forget.push_notification.error",
- "translation": "Falló el envio de la notificación push al device_id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.send.error",
- "translation": "Falla al enviar el correo con la mención satisfactoriamente email=%v err=%v"
+ "translation": "Error al enviar push device_id={{.DeviceId}}, err={{.Error}}"
},
{
"id": "api.post.send_notifications_and_forget.sent",
"translation": "{{.Prefix}} {{.Filenames}} enviado"
},
{
- "id": "api.post.send_notifications_and_forget.sessions.error",
- "translation": "Falla al recuperar las sesiones en las notificaciones id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.user_id.error",
- "translation": "El user_id del Mensaje no fue retornado por GetProfiles user_id=%v"
- },
- {
"id": "api.post.update_mention_count_and_forget.update_error",
"translation": "Falla al actualizar el contador de mensiones para post_id=%v channel_id=%v err=%v"
},
@@ -1612,10 +1580,18 @@
"translation": "No tienes los permisos apropiados"
},
{
+ "id": "api.post.update_post.permissions_denied.app_error",
+ "translation": "La edición de mensajes ha sido inhabilitada. Por favor pregunta a tu administrador de sistema por detalles."
+ },
+ {
"id": "api.post.update_post.permissions_details.app_error",
"translation": "Ya fué elminado el id={{.PostId}}"
},
{
+ "id": "api.post.update_post.permissions_time_limit.app_error",
+ "translation": "La edición del mensaje sólo está permitido por {{.timeLimit}} segundos. Por favor, pregunta a tu administrador de sistemas para obtener más detalles."
+ },
+ {
"id": "api.post.update_post.system_message.app_error",
"translation": "No se puede actualizar el mensaje del sistema"
},
@@ -1685,7 +1661,7 @@
},
{
"id": "api.server.start_server.rate.warn",
- "translation": "La configuración del límite de velocidad no ha sido configurado apropiadamente utilizando VaryByHeader y deshabilitado VaryByRemoteAddr"
+ "translation": "La configuración del límite de velocidad no ha sido configurado apropiadamente utilizando VaryByHeader y inhabilitado VaryByRemoteAddr"
},
{
"id": "api.server.start_server.rate_limiting_memory_store",
@@ -1901,11 +1877,11 @@
},
{
"id": "api.team.create_team.email_disabled.app_error",
- "translation": "El registro a equipos por correo electrónico está deshabilitado."
+ "translation": "El registro a equipos por correo electrónico está inhabilitado."
},
{
"id": "api.team.create_team_from_signup.email_disabled.app_error",
- "translation": "El registro a equipos por correo electrónico está deshabilitado."
+ "translation": "El registro a equipos por correo electrónico está inhabilitado."
},
{
"id": "api.team.create_team_from_signup.expired_link.app_error",
@@ -1993,7 +1969,7 @@
},
{
"id": "api.team.is_team_creation_allowed.disabled.app_error",
- "translation": "La creación de Equipos ha sido deshabilitada. Por favor pregunta a tu administrador de sistema por detalles."
+ "translation": "La creación de Equipos ha sido inhabilitada. Por favor pregunta a tu administrador de sistema por detalles."
},
{
"id": "api.team.is_team_creation_allowed.domain.app_error",
@@ -2013,7 +1989,7 @@
},
{
"id": "api.team.signup_team.email_disabled.app_error",
- "translation": "El registro a equipos por correo electrónico está deshabilitado."
+ "translation": "El registro a equipos por correo electrónico está inhabilitado."
},
{
"id": "api.team.update_member_roles.not_a_member",
@@ -2273,7 +2249,7 @@
},
{
"id": "api.user.add_direct_channels_and_forget.failed.error",
- "translation": "Falla al agregar las preferencias del canal directo para el usuario user_id=%s, team_id=%s, err=%v"
+ "translation": "Error al agregar un canal directo a las preferencias del usuario user_id={{.UserId}}, team_id={{.TeamId}}, err={{.Error}}"
},
{
"id": "api.user.authorize_oauth_user.bad_response.app_error",
@@ -2381,7 +2357,7 @@
},
{
"id": "api.user.create_user.signup_email_disabled.app_error",
- "translation": "Registro de usuarios por correo electrónico está deshabilitado."
+ "translation": "Registro de usuarios por correo electrónico está inhabilitado."
},
{
"id": "api.user.create_user.signup_link_expired.app_error",
@@ -2525,19 +2501,19 @@
},
{
"id": "api.user.send_email_change_email_and_forget.error",
- "translation": "Falla al enviar la notificación por correo electrónico del cambio de correo err=%v"
+ "translation": "Error al enviar la notificación por correo electrónico del cambio satisfactorio del correo electrónico"
},
{
"id": "api.user.send_email_change_username_and_forget.error",
- "translation": "Falla al enviar la notificación por correo electrónico del cambio de nombre de usuario err=%v"
+ "translation": "Error al enviar la notificación por correo electrónico del cambio satisfactorio del nombre de usuario"
},
{
"id": "api.user.send_email_change_verify_email_and_forget.error",
- "translation": "Error al enviar correo electrónico de verificación de cambio de correo electrónico con éxito err=%v"
+ "translation": "Error al enviar la notificación por correo electrónico para verificar el cambio satisfactorio del correo electrónico"
},
{
"id": "api.user.send_password_change_email_and_forget.error",
- "translation": "Falla al enviar el correo del cambio de contraseña satisfactorio err=%v"
+ "translation": "Error al enviar la notificación por correo electrónico del cambio satisfactorio de contraseña"
},
{
"id": "api.user.send_password_reset.find.app_error",
@@ -2553,15 +2529,15 @@
},
{
"id": "api.user.send_sign_in_change_email_and_forget.error",
- "translation": "Falla al enviar el correo del cambio de contraseña satisfactorio err=%v"
+ "translation": "Error al enviar la notificación por correo del cambio satisfactorio de contraseña"
},
{
"id": "api.user.send_verify_email_and_forget.failed.error",
- "translation": "Falla al enviar el correo con la verificación éxitosa del correo electrónico err=%v"
+ "translation": "Error al enviar la notificación por correo electrónico de la verificación del correo electrónico"
},
{
"id": "api.user.send_welcome_email_and_forget.failed.error",
- "translation": "Falla al enviar el correo de bienvenida err=%v"
+ "translation": "Error al enviar la notificación de bienvenida por correo electrónico"
},
{
"id": "api.user.update_active.no_deactivate_ldap.app_error",
@@ -2697,11 +2673,11 @@
},
{
"id": "api.webhook.create_incoming.disabled.app_errror",
- "translation": "Webhooks entrantes han sido deshabilitados por el administrador del sistema."
+ "translation": "Webhooks entrantes han sido inhabilitados por el administrador del sistema."
},
{
"id": "api.webhook.create_outgoing.disabled.app_error",
- "translation": "Webhooks de Salida han sido deshabilitados por el administrador del sistema."
+ "translation": "Webhooks de Salida han sido inhabilitados por el administrador del sistema."
},
{
"id": "api.webhook.create_outgoing.intersect.app_error",
@@ -2721,7 +2697,7 @@
},
{
"id": "api.webhook.delete_incoming.disabled.app_errror",
- "translation": "Webhooks entrantes han sido deshabilitados por el administrador del sistema."
+ "translation": "Webhooks entrantes han sido inhabilitados por el administrador del sistema."
},
{
"id": "api.webhook.delete_incoming.permissions.app_errror",
@@ -2729,7 +2705,7 @@
},
{
"id": "api.webhook.delete_outgoing.disabled.app_error",
- "translation": "Webhooks de Salida han sido deshabilitados por el administrador del sistema."
+ "translation": "Webhooks de Salida han sido inhabilitados por el administrador del sistema."
},
{
"id": "api.webhook.delete_outgoing.permissions.app_error",
@@ -2737,11 +2713,11 @@
},
{
"id": "api.webhook.get_incoming.disabled.app_error",
- "translation": "Webhooks entrantes han sido deshabilitados por el administrador del sistema."
+ "translation": "Webhooks entrantes han sido inhabilitados por el administrador del sistema."
},
{
"id": "api.webhook.get_outgoing.disabled.app_error",
- "translation": "Webhooks de Salida han sido deshabilitados por el administrador del sistema."
+ "translation": "Webhooks de Salida han sido inhabilitados por el administrador del sistema."
},
{
"id": "api.webhook.incoming.debug",
@@ -2757,7 +2733,7 @@
},
{
"id": "api.webhook.regen_outgoing_token.disabled.app_error",
- "translation": "Webhooks de Salida han sido deshabilitados por el administrador del sistema."
+ "translation": "Webhooks de Salida han sido inhabilitados por el administrador del sistema."
},
{
"id": "api.webhook.regen_outgoing_token.permissions.app_error",
@@ -2776,10 +2752,50 @@
"translation": "Hemos encontrado un error al intentar registrar el Token de WebRTC"
},
{
+ "id": "api.websocket.invalid_session.error",
+ "translation": "Sesión no válida err=%v"
+ },
+ {
"id": "api.websocket_handler.invalid_param.app_error",
"translation": "Parámetro {{.Name}} inválido"
},
{
+ "id": "app.channel.post_update_channel_purpose_message.post.error",
+ "translation": "No pudo publicar el mensaje del propósito del canal"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.removed",
+ "translation": "%s removió el propósito del canal (era: %s)"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error",
+ "translation": "No se pudo recuperar el usuario al actualizar el mensaje del propósito del canal %v"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_from",
+ "translation": "%s actualizó el propósito del canal de: %s a: %s"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_to",
+ "translation": "%s actualizó el propósito del canal a: %s"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.description",
+ "translation": "Ability to create new teams"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.name",
+ "translation": "Create Teams"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.description",
+ "translation": "Ability to change the roles of a team member"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.name",
+ "translation": "Manage Team Roles"
+ },
+ {
"id": "authentication.permissions.team_invite_user.description",
"translation": "Capacidad para invitar usuarios a un equipo"
},
@@ -2845,7 +2861,7 @@
},
{
"id": "ent.cluster.licence_disable.app_error",
- "translation": "La funcionalidad de Agrupamiento de Servidores está deshabilitada por la licencia actual. Por favor, póngase en contacto con su administrador del sistema acerca de la actualización de su licencia empresarial."
+ "translation": "La funcionalidad de Agrupamiento de Servidores está inhabilitada por la licencia actual. Por favor, póngase en contacto con su administrador del sistema acerca de la actualización de su licencia empresarial."
},
{
"id": "ent.cluster.ping_failed.info",
@@ -2869,7 +2885,7 @@
},
{
"id": "ent.compliance.licence_disable.app_error",
- "translation": "La característica de Cumplimiento está deshabilitada para tu licencia actual. Por favor contacta a un administrador del sistema sobre como actualizar a una licencia empresarial."
+ "translation": "La característica de Cumplimiento está inhabilitada para tu licencia actual. Por favor contacta a un administrador del sistema sobre como actualizar a una licencia empresarial."
},
{
"id": "ent.compliance.run_failed.error",
@@ -2897,7 +2913,7 @@
},
{
"id": "ent.ldap.disabled.app_error",
- "translation": "AD/LDAP deshabilitado o la licencia no es compatible con AD/LDAP."
+ "translation": "AD/LDAP inhabilitado o la licencia no es compatible con AD/LDAP."
},
{
"id": "ent.ldap.do_login.bind_admin_user.app_error",
@@ -2909,7 +2925,7 @@
},
{
"id": "ent.ldap.do_login.licence_disable.app_error",
- "translation": "Las funcionalidades de AD/LDAP están deshabilitadas con la licencia actual. Por favor contacta a un administrador del sistema acerca de mejorar la licencia empresarial."
+ "translation": "La funcionalidad de AD/LDAP está inhabilitada con la licencia actual. Por favor contacta a un administrador del sistema acerca de mejorar la licencia empresarial."
},
{
"id": "ent.ldap.do_login.matched_to_many_users.app_error",
@@ -4181,7 +4197,7 @@
},
{
"id": "store.sql.read_replicas_not_licensed.critical",
- "translation": "La funcionalidad de más de 1 replica ha sido deshabilitada debido a la licencia actual. Por favor contacta a un administrador del sistema para realizar una actualización de la licencia empresarial."
+ "translation": "La funcionalidad de más de 1 replica ha sido inhabilitada debido a la licencia actual. Por favor contacta a un administrador del sistema para realizar una actualización de la licencia empresarial."
},
{
"id": "store.sql.remove_index.critical",
@@ -5461,7 +5477,7 @@
},
{
"id": "web.incoming_webhook.disabled.app_error",
- "translation": "Webhooks entrantes han sido deshabilitados por el administrador del sistema."
+ "translation": "Webhooks entrantes han sido inhabilitados por el administrador del sistema."
},
{
"id": "web.incoming_webhook.invalid.app_error",
diff --git a/i18n/fr.json b/i18n/fr.json
index e4b73ca9f..a0c4fe83b 100644
--- a/i18n/fr.json
+++ b/i18n/fr.json
@@ -73,7 +73,7 @@
},
{
"id": "api.admin.init.debug",
- "translation": "Initialisation du routage des API d'administration"
+ "translation": "Initialisation des routes de l'API administration"
},
{
"id": "api.admin.recycle_db_end.warn",
@@ -269,7 +269,7 @@
},
{
"id": "api.channel.get_channels.error",
- "translation": "Erreur lors de la récupération du profil pour id=%v déconnexion forcée"
+ "translation": "Erreur lors de la récupération de profil pour id=%v déconnexion forcée"
},
{
"id": "api.channel.init.debug",
@@ -312,8 +312,8 @@
"translation": "%s a mis à jour l'en-tête du canal de : %s en : %s"
},
{
- "id": "api.channel.post_update_channel_header_message_and_forget.join_leave.error",
- "translation": "Erreur lors de l'envoi du message joindre/quitter %v"
+ "id": "api.channel.post_update_channel_header_message_and_forget.post.error",
+ "translation": "Failed to post update channel header message"
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.removed",
@@ -333,7 +333,7 @@
},
{
"id": "api.channel.post_user_add_remove_message_and_forget.error",
- "translation": "Impossible d'envoyer le message joindre/quitter %v"
+ "translation": "Erreur lors de l'envoi du message joindre/quitter %v"
},
{
"id": "api.channel.remove.default.app_error",
@@ -800,12 +800,16 @@
"translation": "Paramètre {{.Name}} invalide"
},
{
+ "id": "api.context.invalid_session.error",
+ "translation": "Invalid session err=%v"
+ },
+ {
"id": "api.context.invalid_team_url.debug",
"translation": "URL d'équipe utilisée si l'appel est invalide. L'URL d'équipe ne devrait pas être utilisée dans des fonctions d'API ou des fonctions indépendantes d'une équipe"
},
{
"id": "api.context.invalid_token.error",
- "translation": "Session invalide token=%v, err=%v"
+ "translation": "Invalid session token={{.Token}}, err={{.Error}}"
},
{
"id": "api.context.invalidate_all_caches",
@@ -873,7 +877,7 @@
},
{
"id": "api.email_batching.render_batched_post.direct_message",
- "translation": "Nouveau Message Privé"
+ "translation": "Message privé"
},
{
"id": "api.email_batching.render_batched_post.go_to_post",
@@ -886,8 +890,8 @@
{
"id": "api.email_batching.send_batched_email_notification.body_text",
"translation": {
- "one": "Vous avez un nouveau message.Vous avez {{.Count}} nouveaux messages.",
- "other": ""
+ "one": "Vous avez un nouveau message.",
+ "other": "Vous avez {{.Count}} nouveaux messages."
}
},
{
@@ -1419,7 +1423,7 @@
},
{
"id": "api.post.delete_flagged_post.app_error.warn",
- "translation": "Impossible de supprimer les préférences du message avec indicateur lorsque celui-ci est en train d'être supprimé, err=%v"
+ "translation": "Impossible de supprimer les préférences de marquage de message lorsque celui-ci est en train d'être supprimé, err=%v"
},
{
"id": "api.post.delete_post.permissions.app_error",
@@ -1468,26 +1472,10 @@
"translation": "Vous n'avez pas les permissions appropriées"
},
{
- "id": "api.post.handle_post_events_and_forget.channel.error",
- "translation": "Erreur rencontrée lors de la récupération du canal, channel_id=%s, err=%v"
- },
- {
"id": "api.post.handle_post_events_and_forget.members.error",
"translation": "Impossible de récupérer les membres du canal channel_id=%v err=%v"
},
{
- "id": "api.post.handle_post_events_and_forget.profiles.error",
- "translation": "Impossible de récupérer les profils utilisateurs team_id=%v, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.team.error",
- "translation": "Erreur rencontrée lors de la récupération de l'équipe, team_id=%s, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.user.error",
- "translation": "Erreur rencontrée lors de la récupération de l'utilisateur, user_id=%s, err=%v"
- },
- {
"id": "api.post.handle_webhook_events_and_forget.create_post.error",
"translation": "Impossible de créer le message de réponse, err=%v"
},
@@ -1496,10 +1484,6 @@
"translation": "Échec de l'événement POST, err=%s"
},
{
- "id": "api.post.handle_webhook_events_and_forget.getting.error",
- "translation": "Erreur rencontrée lors de la récupération des webhooks par l'équipe, err=%v"
- },
- {
"id": "api.post.init.debug",
"translation": "Initialisation des routes de l'API des messages"
},
@@ -1520,20 +1504,16 @@
"translation": "Échec de la mise à jour des préférences du canal direct user_id=%v other_user_id=%v err=%v"
},
{
- "id": "api.post.notification.here.warn",
- "translation": "Impossible d'envoyer une notification aux utilisateurs connectés avec @here, err=%v"
- },
- {
"id": "api.post.notification.member_profile.warn",
"translation": "Impossible d'obtenir le profil pour le membre du canal, user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
- "translation": "Suppression de la notification push pour %v avec channel_id %v"
+ "id": "api.post.send_notifications.user_id.debug",
+ "translation": "Post creator not in channel for the post, no notification sent post_id=%v channel_id=%v user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.comment_thread.error",
- "translation": "Impossible de récupérer les fils de commentaires dans les notifications root_post_id=%v, err=%v"
+ "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
+ "translation": "Suppression de la notification push pour %v avec channel_id %v"
},
{
"id": "api.post.send_notifications_and_forget.files.error",
@@ -1557,7 +1537,7 @@
},
{
"id": "api.post.send_notifications_and_forget.message_subject",
- "translation": "Nouveau Message Privé"
+ "translation": "Nouveau message privé"
},
{
"id": "api.post.send_notifications_and_forget.push_in",
@@ -1581,25 +1561,13 @@
},
{
"id": "api.post.send_notifications_and_forget.push_notification.error",
- "translation": "Echec lors de l'envoi de la notificationid=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.send.error",
- "translation": "Impossible d'envoyer l'e-mail de mention email=%v err=%v"
+ "translation": "Failed to send push device_id={{.DeviceId}}, err={{.Error}}"
},
{
"id": "api.post.send_notifications_and_forget.sent",
"translation": "{{.Prefix}} {{.Filenames}} envoyé"
},
{
- "id": "api.post.send_notifications_and_forget.sessions.error",
- "translation": "Impossible de récupérer les sessions dans les notifications id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.user_id.error",
- "translation": "Message user_id non renvoyé par GetProfiles user_id=%v"
- },
- {
"id": "api.post.update_mention_count_and_forget.update_error",
"translation": "Impossible de mettre à jour le compteur de mentions pour l'utilisateur user_id=%v sur channel_id=%v err=%v"
},
@@ -1612,10 +1580,18 @@
"translation": "Vous n'avez pas les permissions appropriées"
},
{
+ "id": "api.post.update_post.permissions_denied.app_error",
+ "translation": "La création d'équipes est désactivée. Veuillez demander les détails à votre administrateur système."
+ },
+ {
"id": "api.post.update_post.permissions_details.app_error",
"translation": "Déjà supprimé id={{.PostId}}"
},
{
+ "id": "api.post.update_post.permissions_time_limit.app_error",
+ "translation": "Post edit is only allowed for {{.timeLimit}} seconds. Please ask your systems administrator for details."
+ },
+ {
"id": "api.post.update_post.system_message.app_error",
"translation": "Impossible de mettre à jour un message système"
},
@@ -1977,11 +1953,11 @@
},
{
"id": "api.team.invite_members.restricted_system_admin.app_error",
- "translation": "Seuls les administrateurs système peuvent inviter de nouveaux utilisateurs."
+ "translation": "Seuls les administrateurs système peuvent inviter des nouveaux utilisateurs."
},
{
"id": "api.team.invite_members.restricted_team_admin.app_error",
- "translation": "Seuls les administrateurs d'équipe et administrateurs système peuvent inviter de nouveaux utilisateurs."
+ "translation": "Seuls les administrateurs d'équipe et les administrateurs système peuvent inviter des nouveaux utilisateurs."
},
{
"id": "api.team.invite_members.send.error",
@@ -2273,7 +2249,7 @@
},
{
"id": "api.user.add_direct_channels_and_forget.failed.error",
- "translation": "Impossible d'ajouter les préférences pour l'utilisateur user_id=%s, team_id=%s, err=%v"
+ "translation": "Failed to add direct channel preferences for user user_id={{.UserId}}, team_id={{.TeamId}}, err={{.Error}}"
},
{
"id": "api.user.authorize_oauth_user.bad_response.app_error",
@@ -2357,7 +2333,7 @@
},
{
"id": "api.user.create_profile_image.encode.app_error",
- "translation": "Impossible d'encoder la photo du profil"
+ "translation": "Impossible d'encoder la photo de profil"
},
{
"id": "api.user.create_profile_image.initial.app_error",
@@ -2449,7 +2425,7 @@
},
{
"id": "api.user.login.not_verified.app_error",
- "translation": "Connexion impossible : l'adresse électronique n'a pas été vérifiée"
+ "translation": "Connexion impossible : l'adresse e-mail n'a pas été vérifiée"
},
{
"id": "api.user.login.revoking.app_error",
@@ -2661,7 +2637,7 @@
},
{
"id": "api.user.verify_email.bad_link.app_error",
- "translation": "Mauvais lien de vérification de l'adresse électronique."
+ "translation": "Mauvais lien de vérification de l'adresse e-mail."
},
{
"id": "api.web_hub.start.starting.debug",
@@ -2776,10 +2752,50 @@
"translation": "Une erreur s'est produite lors de l'enregistrement du jeton WebRTC"
},
{
+ "id": "api.websocket.invalid_session.error",
+ "translation": "Invalid session err=%v"
+ },
+ {
"id": "api.websocket_handler.invalid_param.app_error",
"translation": "Paramètre {{.Name}} invalide"
},
{
+ "id": "app.channel.post_update_channel_purpose_message.post.error",
+ "translation": "Failed to post channel purpose message"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.removed",
+ "translation": "%s a supprimé le titre du canal (était : %s)"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error",
+ "translation": "Impossible de récupérer l'utilisateur lors de l'enregistrement d'un nouveau message de canal %v"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_from",
+ "translation": "%s a mis à jour l'en-tête du canal de : %s en : %s"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_to",
+ "translation": "%s a mis à jour l'en-tête du canal en : %s"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.description",
+ "translation": "Ability to create new teams"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.name",
+ "translation": "Create Teams"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.description",
+ "translation": "Ability to change the roles of a team member"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.name",
+ "translation": "Manage Team Roles"
+ },
+ {
"id": "authentication.permissions.team_invite_user.description",
"translation": "Possibilité d'inviter des utilisateurs dans une équipe"
},
@@ -3161,11 +3177,11 @@
},
{
"id": "mattermost.entreprise_enabled",
- "translation": "Entreprise Activé : %v"
+ "translation": "Edition Entreprise Activé : %v"
},
{
"id": "mattermost.load_license.find.warn",
- "translation": "Un clé de licence de https://mattermost.com est requise pour déverrouiller les fonctionnalités d'entreprises."
+ "translation": "Une clé de licence de https://mattermost.com est requise pour déverrouiller les fonctionnalités d'entreprise."
},
{
"id": "mattermost.security_bulletin.error",
@@ -5121,15 +5137,15 @@
},
{
"id": "store.sql_user.save.email_exists.app_error",
- "translation": "Un compte avec cette adresse électronique existe déjà."
+ "translation": "Un compte avec cette adresse e-mail existe déjà."
},
{
"id": "store.sql_user.save.email_exists.ldap_app_error",
- "translation": "Ce compte n'utilise pas l'authentification LDAP. Veuillez vous connecter avec votre adresse électronique et votre mot de passe."
+ "translation": "Ce compte n'utilise pas l'authentification LDAP. Veuillez vous connecter avec votre adresse e-mail et votre mot de passe."
},
{
"id": "store.sql_user.save.email_exists.saml_app_error",
- "translation": "Ce compte n'utilise pas l'authentification SAML. Veuillez vous connecter avec votre adresse électronique et votre mot de passe."
+ "translation": "Ce compte n'utilise pas l'authentification SAML. Veuillez vous connecter avec votre adresse e-mail et votre mot de passe."
},
{
"id": "store.sql_user.save.existing.app_error",
@@ -5165,7 +5181,7 @@
},
{
"id": "store.sql_user.update.email_taken.app_error",
- "translation": "Cette adresse électronique est déjà prise. Veuillez en choisir une autre."
+ "translation": "Cette adresse e-mail est déjà utilisée. Veuillez en choisir une autre."
},
{
"id": "store.sql_user.update.find.app_error",
@@ -5189,7 +5205,7 @@
},
{
"id": "store.sql_user.update_auth_data.email_exists.app_error",
- "translation": "Impossible de convertir le compte à {{.Service}}. Un compte utilisant cette adresse électronique {{.Email}} existe déjà."
+ "translation": "Impossible de convertir le compte à {{.Service}}. Un compte utilisant cette adresse e-mail {{.Email}} existe déjà."
},
{
"id": "store.sql_user.update_failed_pwd_attempts.app_error",
@@ -5221,7 +5237,7 @@
},
{
"id": "store.sql_user.verify_email.app_error",
- "translation": "Impossible de mettre à jour le champ de vérification de l'adresse électronique"
+ "translation": "Impossible de mettre à jour le champ de vérification de l'adresse e-mail"
},
{
"id": "store.sql_webhooks.analytics_incoming_count.app_error",
@@ -5373,11 +5389,11 @@
},
{
"id": "utils.mail.send_mail.from_address.app_error",
- "translation": "Échec d'ajout de l'adresse électronique de l'expéditeur"
+ "translation": "Échec d'ajout de l'adresse e-mail de l'expéditeur"
},
{
"id": "utils.mail.send_mail.msg.app_error",
- "translation": "Échec d'écriture du message électronique"
+ "translation": "Échec d'écriture de l'e-mail"
},
{
"id": "utils.mail.send_mail.msg_data.app_error",
@@ -5389,7 +5405,7 @@
},
{
"id": "utils.mail.send_mail.to_address.app_error",
- "translation": "Échec de l'ajout de l'adresse électronique"
+ "translation": "Échec de l'ajout de l'adresse e-mail"
},
{
"id": "utils.mail.test.configured.error",
diff --git a/i18n/ja.json b/i18n/ja.json
index 8dee0b47d..bc90aa7b8 100644
--- a/i18n/ja.json
+++ b/i18n/ja.json
@@ -301,19 +301,19 @@
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error",
- "translation": "表示名更新メッセージ %v を投稿できませんでした"
+ "translation": "表示名更新メッセージを投稿できませんでした"
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error",
- "translation": "チャンネル表示名更新メッセージ%vを保存する際にユーザーを取得できませんでした"
+ "translation": "チャンネル表示名を更新する際にユーザーを取得できませんでした"
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from",
"translation": "%sがチャンネル表示名を %s から %s に更新しました"
},
{
- "id": "api.channel.post_update_channel_header_message_and_forget.join_leave.error",
- "translation": "参加/脱退のメッセージを投稿できませんでした %v"
+ "id": "api.channel.post_update_channel_header_message_and_forget.post.error",
+ "translation": "チャンネルヘッダー更新メッセージを投稿できませんでした"
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.removed",
@@ -321,7 +321,7 @@
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error",
- "translation": "チャンネルのヘッダー更新メッセージ%vを保存する際にユーザーを取得できませんでした"
+ "translation": "チャンネルのヘッダーメッセージ%vを更新する際にユーザーを取得できませんでした"
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.updated_from",
@@ -333,7 +333,7 @@
},
{
"id": "api.channel.post_user_add_remove_message_and_forget.error",
- "translation": "参加/脱退のメッセージ%vを投稿できませんでした"
+ "translation": "参加/脱退のメッセージを投稿できませんでした"
},
{
"id": "api.channel.remove.default.app_error",
@@ -800,12 +800,16 @@
"translation": "不正な{{.Name}}パラメーターです"
},
{
+ "id": "api.context.invalid_session.error",
+ "translation": "無効なセッション err=%v"
+ },
+ {
"id": "api.context.invalid_team_url.debug",
"translation": "チームURLが不正なタイミングでアクセスされました。チームURLはAPI関数内またはチームと関係のない文脈で使用することはできません"
},
{
"id": "api.context.invalid_token.error",
- "translation": "不正なセッショントークンです token=%v、err=%v"
+ "translation": "無効なセッショントークン={{.Token}}, err={{.Error}}"
},
{
"id": "api.context.invalidate_all_caches",
@@ -1468,26 +1472,10 @@
"translation": "あなたには必要な権限が付与されていません"
},
{
- "id": "api.post.handle_post_events_and_forget.channel.error",
- "translation": "チャンネルを取得できませんでした channel_id=%s, err=%v"
- },
- {
"id": "api.post.handle_post_events_and_forget.members.error",
"translation": "チャンネルの参加者が取得できませんでした channel_id=%v err=%v"
},
{
- "id": "api.post.handle_post_events_and_forget.profiles.error",
- "translation": "ユーザーのプロフィールを取得できませんでした team_id=%v, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.team.error",
- "translation": "チームが取得できませんでした team_id=%s, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.user.error",
- "translation": "ユーザーが取得できませんでした user_id=%s, err=%v"
- },
- {
"id": "api.post.handle_webhook_events_and_forget.create_post.error",
"translation": "返信の投稿を作成できませんでした err=%v"
},
@@ -1496,16 +1484,12 @@
"translation": "POSTイベントが失敗しました err=%s"
},
{
- "id": "api.post.handle_webhook_events_and_forget.getting.error",
- "translation": "チームによるウェブフックに失敗しました err=%v"
- },
- {
"id": "api.post.init.debug",
"translation": "投稿APIルートを初期化しています"
},
{
"id": "api.post.make_direct_channel_visible.get_2_members.error",
- "translation": "ダイレクトチャンネルの2人のメンバーを取得できませんでした channel_id=%v"
+ "translation": "ダイレクトチャンネルの2人のメンバーを取得できませんでした channel_id={{.ChannelId}}"
},
{
"id": "api.post.make_direct_channel_visible.get_members.error",
@@ -1520,20 +1504,16 @@
"translation": "ダイレクトチャンネルの設定を更新できませんでした user_id=%v other_user_id=%v err=%v"
},
{
- "id": "api.post.notification.here.warn",
- "translation": "@here を使って、オンラインのユーザーへ通知を送ることができませんでした。err=%v"
- },
- {
"id": "api.post.notification.member_profile.warn",
"translation": "チャンネルメンバーのプロフィールを取得できませんでした user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
- "translation": "%vへのプッシュ通知をクリアーしています(channel_id %v)"
+ "id": "api.post.send_notifications.user_id.debug",
+ "translation": "その投稿の作成者がチャンネルにいないため、通知は送信されませんでした post_id=%v channel_id=%v user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.comment_thread.error",
- "translation": "通知内のコメントスレッドの投稿を取得できませんでした root_post_id=%v、err=%v"
+ "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
+ "translation": "%vへのプッシュ通知をクリアーしています(channel_id %v)"
},
{
"id": "api.post.send_notifications_and_forget.files.error",
@@ -1581,25 +1561,13 @@
},
{
"id": "api.post.send_notifications_and_forget.push_notification.error",
- "translation": "プッシュ通知を送れませんでした device_id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.send.error",
- "translation": "あなたについての投稿を知らせる電子メールの送信できませんでした email=%v err=%v"
+ "translation": "プッシュ通知を送信出来ませんでした device_id={{.DeviceId}}, err={{.Error}}"
},
{
"id": "api.post.send_notifications_and_forget.sent",
"translation": "{{.Prefix}} {{.Filenames}}を送信しました"
},
{
- "id": "api.post.send_notifications_and_forget.sessions.error",
- "translation": "通知でセッションの取得に失敗しました id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.user_id.error",
- "translation": "GetProfilesで見付けられないuser_idを投稿します user_id=%v"
- },
- {
"id": "api.post.update_mention_count_and_forget.update_error",
"translation": "あなたについての投稿数を更新できませんでした post_id=%v on channel_id=%v err=%v"
},
@@ -1612,10 +1580,18 @@
"translation": "あなたには必要な権限が付与されていません"
},
{
+ "id": "api.post.update_post.permissions_denied.app_error",
+ "translation": "投稿を編集する機能は無効になっています。詳細はシステム管理者に問い合わせてください。"
+ },
+ {
"id": "api.post.update_post.permissions_details.app_error",
"translation": "既に削除されたIDです id={{.PostId}}"
},
{
+ "id": "api.post.update_post.permissions_time_limit.app_error",
+ "translation": "投稿を編集する機能は {{.timeLimit}} 秒間のみ有効です。詳細はシステム管理者に問い合わせてください。"
+ },
+ {
"id": "api.post.update_post.system_message.app_error",
"translation": "システムメッセージをアップデートできません"
},
@@ -2273,7 +2249,7 @@
},
{
"id": "api.user.add_direct_channels_and_forget.failed.error",
- "translation": "ユーザーのダイレクトチャンネルの設定に追加できませんでした user_id=%s, team_id=%s, err=%v"
+ "translation": "ユーザーにダイレクトチャンネルの設定を追加できませんでした user_id={{.UserId}}, team_id={{.TeamId}}, err={{.Error}}"
},
{
"id": "api.user.authorize_oauth_user.bad_response.app_error",
@@ -2525,19 +2501,19 @@
},
{
"id": "api.user.send_email_change_email_and_forget.error",
- "translation": "電子メールアドレス変更の確認の電子メールを送信できませんでした err=%v"
+ "translation": "電子メールアドレス変更通知の電子メールを送信できませんでした"
},
{
"id": "api.user.send_email_change_username_and_forget.error",
- "translation": "ユーザー名変更の確認の電子メールを送信しました err=%v"
+ "translation": "ユーザー名変更通知の電子メールを送信できませんでした"
},
{
"id": "api.user.send_email_change_verify_email_and_forget.error",
- "translation": "電子メールアドレス変更確認の電子メールを送信できませんでした err=%v"
+ "translation": "電子メールアドレス変更確認の電子メールを送信できませんでした"
},
{
"id": "api.user.send_password_change_email_and_forget.error",
- "translation": "パスワード更新の電子メールを送信できませんでした err=%v"
+ "translation": "パスワード更新の電子メールを送信できませんでした"
},
{
"id": "api.user.send_password_reset.find.app_error",
@@ -2553,15 +2529,15 @@
},
{
"id": "api.user.send_sign_in_change_email_and_forget.error",
- "translation": "パスワード更新の電子メールを送信できませんでした err=%v"
+ "translation": "パスワード更新の電子メールを送信できませんでした"
},
{
"id": "api.user.send_verify_email_and_forget.failed.error",
- "translation": "確認の電子メールを送信できませんでした err=%v"
+ "translation": "確認の電子メールを送信できませんでした"
},
{
"id": "api.user.send_welcome_email_and_forget.failed.error",
- "translation": "歓迎の電子メールを送信できませんでした err=%v"
+ "translation": "歓迎の電子メールを送信できませんでした"
},
{
"id": "api.user.update_active.no_deactivate_ldap.app_error",
@@ -2776,10 +2752,50 @@
"translation": "WebRTCトークンを登録する際にエラーが発生しました"
},
{
+ "id": "api.websocket.invalid_session.error",
+ "translation": "無効なセッション err=%v"
+ },
+ {
"id": "api.websocket_handler.invalid_param.app_error",
"translation": "不正な{{.Name}}パラメーターです"
},
{
+ "id": "app.channel.post_update_channel_purpose_message.post.error",
+ "translation": "チャンネルの目的メッセージを投稿できませんでした"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.removed",
+ "translation": "%sがチャンネルヘッダーを削除しました(これまでは%sでした)"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error",
+ "translation": "チャンネルの目的メッセージ %v を更新する際にユーザーを取得できませんでした"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_from",
+ "translation": "%s がチャンネルの目的を %s から %s に更新しました"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_to",
+ "translation": "%s がチャンネルの目的を %s に更新しました"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.description",
+ "translation": "Ability to create new teams"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.name",
+ "translation": "Create Teams"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.description",
+ "translation": "Ability to change the roles of a team member"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.name",
+ "translation": "Manage Team Roles"
+ },
+ {
"id": "authentication.permissions.team_invite_user.description",
"translation": "チームにユーザーを招待できるようにする"
},
diff --git a/i18n/ko.json b/i18n/ko.json
index 26d164a5a..878266a8c 100644
--- a/i18n/ko.json
+++ b/i18n/ko.json
@@ -301,7 +301,7 @@
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error",
- "translation": "Failed to post displayname update message %v"
+ "translation": "Failed to post displayname update message"
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error",
@@ -312,8 +312,8 @@
"translation": "%s 이(가) 채널 헤더를 %s에서 %s(으)로 갱신했습니다"
},
{
- "id": "api.channel.post_update_channel_header_message_and_forget.join_leave.error",
- "translation": "메시지 %v 를 포스트 하는 데 실패하였습니다"
+ "id": "api.channel.post_update_channel_header_message_and_forget.post.error",
+ "translation": "Failed to post update channel header message"
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.removed",
@@ -800,12 +800,16 @@
"translation": "적절하지 않은 {{.Name}} 파라미터"
},
{
+ "id": "api.context.invalid_session.error",
+ "translation": "Invalid session err=%v"
+ },
+ {
"id": "api.context.invalid_team_url.debug",
"translation": "올바르지 않은 상황에서 팀 URL에 접근했습니다. 팀 URL은 API 함수나 팀과 관련이 없는 곳에서 사용되어서는 안 됩니다."
},
{
"id": "api.context.invalid_token.error",
- "translation": "유효하지않은 세션 토큰=%v, err=%v"
+ "translation": "Invalid session token={{.Token}}, err={{.Error}}"
},
{
"id": "api.context.invalidate_all_caches",
@@ -1468,26 +1472,10 @@
"translation": "작업을 할 수 있는 권한이 없습니다"
},
{
- "id": "api.post.handle_post_events_and_forget.channel.error",
- "translation": "채널을 찾을 수 없습니다, channel_id=%s, err=%v"
- },
- {
"id": "api.post.handle_post_events_and_forget.members.error",
"translation": "채널의 참가자를 얻을 수 없습니다, channel_id=%v err=%v"
},
{
- "id": "api.post.handle_post_events_and_forget.profiles.error",
- "translation": "사용자 프로필을 가져올 수 없습니다, team_id=%v, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.team.error",
- "translation": "팀을 가져올 수 없습니다, team_id=%s, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.user.error",
- "translation": "사용자를 가져올 수 없습니다, user_id=%s, err=%v"
- },
- {
"id": "api.post.handle_webhook_events_and_forget.create_post.error",
"translation": "회신 게시물 생성에 실패했습니다. err=%v"
},
@@ -1496,10 +1484,6 @@
"translation": "이벤트 POST 실패. err=%s"
},
{
- "id": "api.post.handle_webhook_events_and_forget.getting.error",
- "translation": "팀의 웹훅을 가져올 수 없습니다. err=%v"
- },
- {
"id": "api.post.init.debug",
"translation": "post API 경로 초기화 중"
},
@@ -1520,20 +1504,16 @@
"translation": "개인 메시지 채널의 속성을 변경하지 못했습니다. user_id=%v other_user_id=%v err=%v"
},
{
- "id": "api.post.notification.here.warn",
- "translation": "@here (으)로 온라인 사용자에게 알림을 보낼 수 없습니다. err=%v"
- },
- {
"id": "api.post.notification.member_profile.warn",
"translation": "채널 회원의 프로필 사진을 불러올 수 없습니다. user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
- "translation": "Clearing push notification to %v with channel_id %v"
+ "id": "api.post.send_notifications.user_id.debug",
+ "translation": "Post creator not in channel for the post, no notification sent post_id=%v channel_id=%v user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.comment_thread.error",
- "translation": "Failed to retrieve comment thread posts in notifications root_post_id=%v, err=%v"
+ "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
+ "translation": "Clearing push notification to %v with channel_id %v"
},
{
"id": "api.post.send_notifications_and_forget.files.error",
@@ -1581,25 +1561,13 @@
},
{
"id": "api.post.send_notifications_and_forget.push_notification.error",
- "translation": "푸시 알림 발송 실패 device_id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.send.error",
- "translation": "발신 실패한 멘션 이메일=%v, err=%v"
+ "translation": "Failed to send push device_id={{.DeviceId}}, err={{.Error}}"
},
{
"id": "api.post.send_notifications_and_forget.sent",
"translation": "{{.Prefix}} {{.Filenames}} 보냄"
},
{
- "id": "api.post.send_notifications_and_forget.sessions.error",
- "translation": "Failed to retrieve sessions in notifications id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.user_id.error",
- "translation": "Post user_id not returned by GetProfiles user_id=%v"
- },
- {
"id": "api.post.update_mention_count_and_forget.update_error",
"translation": "Failed to update mention count, post_id=%v channel_id=%v err=%v"
},
@@ -1612,10 +1580,18 @@
"translation": "작업을 할 수 있는 권한이 없습니다."
},
{
+ "id": "api.post.update_post.permissions_denied.app_error",
+ "translation": "새로운 팀을 생성할 수 없습니다. 시스템 관리자에게 문의해보세요."
+ },
+ {
"id": "api.post.update_post.permissions_details.app_error",
"translation": "이미 지워짐. ID={{.PostId}}"
},
{
+ "id": "api.post.update_post.permissions_time_limit.app_error",
+ "translation": "Post edit is only allowed for {{.timeLimit}} seconds. Please ask your systems administrator for details."
+ },
+ {
"id": "api.post.update_post.system_message.app_error",
"translation": "시스템 메시지를 변경할 수 없습니다."
},
@@ -2273,7 +2249,7 @@
},
{
"id": "api.user.add_direct_channels_and_forget.failed.error",
- "translation": "사용자의 다이렉트 채널 기본 구성을 실패하였습니다. user_id=%s, team_id=%s, err=%v"
+ "translation": "Failed to add direct channel preferences for user user_id={{.UserId}}, team_id={{.TeamId}}, err={{.Error}}"
},
{
"id": "api.user.authorize_oauth_user.bad_response.app_error",
@@ -2776,10 +2752,50 @@
"translation": "WebRTC 토큰 등록 중에 에러가 발생하였습니다."
},
{
+ "id": "api.websocket.invalid_session.error",
+ "translation": "Invalid session err=%v"
+ },
+ {
"id": "api.websocket_handler.invalid_param.app_error",
"translation": "적절하지 않은 {{.Name}} 파라미터"
},
{
+ "id": "app.channel.post_update_channel_purpose_message.post.error",
+ "translation": "Failed to post channel purpose message"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.removed",
+ "translation": "%s이(가) 채널 헤더를 제거했습니다 (이전: %s)"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error",
+ "translation": "업데이트 채널 헤더 메시지 %v을(를) 저장 시도중 사용자를 가져오는데 실패했습니다"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_from",
+ "translation": "%s 이(가) 채널 헤더를 %s에서 %s(으)로 갱신했습니다"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_to",
+ "translation": "%s이(가) 채널 헤더를 %s로 갱신했습니다"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.description",
+ "translation": "Ability to create new teams"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.name",
+ "translation": "Create Teams"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.description",
+ "translation": "Ability to change the roles of a team member"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.name",
+ "translation": "Manage Team Roles"
+ },
+ {
"id": "authentication.permissions.team_invite_user.description",
"translation": "팀 사용자 초대 권한"
},
diff --git a/i18n/nl.json b/i18n/nl.json
index f6193aee8..60ea07a56 100644
--- a/i18n/nl.json
+++ b/i18n/nl.json
@@ -312,8 +312,8 @@
"translation": "%s heeft de kanaalkoptekst veranderd van: %s naar: %s"
},
{
- "id": "api.channel.post_update_channel_header_message_and_forget.join_leave.error",
- "translation": "Plaatsen van bericht over binnenkomen/verlaten %v is mislukt"
+ "id": "api.channel.post_update_channel_header_message_and_forget.post.error",
+ "translation": "Failed to post update channel header message"
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.removed",
@@ -333,7 +333,7 @@
},
{
"id": "api.channel.post_user_add_remove_message_and_forget.error",
- "translation": "Het plaatsen van een komen/gaan bericht %v is mislukt"
+ "translation": "Plaatsen van bericht over binnenkomen/verlaten %v is mislukt"
},
{
"id": "api.channel.remove.default.app_error",
@@ -800,12 +800,16 @@
"translation": "Ongeldig {{.Name}} parameter"
},
{
+ "id": "api.context.invalid_session.error",
+ "translation": "Invalid session err=%v"
+ },
+ {
"id": "api.context.invalid_team_url.debug",
"translation": "De benaderde Team URL is niet geldig. Team URL moet niet gebruikt worden bij api aanroepen die team onafhankelijk zijn"
},
{
"id": "api.context.invalid_token.error",
- "translation": "Ongeldig sessie token=%v, fout=%v"
+ "translation": "Invalid session token={{.Token}}, err={{.Error}}"
},
{
"id": "api.context.invalidate_all_caches",
@@ -1468,26 +1472,10 @@
"translation": "U beschikt niet over de juiste rechten"
},
{
- "id": "api.post.handle_post_events_and_forget.channel.error",
- "translation": "Er is een probleem opgetreden bij het ophalen van het kanaal. channel_id=%s, fout=%v"
- },
- {
"id": "api.post.handle_post_events_and_forget.members.error",
"translation": "Ophalen van de deelnemers van het kanaal is mislukt. channel_id=%v, fout=%v"
},
{
- "id": "api.post.handle_post_events_and_forget.profiles.error",
- "translation": "Ophalen van gebruikers profielen is mislukt. team_id=%v, fout=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.team.error",
- "translation": "Er is een fout opgetreden bij het ophalen van het team. team_id=%s, fout=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.user.error",
- "translation": "Er is een fout opgetreden bij het ophalen van de gebruiker. user_id=%s, fout=%v"
- },
- {
"id": "api.post.handle_webhook_events_and_forget.create_post.error",
"translation": "Het creeren van een response bericht is mislukt. fout=%v"
},
@@ -1496,10 +1484,6 @@
"translation": "Gebeurtenis POST is mislukt, fout=%s"
},
{
- "id": "api.post.handle_webhook_events_and_forget.getting.error",
- "translation": "Er is een probleem opgetreden bij het ophalen van de webhooks door team. fout=%v"
- },
- {
"id": "api.post.init.debug",
"translation": "Initialisatie van de berichten API routes"
},
@@ -1520,20 +1504,16 @@
"translation": "Het bijwerken van de voorkeuren van een direct kanaal is mislukt. user_id=%v, other_user_id=%v, fout=%v"
},
{
- "id": "api.post.notification.here.warn",
- "translation": "Kon geen bericht sturen naar online leden met @here, err=%v"
- },
- {
"id": "api.post.notification.member_profile.warn",
"translation": "Niet mogelijk om profiel voor kanaal lid op te halen, user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
- "translation": "Verwijderen van push notificatie aan %v met kanaal_id %v"
+ "id": "api.post.send_notifications.user_id.debug",
+ "translation": "Post creator not in channel for the post, no notification sent post_id=%v channel_id=%v user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.comment_thread.error",
- "translation": "Niet gelukt om commentaar thread berichten op te halen uit notificaties root_post_id=%v, err=%v"
+ "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
+ "translation": "Verwijderen van push notificatie aan %v met kanaal_id %v"
},
{
"id": "api.post.send_notifications_and_forget.files.error",
@@ -1581,25 +1561,13 @@
},
{
"id": "api.post.send_notifications_and_forget.push_notification.error",
- "translation": "Het versturen van een push bericht is mislukt, device_id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.send.error",
- "translation": "Het versturen van een notificatie via e-mail is niet gelukt. e-mail=%v, fout=%v"
+ "translation": "Failed to send push device_id={{.DeviceId}}, err={{.Error}}"
},
{
"id": "api.post.send_notifications_and_forget.sent",
"translation": "{{.Prefix}} {{.Filenames}} verstuurd"
},
{
- "id": "api.post.send_notifications_and_forget.sessions.error",
- "translation": "Er is een probleem opgetreden bij het ophalen van sessies bij notificaties. id=%v, fout=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.user_id.error",
- "translation": "Bericht user_id is niet teruggegeven door GetProfiles user_id=%v"
- },
- {
"id": "api.post.update_mention_count_and_forget.update_error",
"translation": "Kon het aantal vermeldingen niet bijwerken, post_id=%v channel_id=%v err=%v"
},
@@ -1612,10 +1580,18 @@
"translation": "U beschikt niet over de juiste rechten"
},
{
+ "id": "api.post.update_post.permissions_denied.app_error",
+ "translation": "Teams aanmaken is uitgeschakeld. Neem contact op met systeem beheerder voor details."
+ },
+ {
"id": "api.post.update_post.permissions_details.app_error",
"translation": "Is reeds verwijderd id={{.PostId}}"
},
{
+ "id": "api.post.update_post.permissions_time_limit.app_error",
+ "translation": "Post edit is only allowed for {{.timeLimit}} seconds. Please ask your systems administrator for details."
+ },
+ {
"id": "api.post.update_post.system_message.app_error",
"translation": "Het bijwerken van het systeem bericht is mislukt"
},
@@ -2273,7 +2249,7 @@
},
{
"id": "api.user.add_direct_channels_and_forget.failed.error",
- "translation": "Er is een fout opgetreden bij het toevoegen van instellingen voor een direct kanaal voor gebruiker, user_id=%s, team_id=%s, fout=%v"
+ "translation": "Failed to add direct channel preferences for user user_id={{.UserId}}, team_id={{.TeamId}}, err={{.Error}}"
},
{
"id": "api.user.authorize_oauth_user.bad_response.app_error",
@@ -2776,10 +2752,50 @@
"translation": "We encountered an error trying to register the WebRTC Token"
},
{
+ "id": "api.websocket.invalid_session.error",
+ "translation": "Invalid session err=%v"
+ },
+ {
"id": "api.websocket_handler.invalid_param.app_error",
"translation": "Ongeldige {{.Name}} parameter"
},
{
+ "id": "app.channel.post_update_channel_purpose_message.post.error",
+ "translation": "Failed to post channel purpose message"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.removed",
+ "translation": "%s heeft de kanaalkoptekst verwijderd (was: %s)"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error",
+ "translation": "Kon de gebruiker niet ophalen tijdens het bewaren van de nieuwe kanaalkoptekst %v"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_from",
+ "translation": "%s heeft de kanaalkoptekst veranderd van \"%s\" naar \"%s\""
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_to",
+ "translation": "%s heeft de kanaalkoptekst bijgewerkt naar: %s"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.description",
+ "translation": "Ability to create new teams"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.name",
+ "translation": "Create Teams"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.description",
+ "translation": "Ability to change the roles of a team member"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.name",
+ "translation": "Manage Team Roles"
+ },
+ {
"id": "authentication.permissions.team_invite_user.description",
"translation": "Mogelijkheid om gebruikers uit te nodigen in een team"
},
diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json
index 7c222e978..94c93358e 100644
--- a/i18n/pt-BR.json
+++ b/i18n/pt-BR.json
@@ -301,19 +301,19 @@
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error",
- "translation": "Falha ao postar a mensagem de atualização do nome de exibição %v"
+ "translation": "Falha ao postar a mensagem para atualização do nome de exibição"
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error",
- "translation": "Não foi possível recuperar o usuário ao tentar salvar a mensagem de atualização do nome de exibição %v"
+ "translation": "Error ao recuperar o usuário enquanto atualizava o campo DisplayName do canal"
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from",
"translation": "%s atualizou o nome de exibição do canal de: %s para: %s"
},
{
- "id": "api.channel.post_update_channel_header_message_and_forget.join_leave.error",
- "translation": "Não foi possível postar mensagem juntar/deixar %v"
+ "id": "api.channel.post_update_channel_header_message_and_forget.post.error",
+ "translation": "Falha ao postar a mensagem para atualização do cabeçalho do canal"
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.removed",
@@ -321,7 +321,7 @@
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.retrieve_user.error",
- "translation": "Não foi possível recuperar o usuário ao tentar salvar a atualização do cabeçalho do canal de mensagem %v"
+ "translation": "Não foi possível recuperar o usuário ao tentar salvar a atualização da mensagem do cabeçalho do canal %v"
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.updated_from",
@@ -333,7 +333,7 @@
},
{
"id": "api.channel.post_user_add_remove_message_and_forget.error",
- "translation": "Não foi possível postar mensagem de juntar/deixar %v"
+ "translation": "Erro ao postar mensagem de entrar/sair"
},
{
"id": "api.channel.remove.default.app_error",
@@ -800,12 +800,16 @@
"translation": "Parâmetro {{.Name}} inválido"
},
{
+ "id": "api.context.invalid_session.error",
+ "translation": "Sessão inválida err=%v"
+ },
+ {
"id": "api.context.invalid_team_url.debug",
"translation": "A URL da equipe foi acessada de forma inválida. A URL da equipe não deve ser utilizada em funções de API ou aquelas funções que são independente da equipe."
},
{
"id": "api.context.invalid_token.error",
- "translation": "Sessão inválida token=%v, err=%v"
+ "translation": "Token de sessão inválido={{.Token}}, err={{.Error}}"
},
{
"id": "api.context.invalidate_all_caches",
@@ -1015,7 +1019,7 @@
},
{
"id": "api.file.get_public_link.no_post.app_error",
- "translation": "Não foi possível obter o link publico para o arquivo. Arquivo deve ser anexado a um post que pode ser lido pelo usuário atual."
+ "translation": "Não foi possível obter o link público para o arquivo. O arquivo deve ser anexado a um post que pode ser lido pelo usuário atual."
},
{
"id": "api.file.handle_images_forget.decode.error",
@@ -1468,26 +1472,10 @@
"translation": "Você não tem a permissão apropriada"
},
{
- "id": "api.post.handle_post_events_and_forget.channel.error",
- "translation": "Encontrado um erro ao obter o canal, channel_id=%s, err=%v"
- },
- {
"id": "api.post.handle_post_events_and_forget.members.error",
"translation": "Não foi possível obter os membros do channel_id=%v err=%v"
},
{
- "id": "api.post.handle_post_events_and_forget.profiles.error",
- "translation": "Não foi possível obter perfis de usuário team_id=%v, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.team.error",
- "translation": "Encontrado um erro ao obter a equipe, equipe_id=%s, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.user.error",
- "translation": "Encontrado um erro ao obter o usuário, user_id=%s, err=%v"
- },
- {
"id": "api.post.handle_webhook_events_and_forget.create_post.error",
"translation": "Não foi possível criar resposta do post, err=%v"
},
@@ -1496,16 +1484,12 @@
"translation": "Falha no Evento POST, err=%s"
},
{
- "id": "api.post.handle_webhook_events_and_forget.getting.error",
- "translation": "Encontrado um erro ao obter webhooks pela equipe, err=%v"
- },
- {
"id": "api.post.init.debug",
"translation": "Inicializando as rotas de API post"
},
{
"id": "api.post.make_direct_channel_visible.get_2_members.error",
- "translation": "Falha ao obter 2 membros para o canal direto channel_id=%v"
+ "translation": "Falha ao obter 2 membros para o canal direto channel_id={{.ChannelId}}"
},
{
"id": "api.post.make_direct_channel_visible.get_members.error",
@@ -1520,20 +1504,16 @@
"translation": "Falha ao atualizar as preferencias do canal direto user_id=%v other_user_id=%v err=%v"
},
{
- "id": "api.post.notification.here.warn",
- "translation": "Não foi possível enviar a notificação para os usuários conectados usando @here, err=%v"
- },
- {
"id": "api.post.notification.member_profile.warn",
"translation": "Não foi possível obter o perfil do membro do canal, user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
- "translation": "Limpando notificação push para %v com channel_id %v"
+ "id": "api.post.send_notifications.user_id.debug",
+ "translation": "O criador da postagem não está no canal onde a postagem foi criada post_id=%v channel_id=%v user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.comment_thread.error",
- "translation": "Falha ao obter os comentários da mensagem em notificações root_post_id=%v, err=%v"
+ "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
+ "translation": "Limpando notificação push para %v com channel_id %v"
},
{
"id": "api.post.send_notifications_and_forget.files.error",
@@ -1581,25 +1561,13 @@
},
{
"id": "api.post.send_notifications_and_forget.push_notification.error",
- "translation": "Falha ao enviar notificação push device_id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.send.error",
- "translation": "Falha ao enviar uma menção por email email=%v err=%v"
+ "translation": "Falha ao enviar notificação device_id={{.DeviceId}}, err={{.Error}}"
},
{
"id": "api.post.send_notifications_and_forget.sent",
"translation": "{{.Prefix}} {{.Filenames}} enviado"
},
{
- "id": "api.post.send_notifications_and_forget.sessions.error",
- "translation": "Falha ao obter as notificações na sessão id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.user_id.error",
- "translation": "Post user_id not returned by GetProfiles user_id=%v"
- },
- {
"id": "api.post.update_mention_count_and_forget.update_error",
"translation": "Falha ao atualizar o número de menções, post_id=%v channel_id=%v err=%v"
},
@@ -1612,10 +1580,18 @@
"translation": "Você não tem a permissão apropriada"
},
{
+ "id": "api.post.update_post.permissions_denied.app_error",
+ "translation": "Edição de postagem foi desativada. Por favor, pergunte ao administrador do sistema para obter detalhes."
+ },
+ {
"id": "api.post.update_post.permissions_details.app_error",
"translation": "Já deletado id={{.PostId}}"
},
{
+ "id": "api.post.update_post.permissions_time_limit.app_error",
+ "translation": "Edição de postagens só são permitidas por {{.timeLimit}} segundos. Por favor peça ao administrador do sistema por detalhes."
+ },
+ {
"id": "api.post.update_post.system_message.app_error",
"translation": "Não foi possível atualizar a mensagem do sistema"
},
@@ -1993,11 +1969,11 @@
},
{
"id": "api.team.is_team_creation_allowed.disabled.app_error",
- "translation": "Criação de equipe foi desativada. Por favor, pergunte ao seu administrador de sistemas para obter detalhes."
+ "translation": "Criação de equipe foi desativada. Por favor, pergunte ao administrador do sistema para obter detalhes."
},
{
"id": "api.team.is_team_creation_allowed.domain.app_error",
- "translation": "Email precisa ser de um domínio específico (ex. @example.com). Por favor pergunte ao seu administrador de sistema por detalhes."
+ "translation": "Email precisa ser de um domínio específico (ex. @example.com). Por favor pergunte ao administrador do sistema por detalhes."
},
{
"id": "api.team.permanent_delete_team.attempting.warn",
@@ -2149,7 +2125,7 @@
},
{
"id": "api.templates.post_subject_in_channel",
- "translation": "{{.SubjectText}} de {{.TeamDisplayName}} ({{.ChannelName}}) em {{.Day}} {{.Month}}, {{.Year}}"
+ "translation": "{{.SubjectText}} em {{.TeamDisplayName}} ({{.ChannelName}}) de {{.Day}} {{.Month}}, {{.Year}}"
},
{
"id": "api.templates.post_subject_in_direct_message",
@@ -2273,7 +2249,7 @@
},
{
"id": "api.user.add_direct_channels_and_forget.failed.error",
- "translation": "Falha ao adicionar preferencias diretas ao canal para o usuário user_id=%s, team_id=%s, err=%v"
+ "translation": "Falha ao adicionar as preferências de canal direto para o usuário user_id={{.UserId}}, team_id={{.TeamId}}, err={{.Error}}"
},
{
"id": "api.user.authorize_oauth_user.bad_response.app_error",
@@ -2497,7 +2473,7 @@
},
{
"id": "api.user.permanent_delete_user.system_admin.warn",
- "translation": "Você está deletando %v que é um administrador de sistema. Você pode precisar definir outra conta como administrador de sistema usando as ferramentas de linha de comando."
+ "translation": "Você está deletando %v que é um administrador do sistema. Você pode precisar definir outra conta como administrador de sistema usando as ferramentas de linha de comando."
},
{
"id": "api.user.reset_password.invalid_link.app_error",
@@ -2525,19 +2501,19 @@
},
{
"id": "api.user.send_email_change_email_and_forget.error",
- "translation": "Falha ao enviar com sucesso uma notificação por email err=%v"
+ "translation": "Erro ao enviar email de troca de notificação"
},
{
"id": "api.user.send_email_change_username_and_forget.error",
- "translation": "Falha ao enviar notificação por email sobre alteração do nome do usuário err=%v"
+ "translation": "Erro ao enviar notificação por email sobre alteração do nome do usuário"
},
{
"id": "api.user.send_email_change_verify_email_and_forget.error",
- "translation": "Falha ao enviar com sucesso uma verificação de email err=%v"
+ "translation": "Erro ao enviar verificação de email"
},
{
"id": "api.user.send_password_change_email_and_forget.error",
- "translation": "Falha ao enviar com sucesso uma atualização de senha por email err=%v"
+ "translation": "Erro ao enviar uma atualização de senha por email"
},
{
"id": "api.user.send_password_reset.find.app_error",
@@ -2553,15 +2529,15 @@
},
{
"id": "api.user.send_sign_in_change_email_and_forget.error",
- "translation": "Falha ao enviar com sucesso uma atualização de senha por email err=%v"
+ "translation": "Erro ao enviar atualização de senha por email"
},
{
"id": "api.user.send_verify_email_and_forget.failed.error",
- "translation": "Falha ao enviar com sucesso uma verificação de email err=%v"
+ "translation": "Erro ao enviar verificação de email"
},
{
"id": "api.user.send_welcome_email_and_forget.failed.error",
- "translation": "Falha ao enviar com sucesso boas vindas por email err=%v"
+ "translation": "Erro ao enviar boas vindas por email"
},
{
"id": "api.user.update_active.no_deactivate_ldap.app_error",
@@ -2776,10 +2752,50 @@
"translation": "Encontrado um erro ao tentar registrar o Token WebRTC"
},
{
+ "id": "api.websocket.invalid_session.error",
+ "translation": "Sessão inválida err=%v"
+ },
+ {
"id": "api.websocket_handler.invalid_param.app_error",
"translation": "Parâmetro {{.Name}} inválido"
},
{
+ "id": "app.channel.post_update_channel_purpose_message.post.error",
+ "translation": "Falha ao postar a mensagem de propósito do canal"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.removed",
+ "translation": "%s removido do propósito do canal (foi: %s)"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error",
+ "translation": "Não foi possível recuperar o usuário ao tentar salvar a atualização do propósito do canal %v"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_from",
+ "translation": "%s atualizou o propósito do canal de: %s para: %s"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_to",
+ "translation": "%s atualizou o propósito do canal para: %s"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.description",
+ "translation": "Ability to create new teams"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.name",
+ "translation": "Create Teams"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.description",
+ "translation": "Ability to change the roles of a team member"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.name",
+ "translation": "Manage Team Roles"
+ },
+ {
"id": "authentication.permissions.team_invite_user.description",
"translation": "Permissão para convidar usuários para uma equipe"
},
@@ -4921,7 +4937,7 @@
},
{
"id": "store.sql_system.get.app_error",
- "translation": "Encontramos um erro ao procurar as propriedades de sistema"
+ "translation": "Encontramos um erro ao procurar as propriedades do sistema"
},
{
"id": "store.sql_system.get_by_name.app_error",
@@ -4937,7 +4953,7 @@
},
{
"id": "store.sql_system.update.app_error",
- "translation": "Encontramos um erro ao atualizar as propriedades de sistema"
+ "translation": "Encontramos um erro ao atualizar as propriedades do sistema"
},
{
"id": "store.sql_team.analytics_team_count.app_error",
@@ -5137,7 +5153,7 @@
},
{
"id": "store.sql_user.save.max_accounts.app_error",
- "translation": "Esta equipe alcançou o número máximo de contas permitidas. Contate o seu administrador de sistema para ajustar um limite maior."
+ "translation": "Esta equipe alcançou o número máximo de contas permitidas. Contate o administrador do sistema para ajustar um limite maior."
},
{
"id": "store.sql_user.save.member_count.app_error",
diff --git a/i18n/ru.json b/i18n/ru.json
index 4a8a74b2c..ffac86598 100644
--- a/i18n/ru.json
+++ b/i18n/ru.json
@@ -241,11 +241,11 @@
},
{
"id": "api.channel.delete_channel.incoming_webhook.error",
- "translation": "Обнаружена ошибка при удалении входящего webhook'a, id=%v"
+ "translation": "Обнаружена ошибка при удалении входящего вебхукa, id=%v"
},
{
"id": "api.channel.delete_channel.outgoing_webhook.error",
- "translation": "Обнаружена ошибка при удалении исходящего webhook'a, id=%v"
+ "translation": "Обнаружена ошибка при удалении исходящего вебхукa, id=%v"
},
{
"id": "api.channel.delete_channel.permissions.app_error",
@@ -301,19 +301,19 @@
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error",
- "translation": "Ну удалось отправить сообщение об обновлении отображаемого имени канала %v"
+ "translation": "Ну удалось отправить сообщение об обновлении отображаемого имени канала"
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error",
- "translation": "Не удалось получить пользователя при попытке обновления отображаемого имени канала %v"
+ "translation": "Не удалось получить пользователя при попытке обновления сообщения заголовка канала"
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from",
"translation": "%s обновил отображаемое имя канала с: %s на: %s"
},
{
- "id": "api.channel.post_update_channel_header_message_and_forget.join_leave.error",
- "translation": "Не удалось отправить join/leave сообщение %v"
+ "id": "api.channel.post_update_channel_header_message_and_forget.post.error",
+ "translation": "Failed to post update channel header message"
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.removed",
@@ -333,7 +333,7 @@
},
{
"id": "api.channel.post_user_add_remove_message_and_forget.error",
- "translation": "Не удалось отправить сообщение о присоединении/выходе %v"
+ "translation": "Не удалось отправить сообщение о присоединении/отсоединении"
},
{
"id": "api.channel.remove.default.app_error",
@@ -453,7 +453,7 @@
},
{
"id": "api.command.regen.app_error",
- "translation": "Несоответствующие права для генерации нового токена"
+ "translation": "Отсутствуют права на пересоздание токена"
},
{
"id": "api.command.team_mismatch.app_error",
@@ -800,12 +800,16 @@
"translation": "Неверный параметр {{.Name}}"
},
{
+ "id": "api.context.invalid_session.error",
+ "translation": "Invalid session err=%v"
+ },
+ {
"id": "api.context.invalid_team_url.debug",
"translation": "Адрес команды доступен, но не корректен. Он не должен использоваться в API или в тех функциях, что независимы от неё."
},
{
"id": "api.context.invalid_token.error",
- "translation": "Неверный токен сессии %v, ошибка=%v"
+ "translation": "Invalid session token={{.Token}}, err={{.Error}}"
},
{
"id": "api.context.invalidate_all_caches",
@@ -1468,26 +1472,10 @@
"translation": "У вас нет соответствующих прав"
},
{
- "id": "api.post.handle_post_events_and_forget.channel.error",
- "translation": "Возникла ошибка получения канала, channel_id=%s, err=%v"
- },
- {
"id": "api.post.handle_post_events_and_forget.members.error",
"translation": "Не удалось получить членов канала channel_id=%v, err=%v"
},
{
- "id": "api.post.handle_post_events_and_forget.profiles.error",
- "translation": "Не удалось извлечь профили пользователей team_id=%v, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.team.error",
- "translation": "Возникла ошибка получения команды, team_id=%s, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.user.error",
- "translation": "Возникла ошибка получения пользователя, user_id=%s, err=%v"
- },
- {
"id": "api.post.handle_webhook_events_and_forget.create_post.error",
"translation": "Не удалось создать ответ на запись, err=%v"
},
@@ -1496,16 +1484,12 @@
"translation": "Ошибка отправки события, err=%s"
},
{
- "id": "api.post.handle_webhook_events_and_forget.getting.error",
- "translation": "Ошибка получения вебхуков для команды, err=%v"
- },
- {
"id": "api.post.init.debug",
"translation": "Инициализация API отправки"
},
{
"id": "api.post.make_direct_channel_visible.get_2_members.error",
- "translation": "Не удалось получить 2 участников для прямого канала channel_id=%v"
+ "translation": "Не удалось получить 2 участников для канала личных сообщений channel_id={{.ChannelId}}"
},
{
"id": "api.post.make_direct_channel_visible.get_members.error",
@@ -1520,20 +1504,16 @@
"translation": "Ошибка обновления настроек канала user_id=%v other_user_id=%v err=%v"
},
{
- "id": "api.post.notification.here.warn",
- "translation": "Невозможно отправить уведомление активным @пользователям, err=%v"
- },
- {
"id": "api.post.notification.member_profile.warn",
"translation": "Невозможно получить профиль участников канала, user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
- "translation": "Очистка push-уведомлений для %v в channel_id %v"
+ "id": "api.post.send_notifications.user_id.debug",
+ "translation": "Post creator not in channel for the post, no notification sent post_id=%v channel_id=%v user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.comment_thread.error",
- "translation": "Ошибка получения уведомлений о комментариях к теме обсуждения root_post_id=%v err=%v"
+ "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
+ "translation": "Очистка push-уведомлений для %v в channel_id %v"
},
{
"id": "api.post.send_notifications_and_forget.files.error",
@@ -1581,25 +1561,13 @@
},
{
"id": "api.post.send_notifications_and_forget.push_notification.error",
- "translation": "Ошибка отправки push device_id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.send.error",
- "translation": "Не удалось отправить приглашение по электронной почте email=%v err=%v"
+ "translation": "Failed to send push device_id={{.DeviceId}}, err={{.Error}}"
},
{
"id": "api.post.send_notifications_and_forget.sent",
"translation": "{{.Prefix}} {{.Filenames}} отправлен"
},
{
- "id": "api.post.send_notifications_and_forget.sessions.error",
- "translation": "Не удалось получить сеанс в уведомлении id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.user_id.error",
- "translation": "Пост user_id не возвращён GetProfiles user_id=%v"
- },
- {
"id": "api.post.update_mention_count_and_forget.update_error",
"translation": "Не удалось обновить количество упоминаний, post_id=%v channel_id=%v err=%v"
},
@@ -1612,10 +1580,18 @@
"translation": "У вас нет соответствующих прав"
},
{
+ "id": "api.post.update_post.permissions_denied.app_error",
+ "translation": "Возможность создания команд была отключена. Пожалуйста, обратитесь к вашему системному администратору за подробностями."
+ },
+ {
"id": "api.post.update_post.permissions_details.app_error",
"translation": "Уже удален id={{.PostId}}"
},
{
+ "id": "api.post.update_post.permissions_time_limit.app_error",
+ "translation": "Post edit is only allowed for {{.timeLimit}} seconds. Please ask your systems administrator for details."
+ },
+ {
"id": "api.post.update_post.system_message.app_error",
"translation": "Не удалось обновить системное сообщение"
},
@@ -1717,11 +1693,11 @@
},
{
"id": "api.slackimport.slack_add_bot_user.email_pwd",
- "translation": "Slack Bot/Integration Posts Import User: Email, Password: {{.Email}}, {{.Password}}\r\n"
+ "translation": "Пользователь для импорта сообщений ботов и интеграций Slack: {{.Email}}, {{.Password}}\r\n"
},
{
"id": "api.slackimport.slack_add_bot_user.unable_import",
- "translation": "Unable to import Slack Bot/Integration Posts Import User: {{.Username}}\r\n"
+ "translation": "Не удалось импортировать пользователя для импорта сообщений ботов и интеграций Slack: {{.Username}}\r\n"
},
{
"id": "api.slackimport.slack_add_channels.added",
@@ -1753,7 +1729,7 @@
},
{
"id": "api.slackimport.slack_add_posts.bot_user_no_exists.warn",
- "translation": "Slack Importer: Not importing bot message as the bot-importing user does not exist."
+ "translation": "Импорт Slack: Не импортируем сообщение бота. Пользователь для импорта ботов не найден."
},
{
"id": "api.slackimport.slack_add_posts.msg_no_comment.debug",
@@ -1765,7 +1741,7 @@
},
{
"id": "api.slackimport.slack_add_posts.no_bot_id.warn",
- "translation": "Slack Importer: Not importing bot message due to lack of BotId field."
+ "translation": "Импорт Slack: Не импортируем сообщение бота. Отсутствует поле BotId."
},
{
"id": "api.slackimport.slack_add_posts.unsupported.warn",
@@ -1829,7 +1805,7 @@
},
{
"id": "api.slackimport.slack_deactivate_bot_user.failed_to_deactivate",
- "translation": "Slack Importer: Failed to deactivate the bot-importing user."
+ "translation": "Импорт Slack: Не удалось деактивировать пользователя для импорта ботов."
},
{
"id": "api.slackimport.slack_import.log",
@@ -2273,7 +2249,7 @@
},
{
"id": "api.user.add_direct_channels_and_forget.failed.error",
- "translation": "Не удалось добавить предпочтения прямого канала для пользователя user_id=%s, team_id=%s, err=%v"
+ "translation": "Failed to add direct channel preferences for user user_id={{.UserId}}, team_id={{.TeamId}}, err={{.Error}}"
},
{
"id": "api.user.authorize_oauth_user.bad_response.app_error",
@@ -2525,19 +2501,19 @@
},
{
"id": "api.user.send_email_change_email_and_forget.error",
- "translation": "Не удалось отправить письмо с уведомленим о смене адреса электронной почты err=%v"
+ "translation": "Не удалось отправить письмо с уведомленим о смене адреса электронной почты"
},
{
"id": "api.user.send_email_change_username_and_forget.error",
- "translation": "Не удалось отправить письмо с уведомленим о смене имени пользователя err=%v"
+ "translation": "Не удалось отправить письмо с уведомленим о смене имени пользователя"
},
{
"id": "api.user.send_email_change_verify_email_and_forget.error",
- "translation": "Не удалось отправить письмо с подтверждением смены адреса электронной почты err=%v"
+ "translation": "Не удалось отправить письмо с подтверждением смены адреса электронной почты"
},
{
"id": "api.user.send_password_change_email_and_forget.error",
- "translation": "Не удалось отправить письмо об обновлении пароля err=%v"
+ "translation": "Не удалось отправить письмо об обновлении пароля"
},
{
"id": "api.user.send_password_reset.find.app_error",
@@ -2553,15 +2529,15 @@
},
{
"id": "api.user.send_sign_in_change_email_and_forget.error",
- "translation": "Не удалось отправить письмо об обновлении пароля err=%v"
+ "translation": "Не удалось отправить письмо об обновлении пароля"
},
{
"id": "api.user.send_verify_email_and_forget.failed.error",
- "translation": "Не удалось отправить письмо с подтверждением err=%v"
+ "translation": "Не удалось отправить письмо с подтверждением"
},
{
"id": "api.user.send_welcome_email_and_forget.failed.error",
- "translation": "Не удалось отправить приветственное письмо err=%v"
+ "translation": "Не удалось отправить приветственное письмо"
},
{
"id": "api.user.update_active.no_deactivate_ldap.app_error",
@@ -2697,11 +2673,11 @@
},
{
"id": "api.webhook.create_incoming.disabled.app_errror",
- "translation": "Команды были отключены системным администратором."
+ "translation": "Входящие вебхуки были отключены системным администратором."
},
{
"id": "api.webhook.create_outgoing.disabled.app_error",
- "translation": "Команды были отключены системным администратором."
+ "translation": "Исходящие вебхуки были отключены системным администратором."
},
{
"id": "api.webhook.create_outgoing.intersect.app_error",
@@ -2709,7 +2685,7 @@
},
{
"id": "api.webhook.create_outgoing.not_open.app_error",
- "translation": "Исходящие webhooks могут быть созданы только для общественных каналов."
+ "translation": "Исходящие вебхуки могут быть созданы только для общественных каналов."
},
{
"id": "api.webhook.create_outgoing.permissions.app_error",
@@ -2721,35 +2697,35 @@
},
{
"id": "api.webhook.delete_incoming.disabled.app_errror",
- "translation": "Входящие webhooks отключены системным администратором."
+ "translation": "Входящие вебхуки отключены системным администратором."
},
{
"id": "api.webhook.delete_incoming.permissions.app_errror",
- "translation": "Несоответствующие права для удаления входящего вебхука"
+ "translation": "Отсутствуют права на удаление входящего вебхука"
},
{
"id": "api.webhook.delete_outgoing.disabled.app_error",
- "translation": "Исходящие webhooks отключены системным администратором."
+ "translation": "Исходящие вебхуки отключены системным администратором."
},
{
"id": "api.webhook.delete_outgoing.permissions.app_error",
- "translation": "Несоответствующие права для удаления исходящего вебхука"
+ "translation": "Отсутствуют права на удаление исходящего вебхука"
},
{
"id": "api.webhook.get_incoming.disabled.app_error",
- "translation": "Входящие webhooks отключены системным администратором."
+ "translation": "Входящие вебхуки отключены системным администратором."
},
{
"id": "api.webhook.get_outgoing.disabled.app_error",
- "translation": "Исходящие webhooks отключены системным администратором."
+ "translation": "Исходящие вебхуки отключены системным администратором."
},
{
"id": "api.webhook.incoming.debug",
- "translation": "Входящий webhook получен. Содержание="
+ "translation": "Входящий вебхук получен. Содержание="
},
{
"id": "api.webhook.incoming.debug.error",
- "translation": "Не удалось прочитать полезную нагрузку входящего webhook."
+ "translation": "Не удалось прочитать полезную нагрузку входящего вебхука."
},
{
"id": "api.webhook.init.debug",
@@ -2757,11 +2733,11 @@
},
{
"id": "api.webhook.regen_outgoing_token.disabled.app_error",
- "translation": "Исходящие webhooks отключены системным администратором."
+ "translation": "Исходящие вебхуки отключены системным администратором."
},
{
"id": "api.webhook.regen_outgoing_token.permissions.app_error",
- "translation": "Несоответствующие права для генерации нового токена исходящего вебхука"
+ "translation": "Отсутствуют права на пересоздание токена для исходящего вебхука"
},
{
"id": "api.webrtc.disabled.app_error",
@@ -2776,10 +2752,50 @@
"translation": "Мы столкнулись с неожиданной ошибкой при попытке зарегистрировать токен WebRTC"
},
{
+ "id": "api.websocket.invalid_session.error",
+ "translation": "Invalid session err=%v"
+ },
+ {
"id": "api.websocket_handler.invalid_param.app_error",
"translation": "Недопустимый параметр {{.Name}}"
},
{
+ "id": "app.channel.post_update_channel_purpose_message.post.error",
+ "translation": "Failed to post channel purpose message"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.removed",
+ "translation": "%s удалил заголовок канала (был: %s)"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error",
+ "translation": "Не удалось получить пользователя при попытке обновления сообщения заголовка канала %v"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_from",
+ "translation": "%s обновил заголовок канала с: %s на: %s"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_to",
+ "translation": "%s обновил заголовок канала на: %s"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.description",
+ "translation": "Ability to create new teams"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.name",
+ "translation": "Create Teams"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.description",
+ "translation": "Ability to change the roles of a team member"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.name",
+ "translation": "Manage Team Roles"
+ },
+ {
"id": "authentication.permissions.team_invite_user.description",
"translation": "Возможность приглашать пользователей в команду"
},
diff --git a/i18n/zh_CN.json b/i18n/zh_CN.json
index ef51d7ad7..3f78b6208 100644
--- a/i18n/zh_CN.json
+++ b/i18n/zh_CN.json
@@ -301,19 +301,19 @@
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.create_post.error",
- "translation": "发送显示名更新信息 %v 时失败"
+ "translation": "发送显示名更新信息时失败"
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.retrieve_user.error",
- "translation": "尝试保存更新的频道标题消息 %v 时获取用户失败"
+ "translation": "尝试保存更新的频道标题消息 %v 时获取用户信息失败"
},
{
"id": "api.channel.post_update_channel_displayname_message_and_forget.updated_from",
"translation": "%s 将频道显示名从 %s 更新为 %s"
},
{
- "id": "api.channel.post_update_channel_header_message_and_forget.join_leave.error",
- "translation": "发布添加/退出消息失败 %v"
+ "id": "api.channel.post_update_channel_header_message_and_forget.post.error",
+ "translation": "Failed to post update channel header message"
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.removed",
@@ -333,7 +333,7 @@
},
{
"id": "api.channel.post_user_add_remove_message_and_forget.error",
- "translation": "发布添加或者退出消息失败 %v"
+ "translation": "发布添加/退出消息失败"
},
{
"id": "api.channel.remove.default.app_error",
@@ -800,12 +800,16 @@
"translation": "无效 {{.Name}} 参数"
},
{
+ "id": "api.context.invalid_session.error",
+ "translation": "无效会话 err=%v"
+ },
+ {
"id": "api.context.invalid_team_url.debug",
"translation": "团队URL访问无效。团队URL不能在API函数或者和不相干的团队中使用"
},
{
"id": "api.context.invalid_token.error",
- "translation": "无效的会话令牌=%v,err=%v"
+ "translation": "无效会话令牌 token={{.Token}}, err={{.Error}}"
},
{
"id": "api.context.invalidate_all_caches",
@@ -1468,26 +1472,10 @@
"translation": "您没有对应的权限"
},
{
- "id": "api.post.handle_post_events_and_forget.channel.error",
- "translation": "获取频道时遇到错误, channel_id=%s, err=%v"
- },
- {
"id": "api.post.handle_post_events_and_forget.members.error",
"translation": "未获取到频道成员 channel_id=%v err=%v"
},
{
- "id": "api.post.handle_post_events_and_forget.profiles.error",
- "translation": "检索用户配置文件失败 team_id=%v, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.team.error",
- "translation": "获取团队时遇到错误, team_id=%s, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.user.error",
- "translation": "获取用户时遇到错误, user_id=%s, err=%v"
- },
- {
"id": "api.post.handle_webhook_events_and_forget.create_post.error",
"translation": "创建响应请求失败, err=%v"
},
@@ -1496,16 +1484,12 @@
"translation": "事件发布失败, err=%s"
},
{
- "id": "api.post.handle_webhook_events_and_forget.getting.error",
- "translation": "通过团队获取webhooks遇到错误, err=%v"
- },
- {
"id": "api.post.init.debug",
"translation": "正在初始化发文 API 路由"
},
{
"id": "api.post.make_direct_channel_visible.get_2_members.error",
- "translation": "获取私聊频道的2个用户失败 channel_id=%v"
+ "translation": "获取私聊频道的2个用户失败 channel_id={{.ChannelId}}"
},
{
"id": "api.post.make_direct_channel_visible.get_members.error",
@@ -1520,20 +1504,16 @@
"translation": "更新私信频道设置失败 user_id=%v other_user_id=%v err=%v"
},
{
- "id": "api.post.notification.here.warn",
- "translation": "无法发送通知到的用户在线@here, err=%v"
- },
- {
"id": "api.post.notification.member_profile.warn",
"translation": "无法获取频道成员个人资料,user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
- "translation": "清除 %v 拥有 channel_id %v 的通知中"
+ "id": "api.post.send_notifications.user_id.debug",
+ "translation": "Post creator not in channel for the post, no notification sent post_id=%v channel_id=%v user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.comment_thread.error",
- "translation": "在发送通知时获取注解内容时失败 root_post_id=%v, err=%v"
+ "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
+ "translation": "清除 %v 拥有 channel_id %v 的通知中"
},
{
"id": "api.post.send_notifications_and_forget.files.error",
@@ -1581,25 +1561,13 @@
},
{
"id": "api.post.send_notifications_and_forget.push_notification.error",
- "translation": "发送推送失败 device_id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.send.error",
- "translation": "发送电子邮件失败 email=%v err=%v"
+ "translation": "Failed to send push device_id={{.DeviceId}}, err={{.Error}}"
},
{
"id": "api.post.send_notifications_and_forget.sent",
"translation": "{{.Prefix}} {{.Filenames}} 发送"
},
{
- "id": "api.post.send_notifications_and_forget.sessions.error",
- "translation": "未能检索通知中的会话 id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.user_id.error",
- "translation": "通过GetProfiles请求user_id没有返回值 user_id=%v"
- },
- {
"id": "api.post.update_mention_count_and_forget.update_error",
"translation": "更新提及数失败 user_id=%v channel_id=%v err=%v"
},
@@ -1612,10 +1580,18 @@
"translation": "您没有对应的权限"
},
{
+ "id": "api.post.update_post.permissions_denied.app_error",
+ "translation": "团队创建被禁用。详细请询问您的系统管理员。"
+ },
+ {
"id": "api.post.update_post.permissions_details.app_error",
"translation": "已经删除 id={{.PostId}}"
},
{
+ "id": "api.post.update_post.permissions_time_limit.app_error",
+ "translation": "Post edit is only allowed for {{.timeLimit}} seconds. Please ask your systems administrator for details."
+ },
+ {
"id": "api.post.update_post.system_message.app_error",
"translation": "无法更新系统消息"
},
@@ -2273,7 +2249,7 @@
},
{
"id": "api.user.add_direct_channels_and_forget.failed.error",
- "translation": "添加用户私聊频道偏好设置失败 user_id=%s, team_id=%s, err=%v"
+ "translation": "Failed to add direct channel preferences for user user_id={{.UserId}}, team_id={{.TeamId}}, err={{.Error}}"
},
{
"id": "api.user.authorize_oauth_user.bad_response.app_error",
@@ -2776,10 +2752,50 @@
"translation": "我们在注册 WebRTC 令牌时遇到个错误"
},
{
+ "id": "api.websocket.invalid_session.error",
+ "translation": "无效会话 err=%v"
+ },
+ {
"id": "api.websocket_handler.invalid_param.app_error",
"translation": "无效的参数 {{.Name}}"
},
{
+ "id": "app.channel.post_update_channel_purpose_message.post.error",
+ "translation": "Failed to post channel purpose message"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.removed",
+ "translation": "%s 删除了频道标题 (原为: %s)"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error",
+ "translation": "尝试保存更新的频道标题消息 %v 时获取用户信息失败"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_from",
+ "translation": "%s 将频道标题从 %s 更新为 %s"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_to",
+ "translation": "%s 更新频道标题为: %s"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.description",
+ "translation": "Ability to create new teams"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.name",
+ "translation": "Create Teams"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.description",
+ "translation": "Ability to change the roles of a team member"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.name",
+ "translation": "Manage Team Roles"
+ },
+ {
"id": "authentication.permissions.team_invite_user.description",
"translation": "可以邀请用户到团队"
},
diff --git a/i18n/zh_TW.json b/i18n/zh_TW.json
index 1299f2d2b..136f89fc3 100644
--- a/i18n/zh_TW.json
+++ b/i18n/zh_TW.json
@@ -312,8 +312,8 @@
"translation": "%s 將原頻道標題由 %s 改為 %s"
},
{
- "id": "api.channel.post_update_channel_header_message_and_forget.join_leave.error",
- "translation": "張貼 加入/退出 訊息 %v 失敗"
+ "id": "api.channel.post_update_channel_header_message_and_forget.post.error",
+ "translation": "Failed to post update channel header message"
},
{
"id": "api.channel.post_update_channel_header_message_and_forget.removed",
@@ -800,12 +800,16 @@
"translation": "無效的 {{.Name}} 參數"
},
{
+ "id": "api.context.invalid_session.error",
+ "translation": "Invalid session err=%v"
+ },
+ {
"id": "api.context.invalid_team_url.debug",
"translation": "團隊網址在不正確的時機被存取。團隊網址不應於在 API 函式或與團隊無關的用途中被使用"
},
{
"id": "api.context.invalid_token.error",
- "translation": "無效的工作階段 token=%v, err=%v"
+ "translation": "Invalid session token={{.Token}}, err={{.Error}}"
},
{
"id": "api.context.invalidate_all_caches",
@@ -1468,26 +1472,10 @@
"translation": "您沒有適當的權限"
},
{
- "id": "api.post.handle_post_events_and_forget.channel.error",
- "translation": "取得頻道時遇到錯誤 channel_id=%s, err=%v"
- },
- {
"id": "api.post.handle_post_events_and_forget.members.error",
"translation": "取得頻道成員失敗 channel_id=%v err=%v"
},
{
- "id": "api.post.handle_post_events_and_forget.profiles.error",
- "translation": "取得使用者資訊失敗 team_id=%v, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.team.error",
- "translation": "取得團隊時遇到錯誤 team_id=%s, err=%v"
- },
- {
- "id": "api.post.handle_post_events_and_forget.user.error",
- "translation": "取得使用者時遇到錯誤 user_id=%s, err=%v"
- },
- {
"id": "api.post.handle_webhook_events_and_forget.create_post.error",
"translation": "建立回應貼文失敗 err=%v"
},
@@ -1496,10 +1484,6 @@
"translation": "POST 事件失敗 err=%s"
},
{
- "id": "api.post.handle_webhook_events_and_forget.getting.error",
- "translation": "團隊取得 Webhook 時遇到錯誤 err=%v"
- },
- {
"id": "api.post.init.debug",
"translation": "正在初始化貼文 API 路徑"
},
@@ -1520,20 +1504,16 @@
"translation": "更新直接訊息頻道的偏好設定失敗 user_id=%v other_user_id=%v err=%v"
},
{
- "id": "api.post.notification.here.warn",
- "translation": "無法寄送 @here 通知給在線使用者 err=%v"
- },
- {
"id": "api.post.notification.member_profile.warn",
"translation": "無法取得頻道成員資訊 user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
- "translation": "正在清除至 %v 的推播通知,channel_id %v"
+ "id": "api.post.send_notifications.user_id.debug",
+ "translation": "Post creator not in channel for the post, no notification sent post_id=%v channel_id=%v user_id=%v"
},
{
- "id": "api.post.send_notifications_and_forget.comment_thread.error",
- "translation": "在發送通知時取得註解文章失敗 root_post_id=%v err=%v"
+ "id": "api.post.send_notifications_and_forget.clear_push_notification.debug",
+ "translation": "正在清除至 %v 的推播通知,channel_id %v"
},
{
"id": "api.post.send_notifications_and_forget.files.error",
@@ -1581,25 +1561,13 @@
},
{
"id": "api.post.send_notifications_and_forget.push_notification.error",
- "translation": "無法發送推播 device_id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.send.error",
- "translation": "提及通知電子郵件傳送失敗 email=%v err=%v"
+ "translation": "Failed to send push device_id={{.DeviceId}}, err={{.Error}}"
},
{
"id": "api.post.send_notifications_and_forget.sent",
"translation": "{{.Prefix}} {{.Filenames}} 已送出"
},
{
- "id": "api.post.send_notifications_and_forget.sessions.error",
- "translation": "於通知中取得工作階段失敗 id=%v, err=%v"
- },
- {
- "id": "api.post.send_notifications_and_forget.user_id.error",
- "translation": "GetProfiles 找不到資料的 user_id 貼文 user_id=%v"
- },
- {
"id": "api.post.update_mention_count_and_forget.update_error",
"translation": "更新提及的計數失敗 post_id=%v channel_id=%v err=%v"
},
@@ -1612,10 +1580,18 @@
"translation": "您沒有適當的權限"
},
{
+ "id": "api.post.update_post.permissions_denied.app_error",
+ "translation": "建立團隊已被停用。請洽詢系統管理員了解詳情。"
+ },
+ {
"id": "api.post.update_post.permissions_details.app_error",
"translation": "已刪除 id={{.PostId}}"
},
{
+ "id": "api.post.update_post.permissions_time_limit.app_error",
+ "translation": "Post edit is only allowed for {{.timeLimit}} seconds. Please ask your systems administrator for details."
+ },
+ {
"id": "api.post.update_post.system_message.app_error",
"translation": "無法更新系統訊息"
},
@@ -2273,7 +2249,7 @@
},
{
"id": "api.user.add_direct_channels_and_forget.failed.error",
- "translation": "為使用者新增直接頻道偏好設定失敗 user_id=%s, team_id=%s, err=%v"
+ "translation": "Failed to add direct channel preferences for user user_id={{.UserId}}, team_id={{.TeamId}}, err={{.Error}}"
},
{
"id": "api.user.authorize_oauth_user.bad_response.app_error",
@@ -2776,10 +2752,50 @@
"translation": "嘗試註冊 WebRTC Token時發生錯誤"
},
{
+ "id": "api.websocket.invalid_session.error",
+ "translation": "Invalid session err=%v"
+ },
+ {
"id": "api.websocket_handler.invalid_param.app_error",
"translation": "無效的參數 {{.Name}}"
},
{
+ "id": "app.channel.post_update_channel_purpose_message.post.error",
+ "translation": "Failed to post channel purpose message"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.removed",
+ "translation": "%s 已移除了頻道標題 (原為: %s)"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.retrieve_user.error",
+ "translation": "嘗試儲存更新的頻道標題訊息 %v 時取得使用者資訊失敗"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_from",
+ "translation": "%s 將原頻道標題由 %s 改為 %s"
+ },
+ {
+ "id": "app.channel.post_update_channel_purpose_message.updated_to",
+ "translation": "%s 已更新頻道標題為:%s"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.description",
+ "translation": "Ability to create new teams"
+ },
+ {
+ "id": "authentication.permissions.create_team_roles.name",
+ "translation": "Create Teams"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.description",
+ "translation": "Ability to change the roles of a team member"
+ },
+ {
+ "id": "authentication.permissions.manage_team_roles.name",
+ "translation": "Manage Team Roles"
+ },
+ {
"id": "authentication.permissions.team_invite_user.description",
"translation": "允許邀請使用者至團隊"
},
diff --git a/manualtesting/manual_testing.go b/manualtesting/manual_testing.go
index 9a2d557bc..30249f995 100644
--- a/manualtesting/manual_testing.go
+++ b/manualtesting/manual_testing.go
@@ -4,16 +4,18 @@
package manualtesting
import (
- l4g "github.com/alecthomas/log4go"
- "github.com/mattermost/platform/api"
- "github.com/mattermost/platform/model"
- "github.com/mattermost/platform/utils"
"hash/fnv"
"math/rand"
"net/http"
"net/url"
"strconv"
"time"
+
+ l4g "github.com/alecthomas/log4go"
+ "github.com/mattermost/platform/api"
+ "github.com/mattermost/platform/app"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/utils"
)
type TestEnvironment struct {
@@ -27,7 +29,7 @@ type TestEnvironment struct {
}
func InitManualTesting() {
- api.Srv.Router.Handle("/manualtest", api.AppHandler(manualTest)).Methods("GET")
+ app.Srv.Router.Handle("/manualtest", api.AppHandler(manualTest)).Methods("GET")
}
func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) {
@@ -70,7 +72,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) {
Type: model.TEAM_OPEN,
}
- if result := <-api.Srv.Store.Team().Save(team); result.Err != nil {
+ if result := <-app.Srv.Store.Team().Save(team); result.Err != nil {
c.Err = result.Err
return
} else {
@@ -78,7 +80,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) {
createdTeam := result.Data.(*model.Team)
channel := &model.Channel{DisplayName: "Town Square", Name: "town-square", Type: model.CHANNEL_OPEN, TeamId: createdTeam.Id}
- if _, err := api.CreateChannel(c, channel, false); err != nil {
+ if _, err := app.CreateChannel(channel, false); err != nil {
c.Err = err
return
}
@@ -98,8 +100,8 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) {
return
}
- <-api.Srv.Store.User().VerifyEmail(result.Data.(*model.User).Id)
- <-api.Srv.Store.Team().SaveMember(&model.TeamMember{TeamId: teamID, UserId: result.Data.(*model.User).Id})
+ <-app.Srv.Store.User().VerifyEmail(result.Data.(*model.User).Id)
+ <-app.Srv.Store.Team().SaveMember(&model.TeamMember{TeamId: teamID, UserId: result.Data.(*model.User).Id})
newuser := result.Data.(*model.User)
userID = newuser.Id
@@ -153,7 +155,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) {
func getChannelID(channelname string, teamid string, userid string) (id string, err bool) {
// Grab all the channels
- result := <-api.Srv.Store.Channel().GetChannels(teamid, userid)
+ result := <-app.Srv.Store.Channel().GetChannels(teamid, userid)
if result.Err != nil {
l4g.Debug(utils.T("manaultesting.get_channel_id.unable.debug"))
return "", false
diff --git a/model/authorization.go b/model/authorization.go
index 75aebf55c..56bb58913 100644
--- a/model/authorization.go
+++ b/model/authorization.go
@@ -27,6 +27,8 @@ var PERMISSION_MANAGE_PUBLIC_CHANNEL_MEMBERS *Permission
var PERMISSION_MANAGE_PRIVATE_CHANNEL_MEMBERS *Permission
var PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE *Permission
var PERMISSION_MANAGE_ROLES *Permission
+var PERMISSION_MANAGE_TEAM_ROLES *Permission
+var PERMISSION_MANAGE_CHANNEL_ROLES *Permission
var PERMISSION_CREATE_DIRECT_CHANNEL *Permission
var PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES *Permission
var PERMISSION_MANAGE_PRIVATE_CHANNEL_PROPERTIES *Permission
@@ -46,7 +48,10 @@ var PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH *Permission
var PERMISSION_CREATE_POST *Permission
var PERMISSION_EDIT_POST *Permission
var PERMISSION_EDIT_OTHERS_POSTS *Permission
+var PERMISSION_DELETE_POST *Permission
+var PERMISSION_DELETE_OTHERS_POSTS *Permission
var PERMISSION_REMOVE_USER_FROM_TEAM *Permission
+var PERMISSION_CREATE_TEAM *Permission
var PERMISSION_MANAGE_TEAM *Permission
var PERMISSION_IMPORT_TEAM *Permission
@@ -123,6 +128,16 @@ func InitalizePermissions() {
"authentication.permissions.manage_roles.name",
"authentication.permissions.manage_roles.description",
}
+ PERMISSION_MANAGE_TEAM_ROLES = &Permission{
+ "manage_team_roles",
+ "authentication.permissions.manage_team_roles.name",
+ "authentication.permissions.manage_team_roles.description",
+ }
+ PERMISSION_MANAGE_CHANNEL_ROLES = &Permission{
+ "manage_channel_roles",
+ "authentication.permissions.manage_channel_roles.name",
+ "authentication.permissions.manage_channel_roles.description",
+ }
PERMISSION_MANAGE_SYSTEM = &Permission{
"manage_system",
"authentication.permissions.manage_system.name",
@@ -223,11 +238,26 @@ func InitalizePermissions() {
"authentication.permissions.edit_others_posts.name",
"authentication.permissions.edit_others_posts.description",
}
+ PERMISSION_DELETE_POST = &Permission{
+ "delete_post",
+ "authentication.permissions.delete_post.name",
+ "authentication.permissions.delete_post.description",
+ }
+ PERMISSION_DELETE_OTHERS_POSTS = &Permission{
+ "delete_others_posts",
+ "authentication.permissions.delete_others_posts.name",
+ "authentication.permissions.delete_others_posts.description",
+ }
PERMISSION_REMOVE_USER_FROM_TEAM = &Permission{
"remove_user_from_team",
"authentication.permissions.remove_user_from_team.name",
"authentication.permissions.remove_user_from_team.description",
}
+ PERMISSION_CREATE_TEAM = &Permission{
+ "create_team",
+ "authentication.permissions.create_team.name",
+ "authentication.permissions.create_team.description",
+ }
PERMISSION_MANAGE_TEAM = &Permission{
"manage_team",
"authentication.permissions.manage_team.name",
@@ -264,7 +294,9 @@ func InitalizeRoles() {
"channel_admin",
"authentication.roles.channel_admin.name",
"authentication.roles.channel_admin.description",
- []string{},
+ []string{
+ PERMISSION_MANAGE_CHANNEL_ROLES.Id,
+ },
}
BuiltInRoles[ROLE_CHANNEL_ADMIN.Id] = ROLE_CHANNEL_ADMIN
ROLE_CHANNEL_GUEST = &Role{
@@ -295,7 +327,8 @@ func InitalizeRoles() {
PERMISSION_REMOVE_USER_FROM_TEAM.Id,
PERMISSION_MANAGE_TEAM.Id,
PERMISSION_IMPORT_TEAM.Id,
- PERMISSION_MANAGE_ROLES.Id,
+ PERMISSION_MANAGE_TEAM_ROLES.Id,
+ PERMISSION_MANAGE_CHANNEL_ROLES.Id,
PERMISSION_MANAGE_OTHERS_WEBHOOKS.Id,
PERMISSION_MANAGE_SLASH_COMMANDS.Id,
PERMISSION_MANAGE_OTHERS_SLASH_COMMANDS.Id,
@@ -329,6 +362,7 @@ func InitalizeRoles() {
[]string{
PERMISSION_ASSIGN_SYSTEM_ADMIN_ROLE.Id,
PERMISSION_MANAGE_SYSTEM.Id,
+ PERMISSION_MANAGE_ROLES.Id,
PERMISSION_MANAGE_PUBLIC_CHANNEL_PROPERTIES.Id,
PERMISSION_DELETE_PUBLIC_CHANNEL.Id,
PERMISSION_CREATE_PUBLIC_CHANNEL.Id,
@@ -340,6 +374,9 @@ func InitalizeRoles() {
PERMISSION_EDIT_OTHER_USERS.Id,
PERMISSION_MANAGE_OAUTH.Id,
PERMISSION_INVITE_USER.Id,
+ PERMISSION_DELETE_POST.Id,
+ PERMISSION_DELETE_OTHERS_POSTS.Id,
+ PERMISSION_CREATE_TEAM.Id,
},
ROLE_TEAM_USER.Permissions...,
),
diff --git a/model/client.go b/model/client.go
index 540bc747f..c75121e97 100644
--- a/model/client.go
+++ b/model/client.go
@@ -1471,6 +1471,21 @@ func (c *Client) GetPostById(postId string, etag string) (*PostList, *ResponseMe
}
}
+// GetPermalink returns a post list, based on the provided channel and post ID.
+func (c *Client) GetPermalink(channelId string, postId string, etag string) (*PostList, *ResponseMetadata) {
+ if r, err := c.DoApiGet(c.GetTeamRoute()+fmt.Sprintf("/pltmp/%v", postId), "", etag); err != nil {
+ return nil, &ResponseMetadata{StatusCode: r.StatusCode, Error: err}
+ } else {
+ defer closeBody(r)
+ return PostListFromJson(r.Body),
+ &ResponseMetadata{
+ StatusCode: r.StatusCode,
+ RequestId: r.Header.Get(HEADER_REQUEST_ID),
+ Etag: r.Header.Get(HEADER_ETAG_SERVER),
+ }
+ }
+}
+
func (c *Client) DeletePost(channelId string, postId string) (*Result, *AppError) {
if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+fmt.Sprintf("/posts/%v/delete", postId), ""); err != nil {
return nil, err
@@ -2319,3 +2334,26 @@ func (c *Client) ListReactions(channelId string, postId string) ([]*Reaction, *A
return ReactionsFromJson(r.Body), nil
}
}
+
+// Updates the user's roles in the channel by replacing them with the roles provided.
+func (c *Client) UpdateChannelRoles(channelId string, userId string, roles string) (map[string]string, *ResponseMetadata) {
+ data := make(map[string]string)
+ data["new_roles"] = roles
+ data["user_id"] = userId
+
+ if r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/update_member_roles", MapToJson(data)); err != nil {
+ metadata := ResponseMetadata{Error: err}
+ if r != nil {
+ metadata.StatusCode = r.StatusCode
+ }
+ return nil, &metadata
+ } else {
+ defer closeBody(r)
+ return MapFromJson(r.Body),
+ &ResponseMetadata{
+ StatusCode: r.StatusCode,
+ RequestId: r.Header.Get(HEADER_REQUEST_ID),
+ Etag: r.Header.Get(HEADER_ETAG_SERVER),
+ }
+ }
+}
diff --git a/model/config.go b/model/config.go
index 0134e1a34..13e795170 100644
--- a/model/config.go
+++ b/model/config.go
@@ -49,6 +49,14 @@ const (
RESTRICT_EMOJI_CREATION_ADMIN = "admin"
RESTRICT_EMOJI_CREATION_SYSTEM_ADMIN = "system_admin"
+ PERMISSIONS_DELETE_POST_ALL = "all"
+ PERMISSIONS_DELETE_POST_TEAM_ADMIN = "team_admin"
+ PERMISSIONS_DELETE_POST_SYSTEM_ADMIN = "system_admin"
+
+ ALLOW_EDIT_POST_ALWAYS = "always"
+ ALLOW_EDIT_POST_NEVER = "never"
+ ALLOW_EDIT_POST_TIME_LIMIT = "time_limit"
+
EMAIL_BATCHING_BUFFER_SIZE = 256
EMAIL_BATCHING_INTERVAL = 30
@@ -92,6 +100,9 @@ type ServiceSettings struct {
WebserverMode *string
EnableCustomEmoji *bool
RestrictCustomEmojiCreation *string
+ RestrictPostDelete *string
+ AllowEditPost *string
+ PostEditTimeLimit *int
}
type ClusterSettings struct {
@@ -827,6 +838,21 @@ func (o *Config) SetDefaults() {
*o.ServiceSettings.RestrictCustomEmojiCreation = RESTRICT_EMOJI_CREATION_ALL
}
+ if o.ServiceSettings.RestrictPostDelete == nil {
+ o.ServiceSettings.RestrictPostDelete = new(string)
+ *o.ServiceSettings.RestrictPostDelete = PERMISSIONS_DELETE_POST_ALL
+ }
+
+ if o.ServiceSettings.AllowEditPost == nil {
+ o.ServiceSettings.AllowEditPost = new(string)
+ *o.ServiceSettings.AllowEditPost = ALLOW_EDIT_POST_TIME_LIMIT
+ }
+
+ if o.ServiceSettings.PostEditTimeLimit == nil {
+ o.ServiceSettings.PostEditTimeLimit = new(int)
+ *o.ServiceSettings.PostEditTimeLimit = 300
+ }
+
if o.ClusterSettings.InterNodeListenAddress == nil {
o.ClusterSettings.InterNodeListenAddress = new(string)
*o.ClusterSettings.InterNodeListenAddress = ":8075"
diff --git a/model/file.go b/model/file.go
index c218c4246..20f6236de 100644
--- a/model/file.go
+++ b/model/file.go
@@ -8,6 +8,10 @@ import (
"io"
)
+const (
+ MaxImageSize = 6048 * 4032 // 24 megapixels, roughly 36MB as a raw image
+)
+
var (
IMAGE_EXTENSIONS = [5]string{".jpg", ".jpeg", ".gif", ".bmp", ".png"}
IMAGE_MIME_TYPES = map[string]string{".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".bmp": "image/bmp", ".png": "image/png", ".tiff": "image/tiff"}
diff --git a/model/post.go b/model/post.go
index 7097e031d..668c4db61 100644
--- a/model/post.go
+++ b/model/post.go
@@ -18,6 +18,7 @@ const (
POST_ADD_REMOVE = "system_add_remove"
POST_HEADER_CHANGE = "system_header_change"
POST_DISPLAYNAME_CHANGE = "system_displayname_change"
+ POST_PURPOSE_CHANGE = "system_purpose_change"
POST_CHANNEL_DELETED = "system_channel_deleted"
POST_EPHEMERAL = "system_ephemeral"
POST_FILEIDS_MAX_RUNES = 150
@@ -31,6 +32,7 @@ type Post struct {
Id string `json:"id"`
CreateAt int64 `json:"create_at"`
UpdateAt int64 `json:"update_at"`
+ EditAt int64 `json:"edit_at"`
DeleteAt int64 `json:"delete_at"`
UserId string `json:"user_id"`
ChannelId string `json:"channel_id"`
@@ -119,7 +121,7 @@ func (o *Post) IsValid() *AppError {
// should be removed once more message types are supported
if !(o.Type == POST_DEFAULT || o.Type == POST_JOIN_LEAVE || o.Type == POST_ADD_REMOVE ||
- o.Type == POST_SLACK_ATTACHMENT || o.Type == POST_HEADER_CHANGE ||
+ o.Type == POST_SLACK_ATTACHMENT || o.Type == POST_HEADER_CHANGE || o.Type == POST_PURPOSE_CHANGE ||
o.Type == POST_DISPLAYNAME_CHANGE || o.Type == POST_CHANNEL_DELETED) {
return NewLocAppError("Post.IsValid", "model.post.is_valid.type.app_error", nil, "id="+o.Type)
}
diff --git a/model/team.go b/model/team.go
index 3f05ce83a..195bac571 100644
--- a/model/team.go
+++ b/model/team.go
@@ -48,6 +48,14 @@ func InvitesFromJson(data io.Reader) *Invites {
}
}
+func (o *Invites) ToEmailList() []string {
+ emailList := make([]string, len(o.Invites))
+ for _, invite := range o.Invites {
+ emailList = append(emailList, invite["email"])
+ }
+ return emailList
+}
+
func (o *Invites) ToJson() string {
b, err := json.Marshal(o)
if err != nil {
diff --git a/model/user.go b/model/user.go
index 76c3772cb..876ba70e7 100644
--- a/model/user.go
+++ b/model/user.go
@@ -376,6 +376,13 @@ func IsInRole(userRoles string, inRole string) bool {
return false
}
+func (u *User) IsSSOUser() bool {
+ if u.AuthService != "" && u.AuthService != USER_AUTH_SERVICE_EMAIL {
+ return true
+ }
+ return false
+}
+
func (u *User) IsOAuthUser() bool {
if u.AuthService == USER_AUTH_SERVICE_GITLAB {
return true
diff --git a/model/websocket_client.go b/model/websocket_client.go
index 453ae49b7..c91855134 100644
--- a/model/websocket_client.go
+++ b/model/websocket_client.go
@@ -8,6 +8,10 @@ import (
"github.com/gorilla/websocket"
)
+const (
+ SOCKET_MAX_MESSAGE_SIZE_KB = 8 * 1024 // 8KB
+)
+
type WebSocketClient struct {
Url string // The location of the server like "ws://localhost:8065"
ApiUrl string // The api location of the server like "ws://localhost:8065/api/v3"
diff --git a/store/sql_channel_store.go b/store/sql_channel_store.go
index 2ab9d87c1..e3df07f74 100644
--- a/store/sql_channel_store.go
+++ b/store/sql_channel_store.go
@@ -579,6 +579,31 @@ func (s SqlChannelStore) getByName(teamId string, name string, includeDeleted bo
return storeChannel
}
+func (s SqlChannelStore) GetDeletedByName(teamId string, name string) StoreChannel {
+ storeChannel := make(StoreChannel, 1)
+
+ go func() {
+ result := StoreResult{}
+
+ channel := model.Channel{}
+
+ if err := s.GetReplica().SelectOne(&channel, "SELECT * FROM Channels WHERE (TeamId = :TeamId OR TeamId = '') AND Name = :Name AND DeleteAt != 0", map[string]interface{}{"TeamId": teamId, "Name": name}); err != nil {
+ if err == sql.ErrNoRows {
+ result.Err = model.NewLocAppError("SqlChannelStore.GetDeletedByName", "store.sql_channel.get_deleted_by_name.missing.app_error", nil, "teamId="+teamId+", "+"name="+name+", "+err.Error())
+ } else {
+ result.Err = model.NewLocAppError("SqlChannelStore.GetDeletedByName", "store.sql_channel.get_deleted_by_name.existing.app_error", nil, "teamId="+teamId+", "+"name="+name+", "+err.Error())
+ }
+ } else {
+ result.Data = &channel
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (s SqlChannelStore) SaveMember(member *model.ChannelMember) StoreChannel {
storeChannel := make(StoreChannel, 1)
@@ -702,7 +727,7 @@ func (s SqlChannelStore) GetMember(channelId string, userId string) StoreChannel
result.Err = model.NewLocAppError("SqlChannelStore.GetMember", "store.sql_channel.get_member.app_error", nil, "channel_id="+channelId+"user_id="+userId+","+err.Error())
}
} else {
- result.Data = member
+ result.Data = &member
}
storeChannel <- result
@@ -1292,7 +1317,7 @@ func (s SqlChannelStore) performSearch(searchQuery string, term string, paramete
result := StoreResult{}
// these chars have special meaning and can be treated as spaces
- for _, c := range specialSearchChar {
+ for _, c := range specialUserSearchChar {
term = strings.Replace(term, c, " ", -1)
}
@@ -1361,7 +1386,7 @@ func (s SqlChannelStore) GetMembersByIds(channelId string, userIds []string) Sto
if _, err := s.GetReplica().Select(&members, "SELECT * FROM ChannelMembers WHERE ChannelId = :ChannelId AND UserId IN ("+idQuery+")", props); err != nil {
result.Err = model.NewLocAppError("SqlChannelStore.GetMembersByIds", "store.sql_channel.get_members_by_ids.app_error", nil, "channelId="+channelId+" "+err.Error())
} else {
- result.Data = members
+ result.Data = &members
}
storeChannel <- result
diff --git a/store/sql_channel_store_test.go b/store/sql_channel_store_test.go
index 6186a242c..5202a7c29 100644
--- a/store/sql_channel_store_test.go
+++ b/store/sql_channel_store_test.go
@@ -352,7 +352,8 @@ func TestChannelStoreGetByName(t *testing.T) {
o1.Type = model.CHANNEL_OPEN
Must(store.Channel().Save(&o1))
- if r1 := <-store.Channel().GetByName(o1.TeamId, o1.Name); r1.Err != nil {
+ r1 := <-store.Channel().GetByName(o1.TeamId, o1.Name)
+ if r1.Err != nil {
t.Fatal(r1.Err)
} else {
if r1.Data.(*model.Channel).ToJson() != o1.ToJson() {
@@ -363,6 +364,36 @@ func TestChannelStoreGetByName(t *testing.T) {
if err := (<-store.Channel().GetByName(o1.TeamId, "")).Err; err == nil {
t.Fatal("Missing id should have failed")
}
+
+ Must(store.Channel().Delete(r1.Data.(*model.Channel).Id, model.GetMillis()))
+
+ if err := (<-store.Channel().GetByName(o1.TeamId, "")).Err; err == nil {
+ t.Fatal("Deleted channel should not be returned by GetByName()")
+ }
+}
+
+func TestChannelStoreGetDeletedByName(t *testing.T) {
+ Setup()
+
+ o1 := model.Channel{}
+ o1.TeamId = model.NewId()
+ o1.DisplayName = "Name"
+ o1.Name = "a" + model.NewId() + "b"
+ o1.Type = model.CHANNEL_OPEN
+ o1.DeleteAt = model.GetMillis()
+ Must(store.Channel().Save(&o1))
+
+ if r1 := <-store.Channel().GetDeletedByName(o1.TeamId, o1.Name); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.Channel).ToJson() != o1.ToJson() {
+ t.Fatal("invalid returned channel")
+ }
+ }
+
+ if err := (<-store.Channel().GetDeletedByName(o1.TeamId, "")).Err; err == nil {
+ t.Fatal("Missing id should have failed")
+ }
}
func TestChannelMemberStore(t *testing.T) {
@@ -446,7 +477,7 @@ func TestChannelMemberStore(t *testing.T) {
t.Fatal("Member update time incorrect on delete")
}
- member := (<-store.Channel().GetMember(o1.ChannelId, o1.UserId)).Data.(model.ChannelMember)
+ member := (<-store.Channel().GetMember(o1.ChannelId, o1.UserId)).Data.(*model.ChannelMember)
if member.ChannelId != o1.ChannelId {
t.Fatal("should have go member")
}
@@ -918,7 +949,7 @@ func TestGetMember(t *testing.T) {
if result := <-store.Channel().GetMember(c1.Id, userId); result.Err != nil {
t.Fatal("shouldn't have errored when getting member", result.Err)
- } else if member := result.Data.(model.ChannelMember); member.ChannelId != c1.Id {
+ } else if member := result.Data.(*model.ChannelMember); member.ChannelId != c1.Id {
t.Fatal("should've gotten member of channel 1")
} else if member.UserId != userId {
t.Fatal("should've gotten member for user")
@@ -926,7 +957,7 @@ func TestGetMember(t *testing.T) {
if result := <-store.Channel().GetMember(c2.Id, userId); result.Err != nil {
t.Fatal("shouldn't have errored when getting member", result.Err)
- } else if member := result.Data.(model.ChannelMember); member.ChannelId != c2.Id {
+ } else if member := result.Data.(*model.ChannelMember); member.ChannelId != c2.Id {
t.Fatal("should've gotten member of channel 2")
} else if member.UserId != userId {
t.Fatal("should've gotten member for user")
@@ -1313,7 +1344,7 @@ func TestChannelStoreGetMembersByIds(t *testing.T) {
if r := <-store.Channel().GetMembersByIds(m1.ChannelId, []string{m1.UserId}); r.Err != nil {
t.Fatal(r.Err)
} else {
- rm1 := r.Data.(model.ChannelMembers)[0]
+ rm1 := (*r.Data.(*model.ChannelMembers))[0]
if rm1.ChannelId != m1.ChannelId {
t.Fatal("bad team id")
@@ -1330,7 +1361,7 @@ func TestChannelStoreGetMembersByIds(t *testing.T) {
if r := <-store.Channel().GetMembersByIds(m1.ChannelId, []string{m1.UserId, m2.UserId, model.NewId()}); r.Err != nil {
t.Fatal(r.Err)
} else {
- rm := r.Data.(model.ChannelMembers)
+ rm := (*r.Data.(*model.ChannelMembers))
if len(rm) != 2 {
t.Fatal("return wrong number of results")
diff --git a/store/sql_post_store.go b/store/sql_post_store.go
index c1aaee3e6..ed87b01f7 100644
--- a/store/sql_post_store.go
+++ b/store/sql_post_store.go
@@ -222,6 +222,27 @@ func (s SqlPostStore) Get(id string) StoreChannel {
return storeChannel
}
+func (s SqlPostStore) GetSingle(id string) StoreChannel {
+ storeChannel := make(StoreChannel, 1)
+
+ go func() {
+ result := StoreResult{}
+
+ var post model.Post
+ err := s.GetReplica().SelectOne(&post, "SELECT * FROM Posts WHERE Id = :Id AND DeleteAt = 0", map[string]interface{}{"Id": id})
+ if err != nil {
+ result.Err = model.NewLocAppError("SqlPostStore.GetSingle", "store.sql_post.get.app_error", nil, "id="+id+err.Error())
+ }
+
+ result.Data = &post
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
type etagPosts struct {
Id string
UpdateAt int64
diff --git a/store/sql_post_store_test.go b/store/sql_post_store_test.go
index eb22979db..e3886c6bc 100644
--- a/store/sql_post_store_test.go
+++ b/store/sql_post_store_test.go
@@ -63,6 +63,29 @@ func TestPostStoreGet(t *testing.T) {
}
}
+func TestPostStoreGetSingle(t *testing.T) {
+ Setup()
+
+ o1 := &model.Post{}
+ o1.ChannelId = model.NewId()
+ o1.UserId = model.NewId()
+ o1.Message = "a" + model.NewId() + "b"
+
+ o1 = (<-store.Post().Save(o1)).Data.(*model.Post)
+
+ if r1 := <-store.Post().GetSingle(o1.Id); r1.Err != nil {
+ t.Fatal(r1.Err)
+ } else {
+ if r1.Data.(*model.Post).CreateAt != o1.CreateAt {
+ t.Fatal("invalid returned post")
+ }
+ }
+
+ if err := (<-store.Post().GetSingle("123")).Err; err == nil {
+ t.Fatal("Missing id should have failed")
+ }
+}
+
func TestGetEtagCache(t *testing.T) {
Setup()
o1 := &model.Post{}
diff --git a/store/sql_preference_store.go b/store/sql_preference_store.go
index 5c46d1328..14a9ff48b 100644
--- a/store/sql_preference_store.go
+++ b/store/sql_preference_store.go
@@ -348,3 +348,25 @@ func (s SqlPreferenceStore) DeleteCategory(userId string, category string) Store
return storeChannel
}
+
+func (s SqlPreferenceStore) DeleteCategoryAndName(category string, name string) StoreChannel {
+ storeChannel := make(StoreChannel, 1)
+
+ go func() {
+ result := StoreResult{}
+
+ if _, err := s.GetMaster().Exec(
+ `DELETE FROM
+ Preferences
+ WHERE
+ Name = :Name
+ AND Category = :Category`, map[string]interface{}{"Name": name, "Category": category}); err != nil {
+ result.Err = model.NewLocAppError("SqlPreferenceStore.DeleteCategoryAndName", "store.sql_preference.delete.app_error", nil, err.Error())
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
diff --git a/store/sql_preference_store_test.go b/store/sql_preference_store_test.go
index fc1cf5f5b..adcaa8d89 100644
--- a/store/sql_preference_store_test.go
+++ b/store/sql_preference_store_test.go
@@ -427,3 +427,48 @@ func TestPreferenceDeleteCategory(t *testing.T) {
t.Fatal("should've returned no preferences")
}
}
+
+func TestPreferenceDeleteCategoryAndName(t *testing.T) {
+ Setup()
+
+ category := model.NewId()
+ name := model.NewId()
+ userId := model.NewId()
+ userId2 := model.NewId()
+
+ preference1 := model.Preference{
+ UserId: userId,
+ Category: category,
+ Name: name,
+ Value: "value1a",
+ }
+
+ preference2 := model.Preference{
+ UserId: userId2,
+ Category: category,
+ Name: name,
+ Value: "value1a",
+ }
+
+ Must(store.Preference().Save(&model.Preferences{preference1, preference2}))
+
+ if prefs := Must(store.Preference().GetAll(userId)).(model.Preferences); len([]model.Preference(prefs)) != 1 {
+ t.Fatal("should've returned 1 preference")
+ }
+
+ if prefs := Must(store.Preference().GetAll(userId2)).(model.Preferences); len([]model.Preference(prefs)) != 1 {
+ t.Fatal("should've returned 1 preference")
+ }
+
+ if result := <-store.Preference().DeleteCategoryAndName(category, name); result.Err != nil {
+ t.Fatal(result.Err)
+ }
+
+ if prefs := Must(store.Preference().GetAll(userId)).(model.Preferences); len([]model.Preference(prefs)) != 0 {
+ t.Fatal("should've returned no preferences")
+ }
+
+ if prefs := Must(store.Preference().GetAll(userId2)).(model.Preferences); len([]model.Preference(prefs)) != 0 {
+ t.Fatal("should've returned no preferences")
+ }
+}
diff --git a/store/sql_team_store.go b/store/sql_team_store.go
index 85a2d995e..6c1cdcad7 100644
--- a/store/sql_team_store.go
+++ b/store/sql_team_store.go
@@ -301,7 +301,7 @@ func (s SqlTeamStore) GetAllTeamListing() StoreChannel {
var data []*model.Team
if _, err := s.GetReplica().Select(&data, query); err != nil {
- result.Err = model.NewLocAppError("SqlTeamStore.GetAllTeams", "store.sql_team.get_all_team_listing.app_error", nil, err.Error())
+ result.Err = model.NewLocAppError("SqlTeamStore.GetAllTeamListing", "store.sql_team.get_all_team_listing.app_error", nil, err.Error())
}
for _, team := range data {
@@ -438,7 +438,7 @@ func (s SqlTeamStore) GetMember(teamId string, userId string) StoreChannel {
result.Err = model.NewLocAppError("SqlTeamStore.GetMember", "store.sql_team.get_member.app_error", nil, "teamId="+teamId+" userId="+userId+" "+err.Error())
}
} else {
- result.Data = member
+ result.Data = &member
}
storeChannel <- result
diff --git a/store/sql_team_store_test.go b/store/sql_team_store_test.go
index 8a302a9e2..0ea499d7e 100644
--- a/store/sql_team_store_test.go
+++ b/store/sql_team_store_test.go
@@ -415,7 +415,7 @@ func TestGetTeamMember(t *testing.T) {
if r := <-store.Team().GetMember(m1.TeamId, m1.UserId); r.Err != nil {
t.Fatal(r.Err)
} else {
- rm1 := r.Data.(model.TeamMember)
+ rm1 := r.Data.(*model.TeamMember)
if rm1.TeamId != m1.TeamId {
t.Fatal("bad team id")
diff --git a/store/sql_upgrade.go b/store/sql_upgrade.go
index d69404baa..9ee8f08e2 100644
--- a/store/sql_upgrade.go
+++ b/store/sql_upgrade.go
@@ -15,6 +15,7 @@ import (
)
const (
+ VERIONS_3_7_0 = "3.7.0"
VERSION_3_6_0 = "3.6.0"
VERSION_3_5_0 = "3.5.0"
VERSION_3_4_0 = "3.4.0"
@@ -39,6 +40,7 @@ func UpgradeDatabase(sqlStore *SqlStore) {
UpgradeDatabaseToVersion34(sqlStore)
UpgradeDatabaseToVersion35(sqlStore)
UpgradeDatabaseToVersion36(sqlStore)
+ UpgradeDatabaseToVersion37(sqlStore)
// If the SchemaVersion is empty this this is the first time it has ran
// so lets set it to the current version.
@@ -229,3 +231,11 @@ func UpgradeDatabaseToVersion36(sqlStore *SqlStore) {
saveSchemaVersion(sqlStore, VERSION_3_6_0)
}
}
+
+func UpgradeDatabaseToVersion37(sqlStore *SqlStore) {
+ // TODO: Uncomment following condition when version 3.7.0 is released
+ // if shouldPerformUpgrade(sqlStore, VERSION_3_6_0, VERSION_3_7_0) {
+ // Add EditAt column to Posts
+ sqlStore.CreateColumnIfNotExists("Posts", "EditAt", " bigint", " bigint", "0")
+ // }
+}
diff --git a/store/sql_user_store.go b/store/sql_user_store.go
index 533757479..09742a4f4 100644
--- a/store/sql_user_store.go
+++ b/store/sql_user_store.go
@@ -1129,6 +1129,31 @@ func (us SqlUserStore) AnalyticsUniqueUserCount(teamId string) StoreChannel {
return storeChannel
}
+func (us SqlUserStore) AnalyticsActiveCount(timePeriod int64) StoreChannel {
+
+ storeChannel := make(StoreChannel, 1)
+
+ go func() {
+ result := StoreResult{}
+
+ time := model.GetMillis() - timePeriod
+
+ query := "SELECT COUNT(*) FROM Status WHERE LastActivityAt > :Time"
+
+ v, err := us.GetReplica().SelectInt(query, map[string]interface{}{"Time": time})
+ if err != nil {
+ result.Err = model.NewLocAppError("SqlUserStore.AnalyticsDailyActiveUsers", "store.sql_user.analytics_daily_active_users.app_error", nil, err.Error())
+ } else {
+ result.Data = v
+ }
+
+ storeChannel <- result
+ close(storeChannel)
+ }()
+
+ return storeChannel
+}
+
func (us SqlUserStore) GetUnreadCount(userId string) StoreChannel {
storeChannel := make(StoreChannel, 1)
diff --git a/store/store.go b/store/store.go
index 88a553b7c..cd918c033 100644
--- a/store/store.go
+++ b/store/store.go
@@ -93,6 +93,7 @@ type ChannelStore interface {
PermanentDeleteByTeam(teamId string) StoreChannel
GetByName(team_id string, name string) StoreChannel
GetByNameIncludeDeleted(team_id string, name string) StoreChannel
+ GetDeletedByName(team_id string, name string) StoreChannel
GetChannels(teamId string, userId string) StoreChannel
GetMoreChannels(teamId string, userId string, offset int, limit int) StoreChannel
GetChannelCounts(teamId string, userId string) StoreChannel
@@ -127,6 +128,7 @@ type PostStore interface {
Save(post *model.Post) StoreChannel
Update(newPost *model.Post, oldPost *model.Post) StoreChannel
Get(id string) StoreChannel
+ GetSingle(id string) StoreChannel
Delete(postId string, time int64) StoreChannel
PermanentDeleteByUser(userId string) StoreChannel
GetPosts(channelId string, offset int, limit int, allowFromCache bool) StoreChannel
@@ -175,6 +177,7 @@ type UserStore interface {
GetSystemAdminProfiles() StoreChannel
PermanentDelete(userId string) StoreChannel
AnalyticsUniqueUserCount(teamId string) StoreChannel
+ AnalyticsActiveCount(time int64) StoreChannel
GetUnreadCount(userId string) StoreChannel
GetUnreadCountForChannel(userId string, channelId string) StoreChannel
GetRecentlyActiveUsersForTeam(teamId string) StoreChannel
@@ -275,6 +278,7 @@ type PreferenceStore interface {
GetAll(userId string) StoreChannel
Delete(userId, category, name string) StoreChannel
DeleteCategory(userId string, category string) StoreChannel
+ DeleteCategoryAndName(category string, name string) StoreChannel
PermanentDeleteByUser(userId string) StoreChannel
IsFeatureEnabled(feature, userId string) StoreChannel
}
diff --git a/tests/test-markdown-lists.md b/tests/test-markdown-lists.md
index e0640a55f..b99c940f2 100644
--- a/tests/test-markdown-lists.md
+++ b/tests/test-markdown-lists.md
@@ -242,3 +242,32 @@ List A:
1. One
List B:
2. Two
+
+### Lists with blank lines before and after
+
+**Expected:**
+
+```
+Line with blank line after
+
+Line with blank line after and before
+
+1. Bullet
+2. Bullet
+3. Bullet
+
+Line with blank line after and before
+
+Line with blank line before
+```
+Line with blank line after
+
+Line with blank line after and before
+
+1. Bullet
+2. Bullet
+3. Bullet
+
+Line with blank line after and before
+
+Line with blank line before
diff --git a/utils/authorization.go b/utils/authorization.go
index 533808467..de288fc81 100644
--- a/utils/authorization.go
+++ b/utils/authorization.go
@@ -3,7 +3,9 @@
package utils
-import "github.com/mattermost/platform/model"
+import (
+ "github.com/mattermost/platform/model"
+)
func SetDefaultRolesBasedOnConfig() {
// Reset the roles to default to make this logic easier
@@ -148,4 +150,33 @@ func SetDefaultRolesBasedOnConfig() {
model.PERMISSION_INVITE_USER.Id,
)
}
+
+ switch *Cfg.ServiceSettings.RestrictPostDelete {
+ case model.PERMISSIONS_DELETE_POST_ALL:
+ model.ROLE_CHANNEL_USER.Permissions = append(
+ model.ROLE_CHANNEL_USER.Permissions,
+ model.PERMISSION_DELETE_POST.Id,
+ )
+ model.ROLE_TEAM_ADMIN.Permissions = append(
+ model.ROLE_TEAM_ADMIN.Permissions,
+ model.PERMISSION_DELETE_POST.Id,
+ model.PERMISSION_DELETE_OTHERS_POSTS.Id,
+ )
+ break
+ case model.PERMISSIONS_DELETE_POST_TEAM_ADMIN:
+ model.ROLE_TEAM_ADMIN.Permissions = append(
+ model.ROLE_TEAM_ADMIN.Permissions,
+ model.PERMISSION_DELETE_POST.Id,
+ model.PERMISSION_DELETE_OTHERS_POSTS.Id,
+ )
+ break
+ }
+
+ if Cfg.TeamSettings.EnableTeamCreation {
+ model.ROLE_SYSTEM_USER.Permissions = append(
+ model.ROLE_SYSTEM_USER.Permissions,
+ model.PERMISSION_CREATE_TEAM.Id,
+ )
+ }
+
}
diff --git a/utils/config.go b/utils/config.go
index ab149d55f..72c30c01b 100644
--- a/utils/config.go
+++ b/utils/config.go
@@ -32,6 +32,15 @@ var CfgHash = ""
var CfgFileName string = ""
var ClientCfg map[string]string = map[string]string{}
var originalDisableDebugLvl l4g.Level = l4g.DEBUG
+var siteURL = ""
+
+func GetSiteURL() string {
+ return siteURL
+}
+
+func SetSiteURL(url string) {
+ siteURL = strings.TrimRight(url, "/")
+}
func FindConfigFile(fileName string) string {
if _, err := os.Stat("./config/" + fileName); err == nil {
@@ -215,6 +224,7 @@ func LoadConfig(fileName string) {
}
SetDefaultRolesBasedOnConfig()
+ SetSiteURL(*Cfg.ServiceSettings.SiteURL)
}
func RegenerateClientConfig() {
@@ -257,6 +267,9 @@ func getClientConfig(c *model.Config) map[string]string {
props["EnableTesting"] = strconv.FormatBool(c.ServiceSettings.EnableTesting)
props["EnableDeveloper"] = strconv.FormatBool(*c.ServiceSettings.EnableDeveloper)
props["EnableDiagnostics"] = strconv.FormatBool(*c.LogSettings.EnableDiagnostics)
+ props["RestrictPostDelete"] = *c.ServiceSettings.RestrictPostDelete
+ props["AllowEditPost"] = *c.ServiceSettings.AllowEditPost
+ props["PostEditTimeLimit"] = fmt.Sprintf("%v", *c.ServiceSettings.PostEditTimeLimit)
props["SendEmailNotifications"] = strconv.FormatBool(c.EmailSettings.SendEmailNotifications)
props["SendPushNotifications"] = strconv.FormatBool(*c.EmailSettings.SendPushNotifications)
diff --git a/utils/diagnostic.go b/utils/diagnostic.go
index 7509ccbb5..525dfd794 100644
--- a/utils/diagnostic.go
+++ b/utils/diagnostic.go
@@ -82,6 +82,9 @@ func trackConfig() {
"restrict_custom_emoji_creation": *Cfg.ServiceSettings.RestrictCustomEmojiCreation,
"enable_testing": Cfg.ServiceSettings.EnableTesting,
"enable_developer": *Cfg.ServiceSettings.EnableDeveloper,
+ "restrict_post_delete": *Cfg.ServiceSettings.RestrictPostDelete,
+ "allow_edit_post": *Cfg.ServiceSettings.AllowEditPost,
+ "post_edit_time_limit": *Cfg.ServiceSettings.PostEditTimeLimit,
})
SendDiagnostic(TRACK_CONFIG_TEAM, map[string]interface{}{
diff --git a/utils/inbucket.go b/utils/inbucket.go
new file mode 100644
index 000000000..1c747cee8
--- /dev/null
+++ b/utils/inbucket.go
@@ -0,0 +1,115 @@
+package utils
+
+import (
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+)
+
+const (
+ INBUCKET_HOST = "http://dockerhost:9000"
+ INBUCKET_API = "/api/v1/mailbox/"
+)
+
+// OutputJSONHeader holds the received Header to test sending emails (inbucket)
+type JSONMessageHeaderInbucket []struct {
+ Mailbox string
+ ID string `json:"Id"`
+ From, Subject, Date string
+ To []string
+ Size int
+}
+
+// OutputJSONMessage holds the received Message fto test sending emails (inbucket)
+type JSONMessageInbucket struct {
+ Mailbox string
+ ID string `json:"Id"`
+ From, Subject, Date string
+ Size int
+ Header map[string][]string
+ Body struct {
+ Text string
+ HTML string `json:"Html"`
+ }
+}
+
+func ParseEmail(email string) string {
+ pos := strings.Index(email, "@")
+ parsedEmail := email[0:pos]
+ return parsedEmail
+}
+
+func GetMailBox(email string) (results JSONMessageHeaderInbucket, err error) {
+
+ parsedEmail := ParseEmail(email)
+
+ url := fmt.Sprintf("%s%s%s", INBUCKET_HOST, INBUCKET_API, parsedEmail)
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ client := &http.Client{}
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+
+ var record JSONMessageHeaderInbucket
+ if err := json.NewDecoder(resp.Body).Decode(&record); err != nil {
+ fmt.Println(err)
+ return nil, err
+ }
+ return record, nil
+}
+
+func GetMessageFromMailbox(email, id string) (results JSONMessageInbucket, err error) {
+
+ parsedEmail := ParseEmail(email)
+
+ var record JSONMessageInbucket
+
+ url := fmt.Sprintf("%s%s%s/%s", INBUCKET_HOST, INBUCKET_API, parsedEmail, id)
+ req, err := http.NewRequest("GET", url, nil)
+ if err != nil {
+ return record, err
+ }
+
+ client := &http.Client{}
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return record, err
+ }
+ defer resp.Body.Close()
+
+ if err := json.NewDecoder(resp.Body).Decode(&record); err != nil {
+ fmt.Println(err)
+ return record, err
+ }
+ return record, nil
+}
+
+func DeleteMailBox(email string) (err error) {
+
+ parsedEmail := ParseEmail(email)
+
+ url := fmt.Sprintf("%s%s%s", INBUCKET_HOST, INBUCKET_API, parsedEmail)
+ req, err := http.NewRequest("DELETE", url, nil)
+ if err != nil {
+ return err
+ }
+
+ client := &http.Client{}
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return err
+ }
+ defer resp.Body.Close()
+
+ return nil
+}
diff --git a/utils/mail_test.go b/utils/mail_test.go
new file mode 100644
index 000000000..012f10d39
--- /dev/null
+++ b/utils/mail_test.go
@@ -0,0 +1,61 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package utils
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestMailConnection(t *testing.T) {
+ LoadConfig("config.json")
+
+ if conn, err := connectToSMTPServer(Cfg); err != nil {
+ t.Log(err)
+ t.Fatal("Should connect to the STMP Server")
+ } else {
+ if _, err1 := newSMTPClient(conn, Cfg); err1 != nil {
+ t.Log(err)
+ t.Fatal("Should get new smtp client")
+ }
+ }
+
+ Cfg.EmailSettings.SMTPServer = "wrongServer"
+ Cfg.EmailSettings.SMTPPort = "553"
+
+ if _, err := connectToSMTPServer(Cfg); err == nil {
+ t.Log(err)
+ t.Fatal("Should not to the STMP Server")
+ }
+
+}
+
+func TestSendMail(t *testing.T) {
+ LoadConfig("config.json")
+ T = GetUserTranslations("en")
+
+ var emailTo string = "test@example.com"
+ var emailSubject string = "Testing this email"
+ var emailBody string = "This is a test from autobot"
+
+ //Delete all the messages before check the sample email
+ DeleteMailBox(emailTo)
+
+ if err := SendMail(emailTo, emailSubject, emailBody); err != nil {
+ t.Log(err)
+ t.Fatal("Should connect to the STMP Server")
+ } else {
+ //Check if the email was send to the rigth email address
+ if resultsMailbox, err := GetMailBox(emailTo); err != nil && !strings.ContainsAny(resultsMailbox[0].To[0], emailTo) {
+ t.Fatal("Wrong To recipient")
+ } else {
+ if resultsEmail, err := GetMessageFromMailbox(emailTo, resultsMailbox[0].ID); err == nil {
+ if !strings.Contains(resultsEmail.Body.Text, emailBody) {
+ t.Log(resultsEmail.Body.Text)
+ t.Fatal("Received message")
+ }
+ }
+ }
+ }
+}
diff --git a/utils/urlencode_test.go b/utils/urlencode_test.go
new file mode 100644
index 000000000..04d69fd98
--- /dev/null
+++ b/utils/urlencode_test.go
@@ -0,0 +1,35 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package utils
+
+import (
+ "testing"
+)
+
+func TestUrlEncode(t *testing.T) {
+
+ toEncode := "testing 1 2 3"
+ encoded := UrlEncode(toEncode)
+
+ if encoded != "testing%201%202%203" {
+ t.Log(encoded)
+ t.Fatal("should be equal")
+ }
+
+ toEncode = "testing123"
+ encoded = UrlEncode(toEncode)
+
+ if encoded != "testing123" {
+ t.Log(encoded)
+ t.Fatal("should be equal")
+ }
+
+ toEncode = "testing$#~123"
+ encoded = UrlEncode(toEncode)
+
+ if encoded != "testing%24%23~123" {
+ t.Log(encoded)
+ t.Fatal("should be equal")
+ }
+}
diff --git a/utils/utils.go b/utils/utils.go
index dd60f6060..6d34387c4 100644
--- a/utils/utils.go
+++ b/utils/utils.go
@@ -4,7 +4,11 @@
package utils
import (
+ "net"
+ "net/http"
"os"
+
+ "github.com/mattermost/platform/model"
)
func StringArrayIntersection(arr1, arr2 []string) []string {
@@ -48,3 +52,17 @@ func RemoveDuplicatesFromStringArray(arr []string) []string {
return result
}
+
+func GetIpAddress(r *http.Request) string {
+ address := r.Header.Get(model.HEADER_FORWARDED)
+
+ if len(address) == 0 {
+ address = r.Header.Get(model.HEADER_REAL_IP)
+ }
+
+ if len(address) == 0 {
+ address, _, _ = net.SplitHostPort(r.RemoteAddr)
+ }
+
+ return address
+}
diff --git a/vendor/github.com/dyatlov/go-opengraph/.gitignore b/vendor/github.com/dyatlov/go-opengraph/.gitignore
new file mode 100644
index 000000000..daf913b1b
--- /dev/null
+++ b/vendor/github.com/dyatlov/go-opengraph/.gitignore
@@ -0,0 +1,24 @@
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+*.test
+*.prof
diff --git a/vendor/github.com/dyatlov/go-opengraph/LICENSE b/vendor/github.com/dyatlov/go-opengraph/LICENSE
new file mode 100644
index 000000000..854759ad2
--- /dev/null
+++ b/vendor/github.com/dyatlov/go-opengraph/LICENSE
@@ -0,0 +1,22 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 Vitaly Dyatlov
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
diff --git a/vendor/github.com/dyatlov/go-opengraph/README.md b/vendor/github.com/dyatlov/go-opengraph/README.md
new file mode 100644
index 000000000..8c8e00212
--- /dev/null
+++ b/vendor/github.com/dyatlov/go-opengraph/README.md
@@ -0,0 +1,118 @@
+Go OpenGraph
+===
+
+Parses given html data into Facebook OpenGraph structure.
+
+To download and install this package run:
+
+`go get github.com/dyatlov/go-opengraph/opengraph`
+
+Methods:
+
+ * `NewOpenGraph()` - create a new OpenGraph instance
+ * `ProcessHTML(buffer io.Reader) error` - process given html into underlying data structure
+ * `ProcessMeta(metaAttrs map[string]string)` - add data to the structure based on meta attributes
+ * `ToJSON() (string, error)` - return JSON representation of data or error
+ * `String() string` - return JSON representation of structure
+
+Source docs: http://godoc.org/github.com/dyatlov/go-opengraph/opengraph
+
+If you just need to parse an OpenGraph data from HTML then method `ProcessHTML` is your needed one.
+
+Example:
+
+```go
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/dyatlov/go-opengraph/opengraph"
+)
+
+func main() {
+ html := `<html><head><meta property="og:type" content="article" />
+ <meta property="og:title" content="WordPress 4.3 &quot;Billie&quot;" />
+ <meta property="og:url" content="https://wordpress.org/news/2015/08/billie/" /></head><body></body></html>`
+
+ og := opengraph.NewOpenGraph()
+ err := og.ProcessHTML(strings.NewReader(html))
+
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+
+ fmt.Printf("Type: %s\n", og.Type)
+ fmt.Printf("Title: %s\n", og.Title)
+ fmt.Printf("URL: %s\n", og.URL)
+ fmt.Printf("String/JSON Representation: %s\n", og)
+}
+```
+
+If you have your own parsing engine and just need an intelligent OpenGraph parsing, then `ProcessMeta` is the method you need.
+While using this method you don't need to reparse your parsed html again, just feed it with meta atributes as they appear and OpenGraph will be built based on the data.
+
+Example:
+
+```go
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/dyatlov/go-opengraph/opengraph"
+ "golang.org/x/net/html"
+)
+
+func main() {
+ h := `<html><head><meta property="og:type" content="article" />
+ <meta property="og:title" content="WordPress 4.3 &quot;Billie&quot;" />
+ <meta property="og:url" content="https://wordpress.org/news/2015/08/billie/" /></head><body></body></html>`
+
+ og := opengraph.NewOpenGraph()
+
+ doc, err := html.Parse(strings.NewReader(h))
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+
+ var parseHead func(*html.Node)
+ parseHead = func(n *html.Node) {
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ if c.Type == html.ElementNode && c.Data == "meta" {
+ m := make(map[string]string)
+ for _, a := range c.Attr {
+ m[a.Key] = a.Val
+ }
+
+ og.ProcessMeta(m)
+ }
+ }
+ }
+
+ var f func(*html.Node)
+ f = func(n *html.Node) {
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ if c.Type == html.ElementNode {
+ if c.Data == "head" {
+ parseHead(c)
+ continue
+ } else if c.Data == "body" { // OpenGraph is only in head, so we don't need body
+ break
+ }
+ }
+ f(c)
+ }
+ }
+ f(doc)
+
+ fmt.Printf("Type: %s\n", og.Type)
+ fmt.Printf("Title: %s\n", og.Title)
+ fmt.Printf("URL: %s\n", og.URL)
+ fmt.Printf("String/JSON Representation: %s\n", og)
+}
+```
diff --git a/vendor/github.com/dyatlov/go-opengraph/examples/advanced.go b/vendor/github.com/dyatlov/go-opengraph/examples/advanced.go
new file mode 100644
index 000000000..e24b821e7
--- /dev/null
+++ b/vendor/github.com/dyatlov/go-opengraph/examples/advanced.go
@@ -0,0 +1,58 @@
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/dyatlov/go-opengraph/opengraph"
+ "golang.org/x/net/html"
+)
+
+func main() {
+ h := `<html><head><meta property="og:type" content="article" />
+ <meta property="og:title" content="WordPress 4.3 &quot;Billie&quot;" />
+ <meta property="og:url" content="https://wordpress.org/news/2015/08/billie/" /></head><body></body></html>`
+
+ og := opengraph.NewOpenGraph()
+
+ doc, err := html.Parse(strings.NewReader(h))
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+
+ var parseHead func(*html.Node)
+ parseHead = func(n *html.Node) {
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ if c.Type == html.ElementNode && c.Data == "meta" {
+ m := make(map[string]string)
+ for _, a := range c.Attr {
+ m[a.Key] = a.Val
+ }
+
+ og.ProcessMeta(m)
+ }
+ }
+ }
+
+ var f func(*html.Node)
+ f = func(n *html.Node) {
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ if c.Type == html.ElementNode {
+ if c.Data == "head" {
+ parseHead(c)
+ continue
+ } else if c.Data == "body" { // OpenGraph is only in head, so we don't need body
+ break
+ }
+ }
+ f(c)
+ }
+ }
+ f(doc)
+
+ fmt.Printf("Type: %s\n", og.Type)
+ fmt.Printf("Title: %s\n", og.Title)
+ fmt.Printf("URL: %s\n", og.URL)
+ fmt.Printf("String/JSON Representation: %s\n", og)
+}
diff --git a/vendor/github.com/dyatlov/go-opengraph/examples/simple.go b/vendor/github.com/dyatlov/go-opengraph/examples/simple.go
new file mode 100644
index 000000000..fa128cd43
--- /dev/null
+++ b/vendor/github.com/dyatlov/go-opengraph/examples/simple.go
@@ -0,0 +1,27 @@
+package main
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/dyatlov/go-opengraph/opengraph"
+)
+
+func main() {
+ html := `<html><head><meta property="og:type" content="article" />
+ <meta property="og:title" content="WordPress 4.3 &quot;Billie&quot;" />
+ <meta property="og:url" content="https://wordpress.org/news/2015/08/billie/" /></head><body></body></html>`
+
+ og := opengraph.NewOpenGraph()
+ err := og.ProcessHTML(strings.NewReader(html))
+
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+
+ fmt.Printf("Type: %s\n", og.Type)
+ fmt.Printf("Title: %s\n", og.Title)
+ fmt.Printf("URL: %s\n", og.URL)
+ fmt.Printf("String/JSON Representation: %s\n", og)
+}
diff --git a/vendor/github.com/dyatlov/go-opengraph/opengraph/opengraph.go b/vendor/github.com/dyatlov/go-opengraph/opengraph/opengraph.go
new file mode 100644
index 000000000..5468d86bb
--- /dev/null
+++ b/vendor/github.com/dyatlov/go-opengraph/opengraph/opengraph.go
@@ -0,0 +1,329 @@
+package opengraph
+
+import (
+ "encoding/json"
+ "io"
+ "strconv"
+ "time"
+
+ "golang.org/x/net/html"
+ "golang.org/x/net/html/atom"
+)
+
+// Image defines Open Graph Image type
+type Image struct {
+ URL string `json:"url"`
+ SecureURL string `json:"secure_url"`
+ Type string `json:"type"`
+ Width uint64 `json:"width"`
+ Height uint64 `json:"height"`
+}
+
+// Video defines Open Graph Video type
+type Video struct {
+ URL string `json:"url"`
+ SecureURL string `json:"secure_url"`
+ Type string `json:"type"`
+ Width uint64 `json:"width"`
+ Height uint64 `json:"height"`
+}
+
+// Audio defines Open Graph Audio Type
+type Audio struct {
+ URL string `json:"url"`
+ SecureURL string `json:"secure_url"`
+ Type string `json:"type"`
+}
+
+// Article contain Open Graph Article structure
+type Article struct {
+ PublishedTime *time.Time `json:"published_time"`
+ ModifiedTime *time.Time `json:"modified_time"`
+ ExpirationTime *time.Time `json:"expiration_time"`
+ Section string `json:"section"`
+ Tags []string `json:"tags"`
+ Authors []*Profile `json:"authors"`
+}
+
+// Profile contains Open Graph Profile structure
+type Profile struct {
+ FirstName string `json:"first_name"`
+ LastName string `json:"last_name"`
+ Username string `json:"username"`
+ Gender string `json:"gender"`
+}
+
+// Book contains Open Graph Book structure
+type Book struct {
+ ISBN string `json:"isbn"`
+ ReleaseDate *time.Time `json:"release_date"`
+ Tags []string `json:"tags"`
+ Authors []*Profile `json:"authors"`
+}
+
+// OpenGraph contains facebook og data
+type OpenGraph struct {
+ isArticle bool
+ isBook bool
+ isProfile bool
+ Type string `json:"type"`
+ URL string `json:"url"`
+ Title string `json:"title"`
+ Description string `json:"description"`
+ Determiner string `json:"determiner"`
+ SiteName string `json:"site_name"`
+ Locale string `json:"locale"`
+ LocalesAlternate []string `json:"locales_alternate"`
+ Images []*Image `json:"images"`
+ Audios []*Audio `json:"audios"`
+ Videos []*Video `json:"videos"`
+ Article *Article `json:"article,omitempty"`
+ Book *Book `json:"book,omitempty"`
+ Profile *Profile `json:"profile,omitempty"`
+}
+
+// NewOpenGraph returns new instance of Open Graph structure
+func NewOpenGraph() *OpenGraph {
+ return &OpenGraph{}
+}
+
+// ToJSON a simple wrapper around json.Marshal
+func (og *OpenGraph) ToJSON() ([]byte, error) {
+ return json.Marshal(og)
+}
+
+// String return json representation of structure, or error string
+func (og *OpenGraph) String() string {
+ data, err := og.ToJSON()
+
+ if err != nil {
+ return err.Error()
+ }
+
+ return string(data[:])
+}
+
+// ProcessHTML parses given html from Reader interface and fills up OpenGraph structure
+func (og *OpenGraph) ProcessHTML(buffer io.Reader) error {
+ z := html.NewTokenizer(buffer)
+ for {
+ tt := z.Next()
+ switch tt {
+ case html.ErrorToken:
+ if z.Err() == io.EOF {
+ return nil
+ }
+ return z.Err()
+ case html.StartTagToken, html.SelfClosingTagToken, html.EndTagToken:
+ name, hasAttr := z.TagName()
+ if atom.Lookup(name) == atom.Body {
+ return nil // OpenGraph is only in head, so we don't need body
+ }
+ if atom.Lookup(name) != atom.Meta || !hasAttr {
+ continue
+ }
+ m := make(map[string]string)
+ var key, val []byte
+ for hasAttr {
+ key, val, hasAttr = z.TagAttr()
+ m[atom.String(key)] = string(val)
+ }
+ og.ProcessMeta(m)
+ }
+ }
+ return nil
+}
+
+// ProcessMeta processes meta attributes and adds them to Open Graph structure if they are suitable for that
+func (og *OpenGraph) ProcessMeta(metaAttrs map[string]string) {
+ switch metaAttrs["property"] {
+ case "og:description":
+ og.Description = metaAttrs["content"]
+ case "og:type":
+ og.Type = metaAttrs["content"]
+ switch og.Type {
+ case "article":
+ og.isArticle = true
+ case "book":
+ og.isBook = true
+ case "profile":
+ og.isProfile = true
+ }
+ case "og:title":
+ og.Title = metaAttrs["content"]
+ case "og:url":
+ og.URL = metaAttrs["content"]
+ case "og:determiner":
+ og.Determiner = metaAttrs["content"]
+ case "og:site_name":
+ og.SiteName = metaAttrs["content"]
+ case "og:locale":
+ og.Locale = metaAttrs["content"]
+ case "og:locale:alternate":
+ og.LocalesAlternate = append(og.LocalesAlternate, metaAttrs["content"])
+ case "og:image":
+ og.Images = append(og.Images, &Image{URL: metaAttrs["content"]})
+ case "og:image:url":
+ if len(og.Images) > 0 {
+ og.Images[len(og.Images)-1].URL = metaAttrs["content"]
+ }
+ case "og:image:secure_url":
+ if len(og.Images) > 0 {
+ og.Images[len(og.Images)-1].SecureURL = metaAttrs["content"]
+ }
+ case "og:image:type":
+ if len(og.Images) > 0 {
+ og.Images[len(og.Images)-1].Type = metaAttrs["content"]
+ }
+ case "og:image:width":
+ if len(og.Images) > 0 {
+ w, err := strconv.ParseUint(metaAttrs["content"], 10, 64)
+ if err == nil {
+ og.Images[len(og.Images)-1].Width = w
+ }
+ }
+ case "og:image:height":
+ if len(og.Images) > 0 {
+ h, err := strconv.ParseUint(metaAttrs["content"], 10, 64)
+ if err == nil {
+ og.Images[len(og.Images)-1].Height = h
+ }
+ }
+ case "og:video":
+ og.Videos = append(og.Videos, &Video{URL: metaAttrs["content"]})
+ case "og:video:url":
+ if len(og.Videos) > 0 {
+ og.Videos[len(og.Videos)-1].URL = metaAttrs["content"]
+ }
+ case "og:video:secure_url":
+ if len(og.Videos) > 0 {
+ og.Videos[len(og.Videos)-1].SecureURL = metaAttrs["content"]
+ }
+ case "og:video:type":
+ if len(og.Videos) > 0 {
+ og.Videos[len(og.Videos)-1].Type = metaAttrs["content"]
+ }
+ case "og:video:width":
+ if len(og.Videos) > 0 {
+ w, err := strconv.ParseUint(metaAttrs["content"], 10, 64)
+ if err == nil {
+ og.Videos[len(og.Videos)-1].Width = w
+ }
+ }
+ case "og:video:height":
+ if len(og.Videos) > 0 {
+ h, err := strconv.ParseUint(metaAttrs["content"], 10, 64)
+ if err == nil {
+ og.Videos[len(og.Videos)-1].Height = h
+ }
+ }
+ default:
+ if og.isArticle {
+ og.processArticleMeta(metaAttrs)
+ } else if og.isBook {
+ og.processBookMeta(metaAttrs)
+ } else if og.isProfile {
+ og.processProfileMeta(metaAttrs)
+ }
+ }
+}
+
+func (og *OpenGraph) processArticleMeta(metaAttrs map[string]string) {
+ if og.Article == nil {
+ og.Article = &Article{}
+ }
+ switch metaAttrs["property"] {
+ case "article:published_time":
+ t, err := time.Parse(time.RFC3339, metaAttrs["content"])
+ if err == nil {
+ og.Article.PublishedTime = &t
+ }
+ case "article:modified_time":
+ t, err := time.Parse(time.RFC3339, metaAttrs["content"])
+ if err == nil {
+ og.Article.ModifiedTime = &t
+ }
+ case "article:expiration_time":
+ t, err := time.Parse(time.RFC3339, metaAttrs["content"])
+ if err == nil {
+ og.Article.ExpirationTime = &t
+ }
+ case "article:secttion":
+ og.Article.Section = metaAttrs["content"]
+ case "article:tag":
+ og.Article.Tags = append(og.Article.Tags, metaAttrs["content"])
+ case "article:author:first_name":
+ if len(og.Article.Authors) == 0 {
+ og.Article.Authors = append(og.Article.Authors, &Profile{})
+ }
+ og.Article.Authors[len(og.Article.Authors)-1].FirstName = metaAttrs["content"]
+ case "article:author:last_name":
+ if len(og.Article.Authors) == 0 {
+ og.Article.Authors = append(og.Article.Authors, &Profile{})
+ }
+ og.Article.Authors[len(og.Article.Authors)-1].LastName = metaAttrs["content"]
+ case "article:author:username":
+ if len(og.Article.Authors) == 0 {
+ og.Article.Authors = append(og.Article.Authors, &Profile{})
+ }
+ og.Article.Authors[len(og.Article.Authors)-1].Username = metaAttrs["content"]
+ case "article:author:gender":
+ if len(og.Article.Authors) == 0 {
+ og.Article.Authors = append(og.Article.Authors, &Profile{})
+ }
+ og.Article.Authors[len(og.Article.Authors)-1].Gender = metaAttrs["content"]
+ }
+}
+
+func (og *OpenGraph) processBookMeta(metaAttrs map[string]string) {
+ if og.Book == nil {
+ og.Book = &Book{}
+ }
+ switch metaAttrs["property"] {
+ case "book:release_date":
+ t, err := time.Parse(time.RFC3339, metaAttrs["content"])
+ if err == nil {
+ og.Book.ReleaseDate = &t
+ }
+ case "book:isbn":
+ og.Book.ISBN = metaAttrs["content"]
+ case "book:tag":
+ og.Book.Tags = append(og.Book.Tags, metaAttrs["content"])
+ case "book:author:first_name":
+ if len(og.Book.Authors) == 0 {
+ og.Book.Authors = append(og.Book.Authors, &Profile{})
+ }
+ og.Book.Authors[len(og.Book.Authors)-1].FirstName = metaAttrs["content"]
+ case "book:author:last_name":
+ if len(og.Book.Authors) == 0 {
+ og.Book.Authors = append(og.Book.Authors, &Profile{})
+ }
+ og.Book.Authors[len(og.Book.Authors)-1].LastName = metaAttrs["content"]
+ case "book:author:username":
+ if len(og.Book.Authors) == 0 {
+ og.Book.Authors = append(og.Book.Authors, &Profile{})
+ }
+ og.Book.Authors[len(og.Book.Authors)-1].Username = metaAttrs["content"]
+ case "book:author:gender":
+ if len(og.Book.Authors) == 0 {
+ og.Book.Authors = append(og.Book.Authors, &Profile{})
+ }
+ og.Book.Authors[len(og.Book.Authors)-1].Gender = metaAttrs["content"]
+ }
+}
+
+func (og *OpenGraph) processProfileMeta(metaAttrs map[string]string) {
+ if og.Profile == nil {
+ og.Profile = &Profile{}
+ }
+ switch metaAttrs["property"] {
+ case "profile:first_name":
+ og.Profile.FirstName = metaAttrs["content"]
+ case "profile:last_name":
+ og.Profile.LastName = metaAttrs["content"]
+ case "profile:username":
+ og.Profile.Username = metaAttrs["content"]
+ case "profile:gender":
+ og.Profile.Gender = metaAttrs["content"]
+ }
+}
diff --git a/vendor/github.com/dyatlov/go-opengraph/opengraph/opengraph_test.go b/vendor/github.com/dyatlov/go-opengraph/opengraph/opengraph_test.go
new file mode 100644
index 000000000..6af7f25d2
--- /dev/null
+++ b/vendor/github.com/dyatlov/go-opengraph/opengraph/opengraph_test.go
@@ -0,0 +1,131 @@
+package opengraph_test
+
+import (
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/dyatlov/go-opengraph/opengraph"
+)
+
+const html = `
+ <!DOCTYPE html>
+<html xmlns="http://www.w3.org/1999/xhtml" dir="ltr" lang="en-US">
+<head profile="http://gmpg.org/xfn/11">
+<meta charset="utf-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1">
+<title>WordPress &#8250; WordPress 4.3 &#8220;Billie&#8221;</title>
+
+<!-- Jetpack Open Graph Tags -->
+<meta property="og:type" content="article" />
+<meta property="og:title" content="WordPress 4.3 &quot;Billie&quot;" />
+<meta property="og:url" content="https://wordpress.org/news/2015/08/billie/" />
+<meta property="og:description" content="Version 4.3 of WordPress, named &quot;Billie&quot; in honor of jazz singer Billie Holiday, is available for download or update in your WordPress dashboard. New features in 4.3 make it even easier to format y..." />
+<meta property="article:published_time" content="2015-08-18T19:12:38+00:00" />
+<meta property="article:modified_time" content="2015-08-19T13:10:24+00:00" />
+<meta property="og:site_name" content="WordPress News" />
+<meta property="og:image" content="https://www.gravatar.com/avatar/2370ea5912750f4cb0f3c51ae1cbca55?d=mm&amp;s=180&amp;r=G" />
+<meta property="og:locale" content="en_US" />
+<meta name="twitter:site" content="@WordPress" />
+<meta name="twitter:card" content="summary" />
+<meta name="twitter:creator" content="@WordPress" />
+ `
+
+func BenchmarkOpenGraph_ProcessHTML(b *testing.B) {
+ og := opengraph.NewOpenGraph()
+ b.ReportAllocs()
+ b.SetBytes(int64(len(html)))
+ for i := 0; i < b.N; i++ {
+ if err := og.ProcessHTML(strings.NewReader(html)); err != nil {
+ b.Fatal(err)
+ }
+ }
+}
+
+func TestOpenGraphProcessHTML(t *testing.T) {
+ og := opengraph.NewOpenGraph()
+ err := og.ProcessHTML(strings.NewReader(html))
+
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if og.Type != "article" {
+ t.Error("type parsed incorrectly")
+ }
+
+ if len(og.Title) == 0 {
+ t.Error("title parsed incorrectly")
+ }
+
+ if len(og.URL) == 0 {
+ t.Error("url parsed incorrectly")
+ }
+
+ if len(og.Description) == 0 {
+ t.Error("description parsed incorrectly")
+ }
+
+ if len(og.Images) == 0 {
+ t.Error("images parsed incorrectly")
+ } else {
+ if len(og.Images[0].URL) == 0 {
+ t.Error("image url parsed incorrectly")
+ }
+ }
+
+ if len(og.Locale) == 0 {
+ t.Error("locale parsed incorrectly")
+ }
+
+ if len(og.SiteName) == 0 {
+ t.Error("site name parsed incorrectly")
+ }
+
+ if og.Article == nil {
+ t.Error("articles parsed incorrectly")
+ } else {
+ ev, _ := time.Parse(time.RFC3339, "2015-08-18T19:12:38+00:00")
+ if !og.Article.PublishedTime.Equal(ev) {
+ t.Error("article published time parsed incorrectly")
+ }
+ }
+}
+
+func TestOpenGraphProcessMeta(t *testing.T) {
+ og := opengraph.NewOpenGraph()
+
+ og.ProcessMeta(map[string]string{"property": "og:type", "content": "book"})
+
+ if og.Type != "book" {
+ t.Error("wrong og:type processing")
+ }
+
+ og.ProcessMeta(map[string]string{"property": "book:isbn", "content": "123456"})
+
+ if og.Book == nil {
+ t.Error("wrong book type processing")
+ } else {
+ if og.Book.ISBN != "123456" {
+ t.Error("wrong book isbn processing")
+ }
+ }
+
+ og.ProcessMeta(map[string]string{"property": "article:section", "content": "testsection"})
+
+ if og.Article != nil {
+ t.Error("article processed when it should not be")
+ }
+
+ og.ProcessMeta(map[string]string{"property": "book:author:first_name", "content": "John"})
+
+ if og.Book != nil {
+ if len(og.Book.Authors) == 0 {
+ t.Error("book author was not processed")
+ } else {
+ if og.Book.Authors[0].FirstName != "John" {
+ t.Error("author first name was processed incorrectly")
+ }
+ }
+ }
+}
diff --git a/web/web.go b/web/web.go
index d1a181d73..1ee216d55 100644
--- a/web/web.go
+++ b/web/web.go
@@ -11,6 +11,7 @@ import (
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/api"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
"github.com/mssola/user_agent"
@@ -19,7 +20,7 @@ import (
func InitWeb() {
l4g.Debug(utils.T("web.init.debug"))
- mainrouter := api.Srv.Router
+ mainrouter := app.Srv.Router
if *utils.Cfg.ServiceSettings.WebserverMode != "disabled" {
staticDir := utils.FindDir(model.CLIENT_DIR)
diff --git a/web/web_test.go b/web/web_test.go
index 812fa4239..dd1dcf19c 100644
--- a/web/web_test.go
+++ b/web/web_test.go
@@ -10,6 +10,7 @@ import (
"time"
"github.com/mattermost/platform/api"
+ "github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
@@ -19,28 +20,28 @@ var ApiClient *model.Client
var URL string
func Setup() {
- if api.Srv == nil {
+ if app.Srv == nil {
utils.TranslationsPreInit()
utils.LoadConfig("config.json")
utils.InitTranslations(utils.Cfg.LocalizationSettings)
- api.NewServer()
- api.InitStores()
+ app.NewServer()
+ app.InitStores()
api.InitRouter()
- api.StartServer()
+ app.StartServer()
api.InitApi()
InitWeb()
URL = "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress
ApiClient = model.NewClient(URL)
- api.Srv.Store.MarkSystemRanUnitTests()
+ app.Srv.Store.MarkSystemRanUnitTests()
*utils.Cfg.TeamSettings.EnableOpenServer = true
}
}
func TearDown() {
- if api.Srv != nil {
- api.StopServer()
+ if app.Srv != nil {
+ app.StopServer()
}
}
@@ -64,18 +65,19 @@ func TestStatic(t *testing.T) {
func TestGetAccessToken(t *testing.T) {
Setup()
- team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
- rteam, _ := ApiClient.CreateTeam(&team)
-
user := model.User{Email: strings.ToLower(model.NewId()) + "success+test@simulator.amazonses.com", Password: "passwd1"}
ruser := ApiClient.Must(ApiClient.CreateUser(&user, "")).Data.(*model.User)
- api.JoinUserToTeam(rteam.Data.(*model.Team), ruser)
- store.Must(api.Srv.Store.User().VerifyEmail(ruser.Id))
+ store.Must(app.Srv.Store.User().VerifyEmail(ruser.Id))
+
+ ApiClient.Must(ApiClient.LoginById(ruser.Id, "passwd1"))
+
+ team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ rteam, _ := ApiClient.CreateTeam(&team)
- app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
+ oauthApp := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = false
- data := url.Values{"grant_type": []string{"junk"}, "client_id": []string{"12345678901234567890123456"}, "client_secret": []string{"12345678901234567890123456"}, "code": []string{"junk"}, "redirect_uri": []string{app.CallbackUrls[0]}}
+ data := url.Values{"grant_type": []string{"junk"}, "client_id": []string{"12345678901234567890123456"}, "client_secret": []string{"12345678901234567890123456"}, "code": []string{"junk"}, "redirect_uri": []string{oauthApp.CallbackUrls[0]}}
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - oauth providing turned off")
@@ -86,18 +88,18 @@ func TestGetAccessToken(t *testing.T) {
ApiClient.SetTeamId(rteam.Data.(*model.Team).Id)
*utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = false
utils.SetDefaultRolesBasedOnConfig()
- app = ApiClient.Must(ApiClient.RegisterApp(app)).Data.(*model.OAuthApp)
+ oauthApp = ApiClient.Must(ApiClient.RegisterApp(oauthApp)).Data.(*model.OAuthApp)
*utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = true
utils.SetDefaultRolesBasedOnConfig()
- redirect := ApiClient.Must(ApiClient.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, app.CallbackUrls[0], "all", "123")).Data.(map[string]string)["redirect"]
+ redirect := ApiClient.Must(ApiClient.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, oauthApp.Id, oauthApp.CallbackUrls[0], "all", "123")).Data.(map[string]string)["redirect"]
rurl, _ := url.Parse(redirect)
teamId := rteam.Data.(*model.Team).Id
ApiClient.Logout()
- data = url.Values{"grant_type": []string{"junk"}, "client_id": []string{app.Id}, "client_secret": []string{app.ClientSecret}, "code": []string{rurl.Query().Get("code")}, "redirect_uri": []string{app.CallbackUrls[0]}}
+ data = url.Values{"grant_type": []string{"junk"}, "client_id": []string{oauthApp.Id}, "client_secret": []string{oauthApp.ClientSecret}, "code": []string{rurl.Query().Get("code")}, "redirect_uri": []string{oauthApp.CallbackUrls[0]}}
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - bad grant type")
@@ -113,7 +115,7 @@ func TestGetAccessToken(t *testing.T) {
t.Fatal("should have failed - bad client id")
}
- data.Set("client_id", app.Id)
+ data.Set("client_id", oauthApp.Id)
data.Set("client_secret", "")
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - missing client secret")
@@ -124,7 +126,7 @@ func TestGetAccessToken(t *testing.T) {
t.Fatal("should have failed - bad client secret")
}
- data.Set("client_secret", app.ClientSecret)
+ data.Set("client_secret", oauthApp.ClientSecret)
data.Set("code", "")
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - missing code")
@@ -143,10 +145,10 @@ func TestGetAccessToken(t *testing.T) {
// reset data for successful request
data.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE)
- data.Set("client_id", app.Id)
- data.Set("client_secret", app.ClientSecret)
+ data.Set("client_id", oauthApp.Id)
+ data.Set("client_secret", oauthApp.ClientSecret)
data.Set("code", rurl.Query().Get("code"))
- data.Set("redirect_uri", app.CallbackUrls[0])
+ data.Set("redirect_uri", oauthApp.CallbackUrls[0])
token := ""
if result, err := ApiClient.GetAccessToken(data); err != nil {
@@ -200,16 +202,18 @@ func TestGetAccessToken(t *testing.T) {
func TestIncomingWebhook(t *testing.T) {
Setup()
- team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
- team = ApiClient.Must(ApiClient.CreateTeam(team)).Data.(*model.Team)
-
user := &model.User{Email: model.NewId() + "success+test@simulator.amazonses.com", Nickname: "Corey Hulen", Password: "passwd1"}
user = ApiClient.Must(ApiClient.CreateUser(user, "")).Data.(*model.User)
- store.Must(api.Srv.Store.User().VerifyEmail(user.Id))
- api.JoinUserToTeam(team, user)
+ store.Must(app.Srv.Store.User().VerifyEmail(user.Id))
- api.UpdateUserRoles(user, model.ROLE_SYSTEM_ADMIN.Id)
ApiClient.Login(user.Email, "passwd1")
+
+ team := &model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
+ team = ApiClient.Must(ApiClient.CreateTeam(team)).Data.(*model.Team)
+
+ app.JoinUserToTeam(team, user)
+
+ app.UpdateUserRoles(user.Id, model.ROLE_SYSTEM_ADMIN.Id)
ApiClient.SetTeamId(team.Id)
channel1 := &model.Channel{DisplayName: "Test API Name", Name: "a" + model.NewId() + "a", Type: model.CHANNEL_OPEN, TeamId: team.Id}
diff --git a/webapp/actions/admin_actions.jsx b/webapp/actions/admin_actions.jsx
new file mode 100644
index 000000000..73b73c130
--- /dev/null
+++ b/webapp/actions/admin_actions.jsx
@@ -0,0 +1,404 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import Client from 'client/web_client.jsx';
+import * as AsyncClient from 'utils/async_client.jsx';
+import {browserHistory} from 'react-router/es6';
+
+export function revokeSession(altId, success, error) {
+ Client.revokeSession(altId,
+ () => {
+ AsyncClient.getSessions();
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function saveConfig(config, success, error) {
+ Client.saveConfig(
+ config,
+ () => {
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function reloadConfig(success, error) {
+ Client.reloadConfig(
+ () => {
+ AsyncClient.getConfig();
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function adminResetMfa(userId, success, error) {
+ Client.adminResetMfa(
+ userId,
+ () => {
+ AsyncClient.getUser(userId);
+
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function getClusterStatus(success, error) {
+ Client.getClusterStatus(
+ (data) => {
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'getClusterStatus');
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function saveComplianceReports(job, success, error) {
+ Client.saveComplianceReports(
+ job,
+ () => {
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function testEmail(config, success, error) {
+ Client.testEmail(
+ config,
+ () => {
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function ldapTest(success, error) {
+ Client.ldapTest(
+ () => {
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function invalidateAllCaches(success, error) {
+ Client.invalidateAllCaches(
+ () => {
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function recycleDatabaseConnection(success, error) {
+ Client.recycleDatabaseConnection(
+ () => {
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function adminResetPassword(userId, password, success, error) {
+ Client.adminResetPassword(
+ userId,
+ password,
+ () => {
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function samlCertificateStatus(success, error) {
+ Client.samlCertificateStatus(
+ (data) => {
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function ldapSyncNow(success, error) {
+ Client.ldapSyncNow(
+ () => {
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function getOAuthAppInfo(clientId, success, error) {
+ Client.getOAuthAppInfo(
+ clientId,
+ (data) => {
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function allowOAuth2(params, success, error) {
+ const responseType = params.response_type;
+ const clientId = params.client_id;
+ const redirectUri = params.redirect_uri;
+ const state = params.state;
+ const scope = params.scope;
+
+ Client.allowOAuth2(responseType, clientId, redirectUri, state, scope,
+ (data) => {
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function emailToLdap(loginId, password, token, ldapId, ldapPassword, success, error) {
+ Client.emailToLdap(
+ loginId,
+ password,
+ token,
+ ldapId,
+ ldapPassword,
+ (data) => {
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function emailToOAuth(loginId, password, token, newType, success, error) {
+ Client.emailToOAuth(
+ loginId,
+ password,
+ token,
+ newType,
+ (data) => {
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function oauthToEmail(email, password, success, error) {
+ Client.oauthToEmail(
+ email,
+ password,
+ (data) => {
+ if (data.follow_link) {
+ browserHistory.push(data.follow_link);
+ }
+
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function regenerateOAuthAppSecret(oauthAppId, success, error) {
+ Client.regenerateOAuthAppSecret(
+ oauthAppId,
+ (data) => {
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function uploadBrandImage(brandImage, success, error) {
+ Client.uploadBrandImage(
+ brandImage,
+ () => {
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function uploadLicenseFile(file, success, error) {
+ Client.uploadLicenseFile(
+ file,
+ () => {
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function removeLicenseFile(success, error) {
+ Client.removeLicenseFile(
+ () => {
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function uploadCertificateFile(certificateFile, success, error) {
+ Client.uploadCertificateFile(
+ certificateFile,
+ () => {
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function removeCertificateFile(certificateId, success, error) {
+ Client.removeCertificateFile(
+ certificateId,
+ () => {
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
diff --git a/webapp/actions/channel_actions.jsx b/webapp/actions/channel_actions.jsx
index 204e6f9f1..4b4e3e10c 100644
--- a/webapp/actions/channel_actions.jsx
+++ b/webapp/actions/channel_actions.jsx
@@ -6,6 +6,7 @@ import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
+import * as ChannelUtils from 'utils/channel_utils.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import {loadProfilesAndTeamMembersForDMSidebar} from 'actions/user_actions.jsx';
@@ -48,7 +49,14 @@ export function executeCommand(message, args, success, error) {
msg = '/shortcuts';
}
}
- Client.executeCommand(msg, args, success, error);
+ Client.executeCommand(msg, args, success,
+ (err) => {
+ AsyncClient.dispatchError(err, 'executeCommand');
+
+ if (error) {
+ error(err);
+ }
+ });
}
export function setChannelAsRead(channelIdParam) {
@@ -101,6 +109,9 @@ export function removeUserFromChannel(channelId, userId, success, error) {
}
UserStore.emitInChannelChange();
+ ChannelStore.removeMemberInChannel(channelId, userId);
+ ChannelStore.emitChange();
+
if (success) {
success(data);
}
@@ -115,6 +126,48 @@ export function removeUserFromChannel(channelId, userId, success, error) {
);
}
+export function makeUserChannelAdmin(channelId, userId, success, error) {
+ Client.updateChannelMemberRoles(
+ channelId,
+ userId,
+ 'channel_user channel_admin',
+ () => {
+ AsyncClient.getChannelMember(channelId, userId);
+ getChannelMembersForUserIds(channelId, [userId]);
+
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function makeUserChannelMember(channelId, userId, success, error) {
+ Client.updateChannelMemberRoles(
+ channelId,
+ userId,
+ 'channel_user',
+ () => {
+ AsyncClient.getChannelMember(channelId, userId);
+ getChannelMembersForUserIds(channelId, [userId]);
+
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
export function openDirectChannelToUser(user, success, error) {
const channelName = Utils.getDirectChannelName(UserStore.getCurrentId(), user.id);
const channel = ChannelStore.getByName(channelName);
@@ -311,3 +364,120 @@ export function createChannel(channel, success, error) {
}
);
}
+
+export function updateChannelPurpose(channelId, purposeValue, success, error) {
+ Client.updateChannelPurpose(
+ channelId,
+ purposeValue,
+ () => {
+ AsyncClient.getChannel(channelId);
+
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function updateChannelHeader(channelId, header, success, error) {
+ Client.updateChannelHeader(
+ channelId,
+ header,
+ (channelData) => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_CHANNEL,
+ channel: channelData
+ });
+
+ if (success) {
+ success(channelData);
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function getChannelMembersForUserIds(channelId, userIds, success, error) {
+ Client.getChannelMembersByIds(
+ channelId,
+ userIds,
+ (data) => {
+ const memberMap = {};
+ for (let i = 0; i < data.length; i++) {
+ memberMap[data[i].user_id] = data[i];
+ }
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_MEMBERS_IN_CHANNEL,
+ channel_id: channelId,
+ channel_members: memberMap
+ });
+
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'getChannelMembersByIds');
+
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function leaveChannel(channelId, success, error) {
+ Client.leaveChannel(channelId,
+ () => {
+ loadChannelsForCurrentUser();
+
+ if (ChannelUtils.isFavoriteChannelId(channelId)) {
+ unmarkFavorite(channelId);
+ }
+
+ const townsquare = ChannelStore.getByName('town-square');
+ browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + townsquare.name);
+
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'handleLeave');
+
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function deleteChannel(channelId, success, error) {
+ Client.deleteChannel(
+ channelId,
+ () => {
+ loadChannelsForCurrentUser();
+
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'handleDelete');
+
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
diff --git a/webapp/actions/file_actions.jsx b/webapp/actions/file_actions.jsx
new file mode 100644
index 000000000..0399a2c28
--- /dev/null
+++ b/webapp/actions/file_actions.jsx
@@ -0,0 +1,26 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import * as AsyncClient from 'utils/async_client.jsx';
+import Client from 'client/web_client.jsx';
+
+export function uploadFile(file, name, channelId, clientId, success, error) {
+ Client.uploadFile(
+ file,
+ name,
+ channelId,
+ clientId,
+ (data) => {
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'uploadFile');
+
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
diff --git a/webapp/actions/global_actions.jsx b/webapp/actions/global_actions.jsx
index ea077d6eb..23e19f22f 100644
--- a/webapp/actions/global_actions.jsx
+++ b/webapp/actions/global_actions.jsx
@@ -256,7 +256,7 @@ export function emitLoadMorePostsFocusedTopEvent() {
}
export function loadMorePostsTop(id, isFocusPost) {
- const earliestPostId = PostStore.getEarliestPost(id).id;
+ const earliestPostId = PostStore.getEarliestPostFromPage(id).id;
if (PostStore.requestVisibilityIncrease(id, Constants.POST_CHUNK_SIZE)) {
loadPostsBefore(earliestPostId, 0, Constants.POST_CHUNK_SIZE, isFocusPost);
}
@@ -596,3 +596,27 @@ export function redirectUserToDefaultTeam() {
browserHistory.push('/select_team');
}
}
+
+requestOpenGraphMetadata.openGraphMetadataOnGoingRequests = {}; // Format: {<url>: true}
+export function requestOpenGraphMetadata(url) {
+ const onself = requestOpenGraphMetadata;
+
+ if (!onself.openGraphMetadataOnGoingRequests[url]) {
+ onself.openGraphMetadataOnGoingRequests[url] = true;
+
+ Client.getOpenGraphMetadata(url,
+ (data) => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECIVED_OPEN_GRAPH_METADATA,
+ url,
+ data
+ });
+ delete onself.openGraphMetadataOnGoingRequests[url];
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'getOpenGraphMetadata');
+ delete onself.openGraphMetadataOnGoingRequests[url];
+ }
+ );
+ }
+}
diff --git a/webapp/actions/post_actions.jsx b/webapp/actions/post_actions.jsx
index 0e48fb0e8..61f193b66 100644
--- a/webapp/actions/post_actions.jsx
+++ b/webapp/actions/post_actions.jsx
@@ -115,12 +115,16 @@ export function getFlaggedPosts() {
);
}
-export function loadPosts(channelId = ChannelStore.getCurrentId()) {
+export function loadPosts(channelId = ChannelStore.getCurrentId(), isPost = false) {
const postList = PostStore.getAllPosts(channelId);
const latestPostTime = PostStore.getLatestPostFromPageTime(channelId);
- if (!postList || Object.keys(postList).length === 0 || postList.order.length < Constants.POST_CHUNK_SIZE || latestPostTime === 0) {
- loadPostsPage(channelId, Constants.POST_CHUNK_SIZE);
+ if (
+ !postList || Object.keys(postList).length === 0 ||
+ (!isPost && postList.order.length < Constants.POST_CHUNK_SIZE) ||
+ latestPostTime === 0
+ ) {
+ loadPostsPage(channelId, Constants.POST_CHUNK_SIZE, isPost);
return;
}
@@ -133,7 +137,8 @@ export function loadPosts(channelId = ChannelStore.getCurrentId()) {
id: channelId,
before: true,
numRequested: 0,
- post_list: data
+ post_list: data,
+ isPost
});
loadProfilesForPosts(data.posts);
@@ -145,7 +150,7 @@ export function loadPosts(channelId = ChannelStore.getCurrentId()) {
);
}
-export function loadPostsPage(channelId = ChannelStore.getCurrentId(), max = Constants.POST_CHUNK_SIZE) {
+export function loadPostsPage(channelId = ChannelStore.getCurrentId(), max = Constants.POST_CHUNK_SIZE, isPost = false) {
const postList = PostStore.getAllPosts(channelId);
// if we already have more than POST_CHUNK_SIZE posts,
@@ -167,7 +172,9 @@ export function loadPostsPage(channelId = ChannelStore.getCurrentId(), max = Con
before: true,
numRequested: numPosts,
checkLatest: true,
- post_list: data
+ checkEarliest: true,
+ post_list: data,
+ isPost
});
loadProfilesForPosts(data.posts);
@@ -195,6 +202,7 @@ export function loadPostsBefore(postId, offset, numPost, isPost) {
type: ActionTypes.RECEIVED_POSTS,
id: channelId,
before: true,
+ checkEarliest: true,
numRequested: numPost,
post_list: data,
isPost
@@ -363,7 +371,76 @@ export function createPost(post, doLoadPost, success, error) {
);
}
+export function updatePost(post, success, isPost) {
+ Client.updatePost(
+ post,
+ () => {
+ loadPosts(post.channel_id, isPost);
+
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'updatePost');
+ });
+}
+
export function removePostFromStore(post) {
PostStore.removePost(post);
PostStore.emitChange();
}
+
+export function deletePost(channelId, post, success, error) {
+ Client.deletePost(
+ channelId,
+ post.id,
+ () => {
+ removePostFromStore(post);
+ if (post.id === PostStore.getSelectedPostId()) {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_POST_SELECTED,
+ postId: null
+ });
+ }
+
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'deletePost');
+
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function performSearch(terms, isMentionSearch, success, error) {
+ Client.search(
+ terms,
+ isMentionSearch,
+ (data) => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_SEARCH,
+ results: data,
+ is_mention_search: isMentionSearch
+ });
+
+ loadProfilesForPosts(data.posts);
+
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'search');
+
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
diff --git a/webapp/actions/team_actions.jsx b/webapp/actions/team_actions.jsx
index 3a86bada9..e23fe1e5d 100644
--- a/webapp/actions/team_actions.jsx
+++ b/webapp/actions/team_actions.jsx
@@ -60,7 +60,10 @@ export function removeUserFromTeam(teamId, userId, success, error) {
userId,
() => {
TeamStore.removeMemberInTeam(teamId, userId);
+ UserStore.removeProfileFromTeam(teamId, userId);
+ UserStore.emitInTeamChange();
AsyncClient.getUser(userId);
+ AsyncClient.getTeamStats(teamId);
if (success) {
success();
@@ -92,3 +95,53 @@ export function updateTeamMemberRoles(teamId, userId, newRoles, success, error)
}
);
}
+
+export function addUserToTeamFromInvite(data, hash, inviteId, success, error) {
+ Client.addUserToTeamFromInvite(
+ data,
+ hash,
+ inviteId,
+ (team) => {
+ if (success) {
+ success(team);
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function getInviteInfo(inviteId, success, error) {
+ Client.getInviteInfo(
+ inviteId,
+ (inviteData) => {
+ if (success) {
+ success(inviteData);
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function inviteMembers(data, success, error) {
+ Client.inviteMembers(
+ data,
+ () => {
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (err) {
+ error(err);
+ }
+ }
+ );
+}
diff --git a/webapp/actions/user_actions.jsx b/webapp/actions/user_actions.jsx
index 0f5fb0731..73a84a7c6 100644
--- a/webapp/actions/user_actions.jsx
+++ b/webapp/actions/user_actions.jsx
@@ -4,17 +4,22 @@
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
+import BrowserStore from 'stores/browser_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
+import * as GlobalActions from 'actions/global_actions.jsx';
+import {getChannelMembersForUserIds} from 'actions/channel_actions.jsx';
import {loadStatusesForProfilesList, loadStatusesForProfilesMap} from 'actions/status_actions.jsx';
import {getDirectChannelName} from 'utils/utils.jsx';
+
import * as AsyncClient from 'utils/async_client.jsx';
import Client from 'client/web_client.jsx';
import {ActionTypes, Preferences} from 'utils/constants.jsx';
+import {browserHistory} from 'react-router/es6';
export function switchFromLdapToEmail(email, password, token, ldapPassword, onSuccess, onError) {
Client.ldapToEmail(
@@ -58,6 +63,34 @@ export function loadProfilesAndTeamMembers(offset, limit, teamId = TeamStore.get
);
}
+export function loadProfilesAndTeamMembersAndChannelMembers(offset, limit, teamId = TeamStore.getCurrentId(), channelId = ChannelStore.getCurrentId(), success, error) {
+ Client.getProfilesInChannel(
+ channelId,
+ offset,
+ limit,
+ (data) => {
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_PROFILES_IN_CHANNEL,
+ profiles: data,
+ channel_id: channelId,
+ offset,
+ count: Object.keys(data).length
+ });
+
+ loadTeamMembersForProfilesMap(
+ data,
+ teamId,
+ () => {
+ loadChannelMembersForProfilesMap(data, channelId, success, error);
+ loadStatusesForProfilesMap(data);
+ });
+ },
+ (err) => {
+ AsyncClient.dispatchError(err, 'getProfilesInChannel');
+ }
+ );
+}
+
export function loadTeamMembersForProfilesMap(profiles, teamId = TeamStore.getCurrentId(), success, error) {
const membersToLoad = {};
for (const pid in profiles) {
@@ -132,6 +165,56 @@ function loadTeamMembersForProfiles(userIds, teamId, success, error) {
);
}
+export function loadChannelMembersForProfilesMap(profiles, channelId = ChannelStore.getCurrentId(), success, error) {
+ const membersToLoad = {};
+ for (const pid in profiles) {
+ if (!profiles.hasOwnProperty(pid)) {
+ continue;
+ }
+
+ if (!ChannelStore.hasActiveMemberInChannel(channelId, pid)) {
+ membersToLoad[pid] = true;
+ }
+ }
+
+ const list = Object.keys(membersToLoad);
+ if (list.length === 0) {
+ if (success) {
+ success({});
+ }
+ return;
+ }
+
+ getChannelMembersForUserIds(channelId, list, success, error);
+}
+
+export function loadTeamMembersAndChannelMembersForProfilesList(profiles, teamId = TeamStore.getCurrentId(), channelId = ChannelStore.getCurrentId(), success, error) {
+ loadTeamMembersForProfilesList(profiles, teamId, () => {
+ loadChannelMembersForProfilesList(profiles, channelId, success, error);
+ }, error);
+}
+
+export function loadChannelMembersForProfilesList(profiles, channelId = ChannelStore.getCurrentId(), success, error) {
+ const membersToLoad = {};
+ for (let i = 0; i < profiles.length; i++) {
+ const pid = profiles[i].id;
+
+ if (!ChannelStore.hasActiveMemberInChannel(channelId, pid)) {
+ membersToLoad[pid] = true;
+ }
+ }
+
+ const list = Object.keys(membersToLoad);
+ if (list.length === 0) {
+ if (success) {
+ success({});
+ }
+ return;
+ }
+
+ getChannelMembersForUserIds(channelId, list, success, error);
+}
+
function populateDMChannelsWithProfiles(userIds) {
const currentUserId = UserStore.getCurrentId();
@@ -348,10 +431,10 @@ export function updateUser(username, type, success, error) {
}
},
(err) => {
- AsyncClient.dispatchError(err, 'updateUser');
-
if (error) {
error(err);
+ } else {
+ AsyncClient.dispatchError(err, 'updateUser');
}
}
);
@@ -374,6 +457,24 @@ export function generateMfaSecret(success, error) {
);
}
+export function updateUserNotifyProps(data, success, error) {
+ Client.updateUserNotifyProps(
+ data,
+ () => {
+ AsyncClient.getMe();
+
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
export function updateUserRoles(userId, newRoles, success, error) {
Client.updateUserRoles(
userId,
@@ -412,6 +513,25 @@ export function activateMfa(code, success, error) {
);
}
+export function deactivateMfa(success, error) {
+ Client.updateMfa(
+ '',
+ false,
+ () => {
+ AsyncClient.getMe();
+
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
export function checkMfa(loginId, success, error) {
if (global.window.mm_config.EnableMultifactorAuthentication !== 'true') {
success(false);
@@ -449,3 +569,205 @@ export function updateActive(userId, active, success, error) {
}
);
}
+
+export function updatePassword(userId, currentPassword, newPassword, success, error) {
+ Client.updatePassword(userId, currentPassword, newPassword,
+ () => {
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function verifyEmail(uid, hid, success, error) {
+ Client.verifyEmail(
+ uid,
+ hid,
+ (data) => {
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function resetPassword(code, password, success, error) {
+ Client.resetPassword(
+ code,
+ password,
+ () => {
+ browserHistory.push('/login?extra=' + ActionTypes.PASSWORD_CHANGE);
+
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function resendVerification(email, success, error) {
+ Client.resendVerification(
+ email,
+ () => {
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function loginById(userId, password, mfaToken, hash, success, error) {
+ Client.loginById(
+ userId,
+ password,
+ mfaToken,
+ hash,
+ () => {
+ if (hash > 0) {
+ BrowserStore.setGlobalItem(hash, JSON.stringify({usedBefore: true}));
+ }
+
+ GlobalActions.emitInitialLoad(
+ () => {
+ const query = this.props.location.query;
+ if (query.redirect_to) {
+ browserHistory.push(query.redirect_to);
+ } else {
+ GlobalActions.redirectUserToDefaultTeam();
+ }
+ }
+ );
+
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function createUserWithInvite(user, data, emailHash, inviteId, success, error) {
+ Client.createUserWithInvite(
+ user,
+ data,
+ emailHash,
+ inviteId,
+ (response) => {
+ if (success) {
+ success(response);
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function webLogin(loginId, password, token, success, error) {
+ Client.webLogin(
+ loginId,
+ password,
+ token,
+ () => {
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function webLoginByLdap(loginId, password, token, success, error) {
+ Client.webLoginByLdap(
+ loginId,
+ password,
+ token,
+ (data) => {
+ if (success) {
+ success(data);
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
+
+export function getAuthorizedApps(success, error) {
+ Client.getAuthorizedApps(
+ (authorizedApps) => {
+ if (success) {
+ success(authorizedApps);
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ });
+}
+
+export function deauthorizeOAuthApp(appId, success, error) {
+ Client.deauthorizeOAuthApp(
+ appId,
+ () => {
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ });
+}
+
+export function uploadProfileImage(userPicture, success, error) {
+ Client.uploadProfileImage(
+ userPicture,
+ () => {
+ AsyncClient.getMe();
+ if (success) {
+ success();
+ }
+ },
+ (err) => {
+ if (error) {
+ error(err);
+ }
+ }
+ );
+}
diff --git a/webapp/actions/webrtc_actions.jsx b/webapp/actions/webrtc_actions.jsx
index 444eee241..b096e1c33 100644
--- a/webapp/actions/webrtc_actions.jsx
+++ b/webapp/actions/webrtc_actions.jsx
@@ -4,6 +4,8 @@
import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
import {WebrtcActionTypes} from 'utils/constants.jsx';
+import Client from 'client/web_client.jsx';
+
export function initWebrtc(userId, isCalling) {
AppDispatcher.handleServerAction({
type: WebrtcActionTypes.INITIALIZE,
@@ -18,3 +20,17 @@ export function handle(message) {
message
});
}
+
+export function webrtcToken(success, error) {
+ Client.webrtcToken(
+ (data) => {
+ if (success) {
+ success(data);
+ }
+ },
+ () => {
+ if (error) {
+ error();
+ }
+ });
+}
diff --git a/webapp/client/client.jsx b/webapp/client/client.jsx
index ba42d7ae8..9f1bc926d 100644
--- a/webapp/client/client.jsx
+++ b/webapp/client/client.jsx
@@ -1496,6 +1496,16 @@ export default class Client {
end(this.handleResponse.bind(this, 'getChannelMember', success, error));
}
+ getChannelMembersByIds(channelId, userIds, success, error) {
+ request.
+ post(`${this.getChannelNeededRoute(channelId)}/members/ids`).
+ set(this.defaultHeaders).
+ type('application/json').
+ accept('application/json').
+ send(userIds).
+ end(this.handleResponse.bind(this, 'getChannelMembersByIds', success, error));
+ }
+
addChannelMember(channelId, userId, success, error) {
request.
post(`${this.getChannelNeededRoute(channelId)}/add`).
@@ -1520,6 +1530,21 @@ export default class Client {
this.track('api', 'api_channels_remove_member');
}
+ updateChannelMemberRoles(channelId, userId, newRoles, success, error) {
+ var data = {
+ user_id: userId,
+ new_roles: newRoles
+ };
+
+ request.
+ post(`${this.getChannelNeededRoute(channelId)}/update_member_roles`).
+ set(this.defaultHeaders).
+ type('application/json').
+ accept('application/json').
+ send(data).
+ end(this.handleResponse.bind(this, 'updateChannelMemberRoles', success, error));
+ }
+
// Routes for Commands
listCommands(success, error) {
@@ -1742,6 +1767,16 @@ export default class Client {
end(this.handleResponse.bind(this, 'getFileInfosForPost', success, error));
}
+ getOpenGraphMetadata(url, success, error) {
+ request.
+ post(`${this.getBaseRoute()}/get_opengraph_metadata`).
+ set(this.defaultHeaders).
+ type('application/json').
+ accept('application/json').
+ send({url}).
+ end(this.handleResponse.bind(this, 'getOpenGraphMetadata', success, error));
+ }
+
// Routes for Files
uploadFile(file, filename, channelId, clientId, success, error) {
diff --git a/webapp/components/activity_log_modal.jsx b/webapp/components/activity_log_modal.jsx
index b907668f0..cd369f742 100644
--- a/webapp/components/activity_log_modal.jsx
+++ b/webapp/components/activity_log_modal.jsx
@@ -5,7 +5,6 @@ import LoadingScreen from './loading_screen.jsx';
import UserStore from 'stores/user_store.jsx';
-import Client from 'client/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
@@ -14,6 +13,8 @@ import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage, FormattedTime, FormattedDate} from 'react-intl';
+import {revokeSession} from 'actions/admin_actions.jsx';
+
export default class ActivityLogModal extends React.Component {
constructor(props) {
super(props);
@@ -46,10 +47,8 @@ export default class ActivityLogModal extends React.Component {
setTimeout(() => {
modalContent.removeClass('animation--highlight');
}, 1500);
- Client.revokeSession(altId,
- () => {
- AsyncClient.getSessions();
- },
+ revokeSession(altId,
+ null,
(err) => {
const state = this.getStateFromStores();
state.serverError = err;
@@ -134,6 +133,17 @@ export default class ActivityLogModal extends React.Component {
} else {
devicePicture = 'fa fa-linux';
}
+ } else if (currentSession.props.os.indexOf('Linux') !== -1) {
+ devicePicture = 'fa fa-linux';
+ }
+
+ if (currentSession.props.browser.indexOf('Desktop App') !== -1) {
+ devicePlatform = (
+ <FormattedMessage
+ id='activity_log_modal.desktop'
+ defaultMessage='Native Desktop App'
+ />
+ );
}
let moreInfo;
diff --git a/webapp/components/admin_console/admin_settings.jsx b/webapp/components/admin_console/admin_settings.jsx
index 9975a3975..b9883d7d8 100644
--- a/webapp/components/admin_console/admin_settings.jsx
+++ b/webapp/components/admin_console/admin_settings.jsx
@@ -4,11 +4,12 @@
import React from 'react';
import * as AsyncClient from 'utils/async_client.jsx';
-import Client from 'client/web_client.jsx';
import FormError from 'components/form_error.jsx';
import SaveButton from 'components/admin_console/save_button.jsx';
+import {saveConfig} from 'actions/admin_actions.jsx';
+
export default class AdminSettings extends React.Component {
static get propTypes() {
return {
@@ -53,7 +54,7 @@ export default class AdminSettings extends React.Component {
let config = JSON.parse(JSON.stringify(this.props.config));
config = this.getConfigFromState(config);
- Client.saveConfig(
+ saveConfig(
config,
() => {
AsyncClient.getConfig((savedConfig) => {
diff --git a/webapp/components/admin_console/admin_sidebar_header.jsx b/webapp/components/admin_console/admin_sidebar_header.jsx
index 86c2c6b0f..5725551bf 100644
--- a/webapp/components/admin_console/admin_sidebar_header.jsx
+++ b/webapp/components/admin_console/admin_sidebar_header.jsx
@@ -42,7 +42,7 @@ export default class SidebarHeader extends React.Component {
profilePicture = (
<img
className='user__picture'
- src={Client.getUsersRoute() + '/' + me.id + '/image?time=' + me.update_at}
+ src={Client.getUsersRoute() + '/' + me.id + '/image?time=' + me.last_picture_update}
/>
);
}
diff --git a/webapp/components/admin_console/admin_team_members_dropdown.jsx b/webapp/components/admin_console/admin_team_members_dropdown.jsx
index ee9e53f6c..01e94db16 100644
--- a/webapp/components/admin_console/admin_team_members_dropdown.jsx
+++ b/webapp/components/admin_console/admin_team_members_dropdown.jsx
@@ -6,12 +6,10 @@ import ConfirmModal from '../confirm_modal.jsx';
import UserStore from 'stores/user_store.jsx';
import TeamStore from 'stores/team_store.jsx';
-import Client from 'client/web_client.jsx';
import Constants from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
import {updateUserRoles, updateActive} from 'actions/user_actions.jsx';
-import {updateTeamMemberRoles} from 'actions/team_actions.jsx';
+import {updateTeamMemberRoles, removeUserFromTeam, adminResetMfa} from 'actions/team_actions.jsx';
import {FormattedMessage} from 'react-intl';
@@ -75,14 +73,10 @@ export default class AdminTeamMembersDropdown extends React.Component {
}
handleRemoveFromTeam() {
- Client.removeUserFromTeam(
+ removeUserFromTeam(
this.props.teamMember.team_id,
this.props.user.id,
- () => {
- AsyncClient.getTeamStats(this.props.teamMember.team_id);
- UserStore.removeProfileFromTeam(this.props.teamMember.team_id, this.props.user.id);
- UserStore.emitInTeamChange();
- },
+ null,
(err) => {
this.setState({serverError: err.message});
}
@@ -150,10 +144,8 @@ export default class AdminTeamMembersDropdown extends React.Component {
handleResetMfa(e) {
e.preventDefault();
- Client.adminResetMfa(this.props.user.id,
- () => {
- AsyncClient.getUser(this.props.user.id);
- },
+ adminResetMfa(this.props.user.id,
+ null,
(err) => {
this.setState({serverError: err.message});
}
diff --git a/webapp/components/admin_console/brand_image_setting.jsx b/webapp/components/admin_console/brand_image_setting.jsx
index 653073200..b58c0159c 100644
--- a/webapp/components/admin_console/brand_image_setting.jsx
+++ b/webapp/components/admin_console/brand_image_setting.jsx
@@ -7,6 +7,7 @@ import ReactDOM from 'react-dom';
import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
+import {uploadBrandImage} from 'actions/admin_actions.jsx';
import FormError from 'components/form_error.jsx';
import {FormattedHTMLMessage, FormattedMessage} from 'react-intl';
@@ -81,7 +82,7 @@ export default class BrandImageSetting extends React.Component {
error: ''
});
- Client.uploadBrandImage(
+ uploadBrandImage(
this.state.brandImage,
() => {
$(ReactDOM.findDOMNode(this.refs.upload)).button('complete');
diff --git a/webapp/components/admin_console/cluster_table_container.jsx b/webapp/components/admin_console/cluster_table_container.jsx
index aad5753b7..8dba80cce 100644
--- a/webapp/components/admin_console/cluster_table_container.jsx
+++ b/webapp/components/admin_console/cluster_table_container.jsx
@@ -4,8 +4,8 @@
import React from 'react';
import ClusterTable from './cluster_table.jsx';
import LoadingScreen from '../loading_screen.jsx';
-import Client from 'client/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
+
+import {getClusterStatus} from 'actions/admin_actions.jsx';
export default class ClusterTableContainer extends React.Component {
constructor(props) {
@@ -19,15 +19,13 @@ export default class ClusterTableContainer extends React.Component {
}
load() {
- Client.getClusterStatus(
+ getClusterStatus(
(data) => {
this.setState({
clusterInfos: data
});
},
- (err) => {
- AsyncClient.dispatchError(err, 'getClusterStatus');
- }
+ null
);
}
diff --git a/webapp/components/admin_console/compliance_reports.jsx b/webapp/components/admin_console/compliance_reports.jsx
index aac09c0de..7274e6774 100644
--- a/webapp/components/admin_console/compliance_reports.jsx
+++ b/webapp/components/admin_console/compliance_reports.jsx
@@ -9,6 +9,7 @@ import UserStore from '../../stores/user_store.jsx';
import Client from 'client/web_client.jsx';
import * as AsyncClient from '../../utils/async_client.jsx';
+import {saveComplianceReports} from 'actions/admin_actions.jsx';
import {FormattedMessage, FormattedDate, FormattedTime} from 'react-intl';
@@ -72,7 +73,7 @@ export default class ComplianceReports extends React.Component {
job.start_at = Date.parse(ReactDOM.findDOMNode(this.refs.from).value);
job.end_at = Date.parse(ReactDOM.findDOMNode(this.refs.to).value);
- Client.saveComplianceReports(
+ saveComplianceReports(
job,
() => {
ReactDOM.findDOMNode(this.refs.emails).value = '';
diff --git a/webapp/components/admin_console/email_connection_test.jsx b/webapp/components/admin_console/email_connection_test.jsx
index 8e11a0bb4..b99633eec 100644
--- a/webapp/components/admin_console/email_connection_test.jsx
+++ b/webapp/components/admin_console/email_connection_test.jsx
@@ -3,11 +3,12 @@
import React from 'react';
-import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import {FormattedMessage} from 'react-intl';
+import {testEmail} from 'actions/admin_actions.jsx';
+
export default class EmailConnectionTestButton extends React.Component {
static get propTypes() {
return {
@@ -41,7 +42,7 @@ export default class EmailConnectionTestButton extends React.Component {
const config = JSON.parse(JSON.stringify(this.props.config));
this.props.getConfigFromState(config);
- Client.testEmail(
+ testEmail(
config,
() => {
this.setState({
diff --git a/webapp/components/admin_console/ldap_test_button.jsx b/webapp/components/admin_console/ldap_test_button.jsx
index e077aec5f..a564fa42a 100644
--- a/webapp/components/admin_console/ldap_test_button.jsx
+++ b/webapp/components/admin_console/ldap_test_button.jsx
@@ -3,11 +3,12 @@
import React from 'react';
-import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {ldapTest} from 'actions/admin_actions.jsx';
+
export default class LdapTestButton extends React.Component {
static get propTypes() {
return {
@@ -38,7 +39,7 @@ export default class LdapTestButton extends React.Component {
});
const doRequest = () => { //eslint-disable-line func-style
- Client.ldapTest(
+ ldapTest(
() => {
this.setState({
buisy: false,
diff --git a/webapp/components/admin_console/license_settings.jsx b/webapp/components/admin_console/license_settings.jsx
index d98309f80..6c14394b7 100644
--- a/webapp/components/admin_console/license_settings.jsx
+++ b/webapp/components/admin_console/license_settings.jsx
@@ -4,7 +4,8 @@
import $ from 'jquery';
import ReactDOM from 'react-dom';
import * as Utils from 'utils/utils.jsx';
-import Client from 'client/web_client.jsx';
+
+import {uploadLicenseFile, removeLicenseFile} from 'actions/admin_actions.jsx';
import {injectIntl, intlShape, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
@@ -54,7 +55,8 @@ class LicenseSettings extends React.Component {
$('#upload-button').button('loading');
- Client.uploadLicenseFile(file,
+ uploadLicenseFile(
+ file,
() => {
Utils.clearFileInput(element[0]);
$('#upload-button').button('reset');
@@ -74,7 +76,7 @@ class LicenseSettings extends React.Component {
$('#remove-button').button('loading');
- Client.removeLicenseFile(
+ removeLicenseFile(
() => {
$('#remove-button').button('reset');
this.setState({fileSelected: false, fileName: null, serverError: null});
diff --git a/webapp/components/admin_console/logs.jsx b/webapp/components/admin_console/logs.jsx
index 8dc0c1e2e..5846c91db 100644
--- a/webapp/components/admin_console/logs.jsx
+++ b/webapp/components/admin_console/logs.jsx
@@ -24,12 +24,14 @@ export default class Logs extends React.Component {
componentDidMount() {
AdminStore.addLogChangeListener(this.onLogListenerChange);
AsyncClient.getLogs();
+ this.refs.logPanel.focus();
}
componentDidUpdate() {
// Scroll Down to get the latest logs
var node = this.refs.logPanel;
node.scrollTop = node.scrollHeight;
+ node.focus();
}
componentWillUnmount() {
@@ -100,6 +102,7 @@ export default class Logs extends React.Component {
/>
</button>
<div
+ tabIndex='-1'
ref='logPanel'
className='log__panel'
>
diff --git a/webapp/components/admin_console/policy_settings.jsx b/webapp/components/admin_console/policy_settings.jsx
index 0e224af73..391726a93 100644
--- a/webapp/components/admin_console/policy_settings.jsx
+++ b/webapp/components/admin_console/policy_settings.jsx
@@ -6,6 +6,8 @@ import React from 'react';
import AdminSettings from './admin_settings.jsx';
import SettingsGroup from './settings_group.jsx';
import DropdownSetting from './dropdown_setting.jsx';
+import RadioSetting from './radio_setting.jsx';
+import PostEditSetting from './post_edit_setting.jsx';
import Constants from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
@@ -22,6 +24,9 @@ export default class PolicySettings extends AdminSettings {
}
getConfigFromState(config) {
+ config.ServiceSettings.RestrictPostDelete = this.state.restrictPostDelete;
+ config.ServiceSettings.AllowEditPost = this.state.allowEditPost;
+ config.ServiceSettings.PostEditTimeLimit = this.parseIntNonZero(this.state.postEditTimeLimit, Constants.DEFAULT_POST_EDIT_TIME_LIMIT);
config.TeamSettings.RestrictTeamInvite = this.state.restrictTeamInvite;
config.TeamSettings.RestrictPublicChannelCreation = this.state.restrictPublicChannelCreation;
config.TeamSettings.RestrictPrivateChannelCreation = this.state.restrictPrivateChannelCreation;
@@ -35,6 +40,9 @@ export default class PolicySettings extends AdminSettings {
getStateFromConfig(config) {
return {
+ restrictPostDelete: config.ServiceSettings.RestrictPostDelete,
+ allowEditPost: config.ServiceSettings.AllowEditPost,
+ postEditTimeLimit: config.ServiceSettings.PostEditTimeLimit,
restrictTeamInvite: config.TeamSettings.RestrictTeamInvite,
restrictPublicChannelCreation: config.TeamSettings.RestrictPublicChannelCreation,
restrictPrivateChannelCreation: config.TeamSettings.RestrictPrivateChannelCreation,
@@ -241,6 +249,47 @@ export default class PolicySettings extends AdminSettings {
/>
}
/>
+ <RadioSetting
+ id='restrictPostDelete'
+ values={[
+ {value: Constants.PERMISSIONS_DELETE_POST_ALL, text: Utils.localizeMessage('admin.general.policy.permissionsDeletePostAll', 'Message authors can delete their own messages, and Administrators can delete any message')},
+ {value: Constants.PERMISSIONS_DELETE_POST_TEAM_ADMIN, text: Utils.localizeMessage('admin.general.policy.permissionsDeletePostAdmin', 'Team Admins and System Admins')},
+ {value: Constants.PERMISSIONS_DELETE_POST_SYSTEM_ADMIN, text: Utils.localizeMessage('admin.general.policy.permissionsDeletePostSystemAdmin', 'System Admins')}
+ ]}
+ label={
+ <FormattedMessage
+ id='admin.general.policy.restrictPostDeleteTitle'
+ defaultMessage='Allow which users to delete messages:'
+ />
+ }
+ value={this.state.restrictPostDelete}
+ onChange={this.handleChange}
+ helpText={
+ <FormattedHTMLMessage
+ id='admin.general.policy.restrictPostDeleteDescription'
+ defaultMessage='Set policy on who has permission to delete messages.'
+ />
+ }
+ />
+ <PostEditSetting
+ id='allowEditPost'
+ timeLimitId='postEditTimeLimit'
+ label={
+ <FormattedMessage
+ id='admin.general.policy.allowEditPostTitle'
+ defaultMessage='Allow users to edit their messages:'
+ />
+ }
+ value={this.state.allowEditPost}
+ timeLimitValue={this.state.postEditTimeLimit}
+ onChange={this.handleChange}
+ helpText={
+ <FormattedHTMLMessage
+ id='admin.general.policy.allowEditPostDescription'
+ defaultMessage='Set policy on the length of time authors have to edit their messages after posting.'
+ />
+ }
+ />
</SettingsGroup>
);
}
diff --git a/webapp/components/admin_console/post_edit_setting.jsx b/webapp/components/admin_console/post_edit_setting.jsx
new file mode 100644
index 000000000..282a1b6c5
--- /dev/null
+++ b/webapp/components/admin_console/post_edit_setting.jsx
@@ -0,0 +1,99 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import Setting from './setting.jsx';
+
+import Constants from 'utils/constants.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+export default class PostEditSetting extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ this.handleTimeLimitChange = this.handleTimeLimitChange.bind(this);
+ }
+
+ handleChange(e) {
+ this.props.onChange(this.props.id, e.target.value);
+ }
+
+ handleTimeLimitChange(e) {
+ this.props.onChange(this.props.timeLimitId, e.target.value);
+ }
+
+ render() {
+ return (
+ <Setting
+ label={this.props.label}
+ inputId={this.props.id}
+ helpText={this.props.helpText}
+ >
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ value={Constants.ALLOW_EDIT_POST_ALWAYS}
+ name={this.props.id}
+ checked={this.props.value === Constants.ALLOW_EDIT_POST_ALWAYS}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
+ />
+ {Utils.localizeMessage('admin.general.policy.allowEditPostAlways', 'Any time')}
+ </label>
+ </div>
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ value={Constants.ALLOW_EDIT_POST_NEVER}
+ name={this.props.id}
+ checked={this.props.value === Constants.ALLOW_EDIT_POST_NEVER}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
+ />
+ {Utils.localizeMessage('admin.general.policy.allowEditPostNever', 'Never')}
+ </label>
+ </div>
+ <div className='radio form-inline'>
+ <label>
+ <input
+ type='radio'
+ value={Constants.ALLOW_EDIT_POST_TIME_LIMIT}
+ name={this.props.id}
+ checked={this.props.value === Constants.ALLOW_EDIT_POST_TIME_LIMIT}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
+ />
+ <input
+ type='text'
+ value={this.props.timeLimitValue}
+ className='form-control'
+ name={this.props.timeLimitId}
+ onChange={this.handleTimeLimitChange}
+ disabled={this.props.disabled || this.props.value !== Constants.ALLOW_EDIT_POST_TIME_LIMIT}
+ />
+ <span> {Utils.localizeMessage('admin.general.policy.allowEditPostTimeLimit', 'seconds after posting')}</span>
+ </label>
+ </div>
+ </Setting>
+ );
+ }
+}
+
+PostEditSetting.defaultProps = {
+ isDisabled: false
+};
+
+PostEditSetting.propTypes = {
+ id: React.PropTypes.string.isRequired,
+ timeLimitId: React.PropTypes.string.isRequired,
+ label: React.PropTypes.node.isRequired,
+ value: React.PropTypes.string.isRequired,
+ timeLimitValue: React.PropTypes.number.isRequired,
+ onChange: React.PropTypes.func.isRequired,
+ disabled: React.PropTypes.bool,
+ helpText: React.PropTypes.node
+};
diff --git a/webapp/components/admin_console/purge_caches.jsx b/webapp/components/admin_console/purge_caches.jsx
index a999f090e..9f52433d5 100644
--- a/webapp/components/admin_console/purge_caches.jsx
+++ b/webapp/components/admin_console/purge_caches.jsx
@@ -3,10 +3,10 @@
import React from 'react';
-import Client from 'client/web_client.jsx';
-
import {FormattedMessage} from 'react-intl';
+import {invalidateAllCaches} from 'actions/admin_actions.jsx';
+
export default class PurgeCachesButton extends React.Component {
constructor(props) {
super(props);
@@ -27,7 +27,7 @@ export default class PurgeCachesButton extends React.Component {
fail: null
});
- Client.invalidateAllCaches(
+ invalidateAllCaches(
() => {
this.setState({
loading: false
@@ -43,10 +43,6 @@ export default class PurgeCachesButton extends React.Component {
}
render() {
- if (global.window.mm_license.IsLicensed !== 'true') {
- return <div/>;
- }
-
let testMessage = null;
if (this.state.fail) {
testMessage = (
diff --git a/webapp/components/admin_console/radio_setting.jsx b/webapp/components/admin_console/radio_setting.jsx
new file mode 100644
index 000000000..dd45a5a26
--- /dev/null
+++ b/webapp/components/admin_console/radio_setting.jsx
@@ -0,0 +1,63 @@
+// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import Setting from './setting.jsx';
+
+export default class RadioSetting extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleChange = this.handleChange.bind(this);
+ }
+
+ handleChange(e) {
+ this.props.onChange(this.props.id, e.target.value);
+ }
+
+ render() {
+ const options = [];
+ for (const {value, text} of this.props.values) {
+ options.push(
+ <div className='radio'>
+ <label>
+ <input
+ type='radio'
+ value={value}
+ name={this.props.id}
+ checked={value === this.props.value}
+ onChange={this.handleChange}
+ disabled={this.props.disabled}
+ />
+ {text}
+ </label>
+ </div>
+ );
+ }
+
+ return (
+ <Setting
+ label={this.props.label}
+ inputId={this.props.id}
+ helpText={this.props.helpText}
+ >
+ {options}
+ </Setting>
+ );
+ }
+}
+
+RadioSetting.defaultProps = {
+ isDisabled: false
+};
+
+RadioSetting.propTypes = {
+ id: React.PropTypes.string.isRequired,
+ values: React.PropTypes.array.isRequired,
+ label: React.PropTypes.node.isRequired,
+ value: React.PropTypes.string.isRequired,
+ onChange: React.PropTypes.func.isRequired,
+ disabled: React.PropTypes.bool,
+ helpText: React.PropTypes.node
+};
diff --git a/webapp/components/admin_console/recycle_db.jsx b/webapp/components/admin_console/recycle_db.jsx
index 53e8e7436..5683f97e2 100644
--- a/webapp/components/admin_console/recycle_db.jsx
+++ b/webapp/components/admin_console/recycle_db.jsx
@@ -3,11 +3,12 @@
import React from 'react';
-import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {recycleDatabaseConnection} from 'actions/admin_actions.jsx';
+
export default class RecycleDbButton extends React.Component {
constructor(props) {
super(props);
@@ -28,7 +29,7 @@ export default class RecycleDbButton extends React.Component {
fail: null
});
- Client.recycleDatabaseConnection(
+ recycleDatabaseConnection(
() => {
this.setState({
loading: false
diff --git a/webapp/components/admin_console/reload_config.jsx b/webapp/components/admin_console/reload_config.jsx
index 0b50d5803..25e9463d3 100644
--- a/webapp/components/admin_console/reload_config.jsx
+++ b/webapp/components/admin_console/reload_config.jsx
@@ -3,13 +3,12 @@
import React from 'react';
-import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
-import {getConfig} from 'utils/async_client.jsx';
-
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {reloadConfig} from 'actions/admin_actions.jsx';
+
export default class ReloadConfigButton extends React.Component {
constructor(props) {
super(props);
@@ -30,9 +29,8 @@ export default class ReloadConfigButton extends React.Component {
fail: null
});
- Client.reloadConfig(
+ reloadConfig(
() => {
- getConfig();
this.setState({
loading: false
});
diff --git a/webapp/components/admin_console/reset_password_modal.jsx b/webapp/components/admin_console/reset_password_modal.jsx
index e3fd2bf00..757f85517 100644
--- a/webapp/components/admin_console/reset_password_modal.jsx
+++ b/webapp/components/admin_console/reset_password_modal.jsx
@@ -1,12 +1,13 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import {Modal} from 'react-bootstrap';
import {injectIntl, intlShape, FormattedMessage} from 'react-intl';
+import {adminResetPassword} from 'actions/admin_actions.jsx';
+
import React from 'react';
class ResetPasswordModal extends React.Component {
@@ -32,7 +33,7 @@ class ResetPasswordModal extends React.Component {
}
this.setState({serverError: null});
- Client.adminResetPassword(
+ adminResetPassword(
this.props.user.id,
password,
() => {
diff --git a/webapp/components/admin_console/saml_settings.jsx b/webapp/components/admin_console/saml_settings.jsx
index ad7a82553..7b9ed38b8 100644
--- a/webapp/components/admin_console/saml_settings.jsx
+++ b/webapp/components/admin_console/saml_settings.jsx
@@ -12,9 +12,10 @@ import RemoveFileSetting from './remove_file_setting.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
import SettingsGroup from './settings_group.jsx';
-import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
+import {samlCertificateStatus, uploadCertificateFile, removeCertificateFile} from 'actions/admin_actions.jsx';
+
export default class SamlSettings extends AdminSettings {
constructor(props) {
super(props);
@@ -73,7 +74,7 @@ export default class SamlSettings extends AdminSettings {
}
componentWillMount() {
- Client.samlCertificateStatus(
+ samlCertificateStatus(
(data) => {
const files = {};
if (!data.IdpCertificateFile) {
@@ -93,7 +94,7 @@ export default class SamlSettings extends AdminSettings {
}
uploadCertificate(id, file, callback) {
- Client.uploadCertificateFile(
+ uploadCertificateFile(
file,
() => {
const fileName = file.name;
@@ -112,7 +113,7 @@ export default class SamlSettings extends AdminSettings {
}
removeCertificate(id, callback) {
- Client.removeCertificateFile(
+ removeCertificateFile(
this.state[id],
() => {
this.handleChange(id, '');
diff --git a/webapp/components/admin_console/sync_now_button.jsx b/webapp/components/admin_console/sync_now_button.jsx
index 95d126291..f1197b216 100644
--- a/webapp/components/admin_console/sync_now_button.jsx
+++ b/webapp/components/admin_console/sync_now_button.jsx
@@ -3,11 +3,12 @@
import React from 'react';
-import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {ldapSyncNow} from 'actions/admin_actions.jsx';
+
export default class SyncNowButton extends React.Component {
static get propTypes() {
return {
@@ -33,7 +34,7 @@ export default class SyncNowButton extends React.Component {
fail: null
});
- Client.ldapSyncNow(
+ ldapSyncNow(
() => {
this.setState({
buisy: false
diff --git a/webapp/components/admin_console/team_users.jsx b/webapp/components/admin_console/team_users.jsx
index 4517e241b..547002a5b 100644
--- a/webapp/components/admin_console/team_users.jsx
+++ b/webapp/components/admin_console/team_users.jsx
@@ -158,13 +158,17 @@ export default class UserList extends React.Component {
clearTimeout(this.searchTimeoutId);
- this.searchTimeoutId = setTimeout(
+ const searchTimeoutId = setTimeout(
() => {
searchUsers(
term,
this.props.params.team,
options,
(users) => {
+ if (searchTimeoutId !== this.searchTimeoutId) {
+ return;
+ }
+
this.setState({loading: true, search: true, users});
loadTeamMembersForProfilesList(users, this.props.params.team, this.loadComplete);
}
@@ -172,6 +176,8 @@ export default class UserList extends React.Component {
},
Constants.SEARCH_TIMEOUT_MILLISECONDS
);
+
+ this.searchTimeoutId = searchTimeoutId;
}
render() {
diff --git a/webapp/components/analytics/system_analytics.jsx b/webapp/components/analytics/system_analytics.jsx
index dd7b90260..89cc98f0b 100644
--- a/webapp/components/analytics/system_analytics.jsx
+++ b/webapp/components/analytics/system_analytics.jsx
@@ -358,6 +358,32 @@ class SystemAnalytics extends React.Component {
/>
);
+ const dailyActiveUsers = (
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.dailyActiveUsers'
+ defaultMessage='Daily Active Users'
+ />
+ }
+ icon='fa-users'
+ count={stats[StatTypes.DAILY_ACTIVE_USERS]}
+ />
+ );
+
+ const monthlyActiveUsers = (
+ <StatisticCount
+ title={
+ <FormattedMessage
+ id='analytics.system.monthlyActiveUsers'
+ defaultMessage='Monthly Active Users'
+ />
+ }
+ icon='fa-users'
+ count={stats[StatTypes.MONTHLY_ACTIVE_USERS]}
+ />
+ );
+
let firstRow;
let secondRow;
if (isLicensed && skippedIntensiveQueries) {
@@ -406,6 +432,13 @@ class SystemAnalytics extends React.Component {
);
}
+ const thirdRow = (
+ <div className='row'>
+ {dailyActiveUsers}
+ {monthlyActiveUsers}
+ </div>
+ );
+
return (
<div className='wrapper--fixed team_statistics'>
<h3>
@@ -417,6 +450,7 @@ class SystemAnalytics extends React.Component {
{banner}
{firstRow}
{secondRow}
+ {thirdRow}
{advancedStats}
{advancedGraphs}
{postTotalGraph}
diff --git a/webapp/components/authorize.jsx b/webapp/components/authorize.jsx
index 684bae589..f3f5770de 100644
--- a/webapp/components/authorize.jsx
+++ b/webapp/components/authorize.jsx
@@ -1,7 +1,6 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import Client from 'client/web_client.jsx';
import FormError from 'components/form_error.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
@@ -9,6 +8,8 @@ import React from 'react';
import icon50 from 'images/icon50x50.png';
+import {getOAuthAppInfo, allowOAuth2} from 'actions/admin_actions.jsx';
+
export default class Authorize extends React.Component {
static get propTypes() {
return {
@@ -27,7 +28,7 @@ export default class Authorize extends React.Component {
}
componentWillMount() {
- Client.getOAuthAppInfo(
+ getOAuthAppInfo(
this.props.location.query.client_id,
(app) => {
this.setState({app});
@@ -46,7 +47,7 @@ export default class Authorize extends React.Component {
handleAllow() {
const params = this.props.location.query;
- Client.allowOAuth2(params.response_type, params.client_id, params.redirect_uri, params.state, params.scope,
+ allowOAuth2(params,
(data) => {
if (data.redirect) {
window.location.href = data.redirect;
diff --git a/webapp/components/channel_header.jsx b/webapp/components/channel_header.jsx
index fc0ec132e..e83d493f4 100644
--- a/webapp/components/channel_header.jsx
+++ b/webapp/components/channel_header.jsx
@@ -29,15 +29,12 @@ import * as ChannelActions from 'actions/channel_actions.jsx';
import * as Utils from 'utils/utils.jsx';
import * as ChannelUtils from 'utils/channel_utils.jsx';
import * as TextFormatting from 'utils/text_formatting.jsx';
-import Client from 'client/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
import {getFlaggedPosts} from 'actions/post_actions.jsx';
import {Constants, Preferences, UserStatuses} from 'utils/constants.jsx';
import React from 'react';
import {FormattedMessage} from 'react-intl';
-import {browserHistory} from 'react-router/es6';
import {Tooltip, OverlayTrigger, Popover} from 'react-bootstrap';
const PreReleaseFeatures = Constants.PRE_RELEASE_FEATURES;
@@ -131,21 +128,7 @@ export default class ChannelHeader extends React.Component {
}
handleLeave() {
- Client.leaveChannel(this.state.channel.id,
- () => {
- const channelId = this.state.channel.id;
-
- if (this.state.isFavorite) {
- ChannelActions.unmarkFavorite(channelId);
- }
-
- const townsquare = ChannelStore.getByName('town-square');
- browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + townsquare.name);
- },
- (err) => {
- AsyncClient.dispatchError(err, 'handleLeave');
- }
- );
+ ChannelActions.leaveChannel(this.state.channel.id);
}
toggleFavorite = (e) => {
@@ -438,7 +421,7 @@ export default class ChannelHeader extends React.Component {
dialogProps={{channel, currentUser: this.state.currentUser}}
>
<FormattedMessage
- id='chanel_header.addMembers'
+ id='channel_header.addMembers'
defaultMessage='Add Members'
/>
</ToggleModalButton>
diff --git a/webapp/components/channel_invite_modal.jsx b/webapp/components/channel_invite_modal.jsx
index 355d23d53..5deec0794 100644
--- a/webapp/components/channel_invite_modal.jsx
+++ b/webapp/components/channel_invite_modal.jsx
@@ -117,19 +117,25 @@ export default class ChannelInviteModal extends React.Component {
clearTimeout(this.searchTimeoutId);
- this.searchTimeoutId = setTimeout(
+ const searchTimeoutId = setTimeout(
() => {
searchUsers(
term,
TeamStore.getCurrentId(),
{not_in_channel_id: this.props.channel.id},
(users) => {
+ if (searchTimeoutId !== this.searchTimeoutId) {
+ return;
+ }
+
this.setState({search: true, users});
}
);
},
Constants.SEARCH_TIMEOUT_MILLISECONDS
);
+
+ this.searchTimeoutId = searchTimeoutId;
}
render() {
diff --git a/webapp/components/channel_members_dropdown.jsx b/webapp/components/channel_members_dropdown.jsx
new file mode 100644
index 000000000..a7b3259af
--- /dev/null
+++ b/webapp/components/channel_members_dropdown.jsx
@@ -0,0 +1,246 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import ChannelStore from 'stores/channel_store.jsx';
+import TeamStore from 'stores/team_store.jsx';
+import UserStore from 'stores/user_store.jsx';
+
+import {removeUserFromChannel, makeUserChannelAdmin, makeUserChannelMember} from 'actions/channel_actions.jsx';
+
+import * as AsyncClient from 'utils/async_client.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import React from 'react';
+import {FormattedMessage} from 'react-intl';
+
+export default class ChannelMembersDropdown extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.handleRemoveFromChannel = this.handleRemoveFromChannel.bind(this);
+ this.handleMakeChannelMember = this.handleMakeChannelMember.bind(this);
+ this.handleMakeChannelAdmin = this.handleMakeChannelAdmin.bind(this);
+
+ this.state = {
+ serverError: null,
+ user: null,
+ role: null
+ };
+ }
+
+ handleRemoveFromChannel() {
+ removeUserFromChannel(
+ this.props.channel.id,
+ this.props.user.id,
+ () => {
+ AsyncClient.getChannelStats(this.props.channel.id);
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeChannelMember() {
+ makeUserChannelMember(
+ this.props.channel.id,
+ this.props.user.id,
+ () => {
+ AsyncClient.getChannelStats(this.props.channel.id);
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ handleMakeChannelAdmin() {
+ makeUserChannelAdmin(
+ this.props.channel.id,
+ this.props.user.id,
+ () => {
+ AsyncClient.getChannelStats(this.props.channel.id);
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
+ }
+
+ // Checks if the user this menu is for is a channel admin or not.
+ isChannelAdmin() {
+ if (Utils.isChannelAdmin(this.props.channelMember.roles)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ // Checks if the current user has the power to change the roles of this member.
+ canChangeMemberRoles() {
+ if (UserStore.isSystemAdminForCurrentUser()) {
+ return true;
+ } else if (TeamStore.isTeamAdminForCurrentTeam()) {
+ return true;
+ } else if (ChannelStore.isChannelAdminForCurrentChannel()) {
+ return true;
+ }
+
+ return false;
+ }
+
+ // Checks if the current user has the power to remove this member from the channel.
+ canRemoveMember() {
+ // TODO: This will be implemented as part of PLT-5047.
+ return true;
+ }
+
+ render() {
+ let serverError = null;
+ if (this.state.serverError) {
+ serverError = (
+ <div className='has-error'>
+ <label className='has-error control-label'>{this.state.serverError}</label>
+ </div>
+ );
+ }
+
+ if (this.props.user.id === UserStore.getCurrentId()) {
+ return null;
+ }
+
+ if (this.canChangeMemberRoles()) {
+ let role = (
+ <FormattedMessage
+ id='channel_members_dropdown.channel_member'
+ defaultMessage='Channel Member'
+ />
+ );
+
+ if (this.isChannelAdmin()) {
+ role = (
+ <FormattedMessage
+ id='channel_members_dropdown.channel_admin'
+ defaultMessage='Channel Admin'
+ />
+ );
+ }
+
+ let removeFromChannel = null;
+ if (this.canRemoveMember()) {
+ removeFromChannel = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleRemoveFromChannel}
+ >
+ <FormattedMessage
+ id='channel_members_dropdown.remove_from_channel'
+ defaultMessage='Remove From Channel'
+ />
+ </a>
+ </li>
+ );
+ }
+
+ let makeChannelMember = null;
+ if (this.isChannelAdmin()) {
+ makeChannelMember = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeChannelMember}
+ >
+ <FormattedMessage
+ id='channel_members_dropdown.make_channel_member'
+ defaultMessage='Make Channel Member'
+ />
+ </a>
+ </li>
+ );
+ }
+
+ let makeChannelAdmin = null;
+ if (!this.isChannelAdmin()) {
+ makeChannelAdmin = (
+ <li role='presentation'>
+ <a
+ role='menuitem'
+ href='#'
+ onClick={this.handleMakeChannelAdmin}
+ >
+ <FormattedMessage
+ id='channel_members_dropdown.make_channel_admin'
+ defaultMessage='Make Channel Admin'
+ />
+ </a>
+ </li>
+ );
+ }
+
+ return (
+ <div className='dropdown member-drop'>
+ <a
+ href='#'
+ className='dropdown-toggle theme'
+ type='button'
+ data-toggle='dropdown'
+ aria-expanded='true'
+ >
+ <span>{role} </span>
+ <span className='fa fa-chevron-down'/>
+ </a>
+ <ul
+ className='dropdown-menu member-menu'
+ role='menu'
+ >
+ {makeChannelMember}
+ {makeChannelAdmin}
+ {removeFromChannel}
+ </ul>
+ {serverError}
+ </div>
+ );
+ } else if (this.canRemoveMember()) {
+ return (
+ <button
+ type='button'
+ className='btn btn-danger btn-message'
+ onClick={this.handleRemoveFromChannel}
+ >
+ <FormattedMessage
+ id='channel_members_dropdown.remove_member'
+ defaultMessage='Remove Member'
+ />
+ </button>
+ );
+ } else if (this.isChannelAdmin()) {
+ return (
+ <div>
+ <FormattedMessage
+ id='channel_members_dropdown.channel_admin'
+ defaultMessage='Channel Admin'
+ />
+ </div>
+ );
+ }
+
+ return (
+ <div>
+ <FormattedMessage
+ id='channel_members_dropdown.channel_member'
+ defaultMessage='Channel Member'
+ />
+ </div>
+ );
+ }
+}
+
+ChannelMembersDropdown.propTypes = {
+ channel: React.PropTypes.object.isRequired,
+ user: React.PropTypes.object.isRequired,
+ teamMember: React.PropTypes.object.isRequired,
+ channelMember: React.PropTypes.object.isRequired
+};
diff --git a/webapp/components/channel_members_modal.jsx b/webapp/components/channel_members_modal.jsx
index 351efed96..96d90e5cc 100644
--- a/webapp/components/channel_members_modal.jsx
+++ b/webapp/components/channel_members_modal.jsx
@@ -1,170 +1,29 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import SearchableUserList from './searchable_user_list.jsx';
-import LoadingScreen from './loading_screen.jsx';
-
-import UserStore from 'stores/user_store.jsx';
-import ChannelStore from 'stores/channel_store.jsx';
-import TeamStore from 'stores/team_store.jsx';
-
-import {searchUsers} from 'actions/user_actions.jsx';
-import {removeUserFromChannel} from 'actions/channel_actions.jsx';
-
-import * as AsyncClient from 'utils/async_client.jsx';
-import * as UserAgent from 'utils/user_agent.jsx';
-import Constants from 'utils/constants.jsx';
+import MemberListChannel from './member_list_channel.jsx';
import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
-const USERS_PER_PAGE = 50;
-
export default class ChannelMembersModal extends React.Component {
constructor(props) {
super(props);
- this.onChange = this.onChange.bind(this);
- this.onStatusChange = this.onStatusChange.bind(this);
this.onHide = this.onHide.bind(this);
- this.handleRemove = this.handleRemove.bind(this);
- this.createRemoveMemberButton = this.createRemoveMemberButton.bind(this);
- this.search = this.search.bind(this);
- this.nextPage = this.nextPage.bind(this);
-
- this.term = '';
- this.searchTimeoutId = 0;
-
- const stats = ChannelStore.getStats(props.channel.id);
this.state = {
- users: [],
- total: stats.member_count,
- show: true,
- search: false,
- statusChange: false
+ channel: this.props.channel,
+ show: true
};
}
- componentDidMount() {
- ChannelStore.addStatsChangeListener(this.onChange);
- UserStore.addInChannelChangeListener(this.onChange);
- UserStore.addStatusesChangeListener(this.onStatusChange);
-
- AsyncClient.getProfilesInChannel(this.props.channel.id, 0);
- }
-
- componentWillUnmount() {
- ChannelStore.removeStatsChangeListener(this.onChange);
- UserStore.removeInChannelChangeListener(this.onChange);
- UserStore.removeStatusesChangeListener(this.onStatusChange);
- }
-
- onChange(force) {
- if (this.state.search && !force) {
- this.search(this.term);
- return;
- }
-
- const stats = ChannelStore.getStats(this.props.channel.id);
- this.setState({
- users: UserStore.getProfileListInChannel(this.props.channel.id),
- total: stats.member_count
- });
- }
-
- onStatusChange() {
- // Initiate a render to pick up on new statuses
- this.setState({
- statusChange: !this.state.statusChange
- });
- }
-
onHide() {
this.setState({show: false});
}
- handleRemove(user) {
- const userId = user.id;
-
- removeUserFromChannel(
- this.props.channel.id,
- userId,
- null,
- (err) => {
- this.setState({inviteError: err.message});
- }
- );
- }
-
- createRemoveMemberButton({user}) {
- if (user.id === UserStore.getCurrentId()) {
- return null;
- }
-
- return (
- <button
- type='button'
- className='btn btn-primary btn-message'
- onClick={this.handleRemove.bind(this, user)}
- >
- <FormattedMessage
- id='channel_members_modal.remove'
- defaultMessage='Remove'
- />
- </button>
- );
- }
-
- nextPage(page) {
- AsyncClient.getProfilesInChannel(this.props.channel.id, (page + 1) * USERS_PER_PAGE, USERS_PER_PAGE);
- }
-
- search(term) {
- this.term = term;
-
- if (term === '') {
- this.onChange(true);
- this.setState({search: false});
- return;
- }
-
- clearTimeout(this.searchTimeoutId);
-
- this.searchTimeoutId = setTimeout(
- () => {
- searchUsers(
- term,
- TeamStore.getCurrentId(),
- {in_channel_id: this.props.channel.id},
- (users) => {
- this.setState({search: true, users});
- }
- );
- },
- Constants.SEARCH_TIMEOUT_MILLISECONDS
- );
- }
-
render() {
- let content;
- if (this.state.loading) {
- content = (<LoadingScreen/>);
- } else {
- content = (
- <SearchableUserList
- users={this.state.users}
- usersPerPage={USERS_PER_PAGE}
- total={this.state.total}
- nextPage={this.nextPage}
- search={this.search}
- actions={[this.createRemoveMemberButton]}
- focusOnMount={!UserAgent.isMobile()}
- />
- );
- }
-
return (
<div>
<Modal
@@ -177,7 +36,7 @@ export default class ChannelMembersModal extends React.Component {
<Modal.Title>
<span className='name'>{this.props.channel.display_name}</span>
<FormattedMessage
- id='channel_memebers_modal.members'
+ id='channel_members_modal.members'
defaultMessage=' Members'
/>
</Modal.Title>
@@ -198,7 +57,9 @@ export default class ChannelMembersModal extends React.Component {
<Modal.Body
ref='modalBody'
>
- {content}
+ <MemberListChannel
+ channel={this.props.channel}
+ />
</Modal.Body>
</Modal>
</div>
diff --git a/webapp/components/channel_select.jsx b/webapp/components/channel_select.jsx
index 59bf2f15a..194de3874 100644
--- a/webapp/components/channel_select.jsx
+++ b/webapp/components/channel_select.jsx
@@ -31,12 +31,13 @@ export default class ChannelSelect extends React.Component {
super(props);
this.handleChannelChange = this.handleChannelChange.bind(this);
+ this.filterChannels = this.filterChannels.bind(this);
this.compareByDisplayName = this.compareByDisplayName.bind(this);
AsyncClient.getMoreChannels(true);
this.state = {
- channels: ChannelStore.getAll().sort(this.compareByDisplayName)
+ channels: ChannelStore.getAll().filter(this.filterChannels).sort(this.compareByDisplayName)
};
}
@@ -50,10 +51,19 @@ export default class ChannelSelect extends React.Component {
handleChannelChange() {
this.setState({
- channels: ChannelStore.getAll().concat(ChannelStore.getMoreAll()).sort(this.compareByDisplayName)
+ channels: ChannelStore.getAll().concat(ChannelStore.getMoreAll()).
+ filter(this.filterChannels).sort(this.compareByDisplayName)
});
}
+ filterChannels(channel) {
+ if (channel.display_name) {
+ return true;
+ }
+
+ return false;
+ }
+
compareByDisplayName(channelA, channelB) {
return channelA.display_name.localeCompare(channelB.display_name);
}
diff --git a/webapp/components/channel_switch_modal.jsx b/webapp/components/channel_switch_modal.jsx
index 2f8595c78..fc66e06b1 100644
--- a/webapp/components/channel_switch_modal.jsx
+++ b/webapp/components/channel_switch_modal.jsx
@@ -64,6 +64,7 @@ export default class SwitchChannelModal extends React.Component {
}
onExited() {
+ this.selected = null;
setTimeout(() => {
$('#post_textbox').get(0).focus();
});
@@ -71,6 +72,7 @@ export default class SwitchChannelModal extends React.Component {
onChange(e) {
this.setState({text: e.target.value});
+ this.selected = null;
}
onItemSelected(item) {
@@ -89,6 +91,15 @@ export default class SwitchChannelModal extends React.Component {
handleSubmit() {
let channel = null;
+ if (!this.selected) {
+ if (this.state.text !== '') {
+ this.setState({
+ error: Utils.localizeMessage('channel_switch_modal.not_found', 'No matches found.')
+ });
+ }
+ return;
+ }
+
if (this.selected.type === Constants.DM_CHANNEL) {
const user = UserStore.getProfileByUsername(this.selected.name);
@@ -117,7 +128,7 @@ export default class SwitchChannelModal extends React.Component {
this.onHide();
} else if (this.state.text !== '') {
this.setState({
- error: Utils.localizeMessage('channel_switch_modal.not_found', 'No matches found.')
+ error: Utils.localizeMessage('channel_switch_modal.failed_to_open', 'Failed to open channel.')
});
}
}
diff --git a/webapp/components/claim/components/email_to_ldap.jsx b/webapp/components/claim/components/email_to_ldap.jsx
index 890512803..7d062a957 100644
--- a/webapp/components/claim/components/email_to_ldap.jsx
+++ b/webapp/components/claim/components/email_to_ldap.jsx
@@ -4,9 +4,9 @@
import LoginMfa from 'components/login/components/login_mfa.jsx';
import * as Utils from 'utils/utils.jsx';
-import Client from 'client/web_client.jsx';
import {checkMfa} from 'actions/user_actions.jsx';
+import {emailToLdap} from 'actions/admin_actions.jsx';
import React from 'react';
import {FormattedMessage} from 'react-intl';
@@ -79,7 +79,7 @@ export default class EmailToLDAP extends React.Component {
}
submit(loginId, password, token, ldapId, ldapPassword) {
- Client.emailToLdap(
+ emailToLdap(
loginId,
password,
token,
diff --git a/webapp/components/claim/components/email_to_oauth.jsx b/webapp/components/claim/components/email_to_oauth.jsx
index 3cede15a3..bc5a7bdaa 100644
--- a/webapp/components/claim/components/email_to_oauth.jsx
+++ b/webapp/components/claim/components/email_to_oauth.jsx
@@ -4,10 +4,10 @@
import LoginMfa from 'components/login/components/login_mfa.jsx';
import * as Utils from 'utils/utils.jsx';
-import Client from 'client/web_client.jsx';
import Constants from 'utils/constants.jsx';
import {checkMfa} from 'actions/user_actions.jsx';
+import {emailToOAuth} from 'actions/admin_actions.jsx';
import React from 'react';
import ReactDOM from 'react-dom';
@@ -55,7 +55,7 @@ export default class EmailToOAuth extends React.Component {
}
submit(loginId, password, token) {
- Client.emailToOAuth(
+ emailToOAuth(
loginId,
password,
token,
diff --git a/webapp/components/claim/components/oauth_to_email.jsx b/webapp/components/claim/components/oauth_to_email.jsx
index ed604583d..ffba1c331 100644
--- a/webapp/components/claim/components/oauth_to_email.jsx
+++ b/webapp/components/claim/components/oauth_to_email.jsx
@@ -2,13 +2,13 @@
// See License.txt for license information.
import * as Utils from 'utils/utils.jsx';
-import Client from 'client/web_client.jsx';
import Constants from 'utils/constants.jsx';
import React from 'react';
import ReactDOM from 'react-dom';
import {FormattedMessage} from 'react-intl';
-import {browserHistory} from 'react-router/es6';
+
+import {oauthToEmail} from 'actions/admin_actions.jsx';
export default class OAuthToEmail extends React.Component {
constructor(props) {
@@ -48,14 +48,10 @@ export default class OAuthToEmail extends React.Component {
state.error = null;
this.setState(state);
- Client.oauthToEmail(
+ oauthToEmail(
this.props.email,
password,
- (data) => {
- if (data.follow_link) {
- browserHistory.push(data.follow_link);
- }
- },
+ null,
(err) => {
this.setState({error: err.message});
}
diff --git a/webapp/components/create_comment.jsx b/webapp/components/create_comment.jsx
index 0e9d2a41a..d9d66c8fa 100644
--- a/webapp/components/create_comment.jsx
+++ b/webapp/components/create_comment.jsx
@@ -331,6 +331,9 @@ export default class CreateComment extends React.Component {
draft.fileInfos = draft.fileInfos.concat(fileInfos);
PostStore.storeCommentDraft(this.props.rootId, draft);
+ // Focus on preview if needed
+ this.refs.preview.refs.container.scrollIntoViewIfNeeded();
+
this.setState({uploadsInProgress: draft.uploadsInProgress, fileInfos: draft.fileInfos});
}
@@ -355,6 +358,9 @@ export default class CreateComment extends React.Component {
const fileInfos = this.state.fileInfos;
const uploadsInProgress = this.state.uploadsInProgress;
+ // Clear previous errors
+ this.handleUploadError(null);
+
// id can either be the id of an uploaded file or the client id of an in progress upload
let index = fileInfos.findIndex((info) => info.id === id);
if (index === -1) {
@@ -432,6 +438,7 @@ export default class CreateComment extends React.Component {
fileInfos={this.state.fileInfos}
onRemove={this.removePreview}
uploadsInProgress={this.state.uploadsInProgress}
+ ref='preview'
/>
);
}
diff --git a/webapp/components/create_post.jsx b/webapp/components/create_post.jsx
index e1b2ca059..9269633ff 100644
--- a/webapp/components/create_post.jsx
+++ b/webapp/components/create_post.jsx
@@ -297,6 +297,9 @@ export default class CreatePost extends React.Component {
const fileInfos = Object.assign([], this.state.fileInfos);
const uploadsInProgress = this.state.uploadsInProgress;
+ // Clear previous errors
+ this.handleUploadError(null);
+
// id can either be the id of an uploaded file or the client id of an in progress upload
let index = fileInfos.findIndex((info) => info.id === id);
if (index === -1) {
diff --git a/webapp/components/delete_channel_modal.jsx b/webapp/components/delete_channel_modal.jsx
index 1b642861a..a6577a4a9 100644
--- a/webapp/components/delete_channel_modal.jsx
+++ b/webapp/components/delete_channel_modal.jsx
@@ -1,8 +1,6 @@
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
-import * as AsyncClient from 'utils/async_client.jsx';
-import Client from 'client/web_client.jsx';
import {Modal} from 'react-bootstrap';
import TeamStore from 'stores/team_store.jsx';
import Constants from 'utils/constants.jsx';
@@ -13,7 +11,7 @@ import {browserHistory} from 'react-router/es6';
import React from 'react';
-import {loadChannelsForCurrentUser} from 'actions/channel_actions.jsx';
+import {deleteChannel} from 'actions/channel_actions.jsx';
export default class DeleteChannelModal extends React.Component {
constructor(props) {
@@ -31,15 +29,7 @@ export default class DeleteChannelModal extends React.Component {
}
browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/town-square');
- Client.deleteChannel(
- this.props.channel.id,
- () => {
- loadChannelsForCurrentUser();
- },
- (err) => {
- AsyncClient.dispatchError(err, 'handleDelete');
- }
- );
+ deleteChannel(this.props.channel.id);
}
onHide() {
diff --git a/webapp/components/delete_post_modal.jsx b/webapp/components/delete_post_modal.jsx
index 84eef4671..39d4f41f9 100644
--- a/webapp/components/delete_post_modal.jsx
+++ b/webapp/components/delete_post_modal.jsx
@@ -3,13 +3,9 @@
import $ from 'jquery';
import ReactDOM from 'react-dom';
-import Client from 'client/web_client.jsx';
-import PostStore from 'stores/post_store.jsx';
-import ModalStore from 'stores/modal_store.jsx';
import {Modal} from 'react-bootstrap';
-import * as AsyncClient from 'utils/async_client.jsx';
-import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
-import {removePostFromStore} from 'actions/post_actions.jsx';
+import ModalStore from 'stores/modal_store.jsx';
+import {deletePost} from 'actions/post_actions.jsx';
import Constants from 'utils/constants.jsx';
import {FormattedMessage} from 'react-intl';
@@ -51,24 +47,16 @@ export default class DeletePostModal extends React.Component {
}
handleDelete() {
- Client.deletePost(
+ deletePost(
this.state.post.channel_id,
- this.state.post.id,
+ this.state.post,
() => {
- removePostFromStore(this.state.post);
- if (this.state.post.id === PostStore.getSelectedPostId()) {
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_POST_SELECTED,
- postId: null
- });
- }
+ this.handleHide();
},
(err) => {
- AsyncClient.dispatchError(err, 'deletePost');
+ this.setState({error: err.message});
}
);
-
- this.handleHide();
}
handleToggle(value, args) {
diff --git a/webapp/components/do_verify_email.jsx b/webapp/components/do_verify_email.jsx
index e0ac3218e..9b6a9ccad 100644
--- a/webapp/components/do_verify_email.jsx
+++ b/webapp/components/do_verify_email.jsx
@@ -2,11 +2,12 @@
// See License.txt for license information.
import {FormattedMessage} from 'react-intl';
-import Client from 'client/web_client.jsx';
import LoadingScreen from './loading_screen.jsx';
import {browserHistory, Link} from 'react-router/es6';
+import {verifyEmail} from 'actions/user_actions.jsx';
+
import React from 'react';
export default class DoVerifyEmail extends React.Component {
@@ -19,15 +20,11 @@ export default class DoVerifyEmail extends React.Component {
};
}
componentWillMount() {
- const uid = this.props.location.query.uid;
- const hid = this.props.location.query.hid;
- const email = this.props.location.query.email;
-
- Client.verifyEmail(
- uid,
- hid,
+ verifyEmail(
+ this.props.location.query.uid,
+ this.props.location.query.hid,
() => {
- browserHistory.push('/login?extra=verified&email=' + email);
+ browserHistory.push('/login?extra=verified&email=' + this.props.location.query.email);
},
(err) => {
this.setState({verifyStatus: 'failure', serverError: err.message});
diff --git a/webapp/components/edit_channel_header_modal.jsx b/webapp/components/edit_channel_header_modal.jsx
index 490b9fb31..0d8eb8acb 100644
--- a/webapp/components/edit_channel_header_modal.jsx
+++ b/webapp/components/edit_channel_header_modal.jsx
@@ -2,13 +2,12 @@
// See License.txt for license information.
import ReactDOM from 'react-dom';
-import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
-import Client from 'client/web_client.jsx';
import Constants from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
+import {updateChannelHeader} from 'actions/channel_actions.jsx';
import {Modal} from 'react-bootstrap';
@@ -64,17 +63,12 @@ class EditChannelHeaderModal extends React.Component {
handleSubmit() {
this.setState({submitted: true});
- Client.updateChannelHeader(
+ updateChannelHeader(
this.props.channel.id,
this.state.header,
- (channel) => {
+ () => {
this.setState({serverError: ''});
this.onHide();
-
- AppDispatcher.handleServerAction({
- type: Constants.ActionTypes.RECEIVED_CHANNEL,
- channel
- });
},
(err) => {
if (err.id === 'api.context.invalid_param.app_error') {
diff --git a/webapp/components/edit_channel_purpose_modal.jsx b/webapp/components/edit_channel_purpose_modal.jsx
index 7ba2eff2c..4bb876460 100644
--- a/webapp/components/edit_channel_purpose_modal.jsx
+++ b/webapp/components/edit_channel_purpose_modal.jsx
@@ -3,14 +3,13 @@
import PreferenceStore from 'stores/preference_store.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
-import Client from 'client/web_client.jsx';
import Constants from 'utils/constants.jsx';
import * as Utils from 'utils/utils.jsx';
import React from 'react';
import {Modal} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
+import {updateChannelPurpose} from 'actions/channel_actions.jsx';
export default class EditChannelPurposeModal extends React.Component {
constructor(props) {
@@ -64,12 +63,10 @@ export default class EditChannelPurposeModal extends React.Component {
this.setState({submitted: true});
- Client.updateChannelPurpose(
+ updateChannelPurpose(
this.props.channel.id,
this.refs.purpose.value.trim(),
() => {
- AsyncClient.getChannel(this.props.channel.id);
-
this.handleHide();
},
(err) => {
diff --git a/webapp/components/edit_post_modal.jsx b/webapp/components/edit_post_modal.jsx
index 2108ec3d1..b2b607428 100644
--- a/webapp/components/edit_post_modal.jsx
+++ b/webapp/components/edit_post_modal.jsx
@@ -9,11 +9,9 @@ import MessageHistoryStore from 'stores/message_history_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
-import {loadPosts} from 'actions/post_actions.jsx';
+import {updatePost} from 'actions/post_actions.jsx';
-import Client from 'client/web_client.jsx';
import * as UserAgent from 'utils/user_agent.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
const KeyCodes = Constants.KeyCodes;
@@ -91,15 +89,12 @@ export default class EditPostModal extends React.Component {
return;
}
- Client.updatePost(
+ updatePost(
updatedPost,
() => {
- loadPosts(updatedPost.channel_id);
window.scrollTo(0, 0);
},
- (err) => {
- AsyncClient.dispatchError(err, 'updatePost');
- }
+ Boolean(PostStore.getFocusedPostId()) // If there is focused post we need to update that post's store too.
);
$('#edit_post').modal('hide');
@@ -125,6 +120,17 @@ export default class EditPostModal extends React.Component {
}
handleEditPostEvent(options) {
+ var post = PostStore.getPost(options.channelId, options.postId);
+ if (global.window.mm_license.IsLicensed === 'true') {
+ if (global.window.mm_config.AllowEditPost === Constants.ALLOW_EDIT_POST_NEVER) {
+ return;
+ }
+ if (global.window.mm_config.AllowEditPost === Constants.ALLOW_EDIT_POST_TIME_LIMIT) {
+ if ((post.create_at + (global.window.mm_config.PostEditTimeLimit * 1000)) < Utils.getTimestamp()) {
+ return;
+ }
+ }
+ }
this.setState({
editText: options.message || '',
originalText: options.message || '',
@@ -180,7 +186,10 @@ export default class EditPostModal extends React.Component {
onModalHide() {
if (this.state.refocusId !== '') {
setTimeout(() => {
- $(this.state.refocusId).get(0).focus();
+ const element = $(this.state.refocusId).get(0);
+ if (element) {
+ element.focus();
+ }
});
}
}
diff --git a/webapp/components/file_attachment_list.jsx b/webapp/components/file_attachment_list.jsx
index 3d39d8709..472cd2686 100644
--- a/webapp/components/file_attachment_list.jsx
+++ b/webapp/components/file_attachment_list.jsx
@@ -50,7 +50,7 @@ export default class FileAttachmentList extends React.Component {
return (
<div>
- <div className='post-image__columns'>
+ <div className='post-image__columns clearfix'>
{postFiles}
</div>
<ViewImageModal
diff --git a/webapp/components/file_preview.jsx b/webapp/components/file_preview.jsx
index 53cec7f7b..624bfaf44 100644
--- a/webapp/components/file_preview.jsx
+++ b/webapp/components/file_preview.jsx
@@ -84,7 +84,10 @@ export default class FilePreview extends React.Component {
});
return (
- <div className='file-preview__container'>
+ <div
+ className='file-preview__container'
+ ref='container'
+ >
{previews}
</div>
);
diff --git a/webapp/components/file_upload.jsx b/webapp/components/file_upload.jsx
index 9eff25ab5..a821fedab 100644
--- a/webapp/components/file_upload.jsx
+++ b/webapp/components/file_upload.jsx
@@ -4,7 +4,6 @@
import $ from 'jquery';
import 'jquery-dragster/jquery.dragster.js';
import ReactDOM from 'react-dom';
-import Client from 'client/web_client.jsx';
import Constants from 'utils/constants.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import DelayedAction from 'utils/delayed_action.jsx';
@@ -13,6 +12,8 @@ import * as Utils from 'utils/utils.jsx';
import {intlShape, injectIntl, defineMessages} from 'react-intl';
+import {uploadFile} from 'actions/file_actions.jsx';
+
const holders = defineMessages({
limited: {
id: 'file_upload.limited',
@@ -47,6 +48,7 @@ class FileUpload extends React.Component {
this.cancelUpload = this.cancelUpload.bind(this);
this.pasteUpload = this.pasteUpload.bind(this);
this.keyUpload = this.keyUpload.bind(this);
+ this.handleMaxUploadReached = this.handleMaxUploadReached.bind(this);
this.state = {
requests: {}
@@ -88,13 +90,14 @@ class FileUpload extends React.Component {
// generate a unique id that can be used by other components to refer back to this upload
const clientId = Utils.generateId();
- const request = Client.uploadFile(files[i],
- files[i].name,
- channelId,
- clientId,
- this.fileUploadSuccess.bind(this, channelId),
- this.fileUploadFail.bind(this, clientId, channelId)
- );
+ const request = uploadFile(
+ files[i],
+ files[i].name,
+ channelId,
+ clientId,
+ this.fileUploadSuccess.bind(this, channelId),
+ this.fileUploadFail.bind(this, clientId)
+ );
const requests = this.state.requests;
requests[clientId] = request;
@@ -270,7 +273,8 @@ class FileUpload extends React.Component {
const name = formatMessage(holders.pasted) + d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate() + ' ' + hour + '-' + min + '.' + ext;
- const request = Client.uploadFile(file,
+ const request = uploadFile(
+ file,
name,
channelId,
clientId,
@@ -309,6 +313,16 @@ class FileUpload extends React.Component {
}
}
+ handleMaxUploadReached(e) {
+ e.preventDefault();
+
+ const {formatMessage} = this.props.intl;
+
+ this.props.onUploadError(formatMessage(holders.limited, {count: Constants.MAX_UPLOAD_FILES}));
+
+ return false;
+ }
+
render() {
let multiple = true;
if (UserAgent.isMobileApp()) {
@@ -322,10 +336,14 @@ class FileUpload extends React.Component {
accept = 'image/*';
}
+ const channelId = this.props.channelId || ChannelStore.getCurrentId();
+
+ const uploadsRemaining = Constants.MAX_UPLOAD_FILES - this.props.getFileCount(channelId);
+
return (
<span
ref='input'
- className='btn btn-file'
+ className={'btn btn-file' + (uploadsRemaining <= 0 ? ' btn-file__disabled' : '')}
>
<span
className='icon'
@@ -335,7 +353,7 @@ class FileUpload extends React.Component {
ref='fileInput'
type='file'
onChange={this.handleChange}
- onClick={this.props.onClick}
+ onClick={uploadsRemaining > 0 ? this.props.onClick : this.handleMaxUploadReached}
multiple={multiple}
accept={accept}
/>
diff --git a/webapp/components/integrations/components/installed_oauth_app.jsx b/webapp/components/integrations/components/installed_oauth_app.jsx
index 15a79ed4c..a6dea65bf 100644
--- a/webapp/components/integrations/components/installed_oauth_app.jsx
+++ b/webapp/components/integrations/components/installed_oauth_app.jsx
@@ -5,10 +5,10 @@ import React from 'react';
import FormError from 'components/form_error.jsx';
-import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {regenerateOAuthAppSecret} from 'actions/admin_actions.jsx';
const FAKE_SECRET = '***************';
@@ -49,7 +49,7 @@ export default class InstalledOAuthApp extends React.Component {
handleRegenerate(e) {
e.preventDefault();
- Client.regenerateOAuthAppSecret(
+ regenerateOAuthAppSecret(
this.props.oauthApp.id,
(data) => {
this.props.oauthApp.client_secret = data.client_secret;
diff --git a/webapp/components/invite_member_modal.jsx b/webapp/components/invite_member_modal.jsx
index f4fd1d712..563c1aba9 100644
--- a/webapp/components/invite_member_modal.jsx
+++ b/webapp/components/invite_member_modal.jsx
@@ -5,13 +5,13 @@ import ReactDOM from 'react-dom';
import * as utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
const ActionTypes = Constants.ActionTypes;
-import Client from 'client/web_client.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import ModalStore from 'stores/modal_store.jsx';
import UserStore from 'stores/user_store.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import TeamStore from 'stores/team_store.jsx';
import ConfirmModal from './confirm_modal.jsx';
+import {inviteMembers} from 'actions/team_actions.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
@@ -142,7 +142,7 @@ class InviteMemberModal extends React.Component {
this.setState({isSendingEmails: true});
- Client.inviteMembers(
+ inviteMembers(
data,
() => {
this.handleHide(false);
diff --git a/webapp/components/logged_in.jsx b/webapp/components/logged_in.jsx
index 841061d48..9282e74ca 100644
--- a/webapp/components/logged_in.jsx
+++ b/webapp/components/logged_in.jsx
@@ -4,7 +4,6 @@
import LoadingScreen from 'components/loading_screen.jsx';
import UserStore from 'stores/user_store.jsx';
-import BrowserStore from 'stores/browser_store.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
@@ -29,30 +28,6 @@ export default class LoggedIn extends React.Component {
this.onUserChanged = this.onUserChanged.bind(this);
this.setupUser = this.setupUser.bind(this);
- // Force logout of all tabs if one tab is logged out
- $(window).bind('storage', (e) => {
- // when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out
- if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
- // make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
- if (BrowserStore.isSignallingLogout(e.originalEvent.newValue)) {
- return;
- }
-
- console.log('detected logout from a different tab'); //eslint-disable-line no-console
- GlobalActions.emitUserLoggedOutEvent('/', false);
- }
-
- if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
- // make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
- if (BrowserStore.isSignallingLogin(e.originalEvent.newValue)) {
- return;
- }
-
- console.log('detected login from a different tab'); //eslint-disable-line no-console
- location.reload();
- }
- });
-
// Because current CSS requires the root tag to have specific stuff
$('#root').attr('class', 'channel-view');
diff --git a/webapp/components/login/login_controller.jsx b/webapp/components/login/login_controller.jsx
index 6dc7af883..535cdfd12 100644
--- a/webapp/components/login/login_controller.jsx
+++ b/webapp/components/login/login_controller.jsx
@@ -6,6 +6,8 @@ import ErrorBar from 'components/error_bar.jsx';
import FormError from 'components/form_error.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
+import {addUserToTeamFromInvite} from 'actions/team_actions.jsx';
+import {checkMfa, webLogin} from 'actions/user_actions.jsx';
import BrowserStore from 'stores/browser_store.jsx';
import UserStore from 'stores/user_store.jsx';
@@ -47,7 +49,8 @@ export default class LoginController extends React.Component {
samlEnabled: global.window.mm_license.IsLicensed === 'true' && global.window.mm_config.EnableSaml === 'true',
loginId: '', // the browser will set a default for this
password: '',
- showMfa: false
+ showMfa: false,
+ loading: false
};
}
@@ -117,40 +120,38 @@ export default class LoginController extends React.Component {
return;
}
- if (global.window.mm_config.EnableMultifactorAuthentication === 'true') {
- Client.checkMfa(
- loginId,
- (data) => {
- if (data.mfa_required === 'true') {
- this.setState({showMfa: true});
- } else {
- this.submit(loginId, password, '');
- }
- },
- (err) => {
- this.setState({serverError: err.message});
+ checkMfa(
+ loginId,
+ (data) => {
+ if (data && data.mfa_required === 'true') {
+ this.setState({showMfa: true});
+ } else {
+ this.submit(loginId, password, '');
}
- );
- } else {
- this.submit(loginId, password, '');
- }
+ },
+ (err) => {
+ this.setState({serverError: err.message});
+ }
+ );
}
submit(loginId, password, token) {
- this.setState({serverError: null});
+ this.setState({serverError: null, loading: true});
- Client.webLogin(
+ webLogin(
loginId,
password,
token,
() => {
// check for query params brought over from signup_user_complete
- const query = this.props.location.query;
- if (query.id || query.h) {
- Client.addUserToTeamFromInvite(
- query.d,
- query.h,
- query.id,
+ const hash = this.props.location.query.h;
+ const data = this.props.location.query.d;
+ const inviteId = this.props.location.query.id;
+ if (inviteId || hash) {
+ addUserToTeamFromInvite(
+ data,
+ hash,
+ inviteId,
(team) => {
this.finishSignin(team);
},
@@ -172,6 +173,7 @@ export default class LoginController extends React.Component {
err.id === 'ent.ldap.do_login.user_not_registered.app_error') {
this.setState({
showMfa: false,
+ loading: false,
serverError: (
<FormattedMessage
id='login.userNotFound'
@@ -182,6 +184,7 @@ export default class LoginController extends React.Component {
} else if (err.id === 'api.user.check_user_password.invalid.app_error' || err.id === 'ent.ldap.do_login.invalid_password.app_error') {
this.setState({
showMfa: false,
+ loading: false,
serverError: (
<FormattedMessage
id='login.invalidPassword'
@@ -190,7 +193,7 @@ export default class LoginController extends React.Component {
)
});
} else {
- this.setState({showMfa: false, serverError: err.message});
+ this.setState({showMfa: false, serverError: err.message, loading: false});
}
}
);
@@ -348,6 +351,23 @@ export default class LoginController extends React.Component {
errorClass = ' has-error';
}
+ let loginButton =
+ (<FormattedMessage
+ id='login.signIn'
+ defaultMessage='Sign in'
+ />);
+
+ if (this.state.loading) {
+ loginButton =
+ (<span>
+ <span className='fa fa-refresh icon--rotate'/>
+ <FormattedMessage
+ id='login.signInLoading'
+ defaultMessage='Signing in...'
+ />
+ </span>);
+ }
+
loginControls.push(
<form
key='loginBoxes'
@@ -387,10 +407,7 @@ export default class LoginController extends React.Component {
type='submit'
className='btn btn-primary'
>
- <FormattedMessage
- id='login.signIn'
- defaultMessage='Sign in'
- />
+ { loginButton }
</button>
</div>
</div>
diff --git a/webapp/components/member_list_channel.jsx b/webapp/components/member_list_channel.jsx
new file mode 100644
index 000000000..6f8a266ad
--- /dev/null
+++ b/webapp/components/member_list_channel.jsx
@@ -0,0 +1,179 @@
+// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import ChannelMembersDropdown from 'components/channel_members_dropdown.jsx';
+import SearchableUserList from 'components/searchable_user_list.jsx';
+
+import ChannelStore from 'stores/channel_store.jsx';
+import UserStore from 'stores/user_store.jsx';
+import TeamStore from 'stores/team_store.jsx';
+
+import {searchUsers, loadProfilesAndTeamMembersAndChannelMembers, loadTeamMembersAndChannelMembersForProfilesList} from 'actions/user_actions.jsx';
+import {getChannelStats} from 'utils/async_client.jsx';
+
+import Constants from 'utils/constants.jsx';
+
+import * as UserAgent from 'utils/user_agent.jsx';
+
+import React from 'react';
+
+const USERS_PER_PAGE = 50;
+
+export default class MemberListChannel extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onChange = this.onChange.bind(this);
+ this.onStatsChange = this.onStatsChange.bind(this);
+ this.search = this.search.bind(this);
+ this.loadComplete = this.loadComplete.bind(this);
+
+ this.searchTimeoutId = 0;
+
+ const stats = ChannelStore.getCurrentStats();
+
+ this.state = {
+ users: UserStore.getProfileListInChannel(),
+ teamMembers: Object.assign({}, TeamStore.getMembersInTeam()),
+ channelMembers: Object.assign({}, ChannelStore.getMembersInChannel()),
+ total: stats.member_count,
+ search: false,
+ term: '',
+ loading: true
+ };
+ }
+
+ componentDidMount() {
+ UserStore.addInTeamChangeListener(this.onChange);
+ UserStore.addStatusesChangeListener(this.onChange);
+ TeamStore.addChangeListener(this.onChange);
+ ChannelStore.addChangeListener(this.onChange);
+ ChannelStore.addStatsChangeListener(this.onStatsChange);
+
+ loadProfilesAndTeamMembersAndChannelMembers(0, Constants.PROFILE_CHUNK_SIZE, TeamStore.getCurrentId(), ChannelStore.getCurrentId(), this.loadComplete);
+ getChannelStats(ChannelStore.getCurrentId());
+ }
+
+ componentWillUnmount() {
+ UserStore.removeInTeamChangeListener(this.onChange);
+ UserStore.removeStatusesChangeListener(this.onChange);
+ TeamStore.removeChangeListener(this.onChange);
+ ChannelStore.removeChangeListener(this.onChange);
+ ChannelStore.removeStatsChangeListener(this.onStatsChange);
+ }
+
+ loadComplete() {
+ this.setState({loading: false});
+ }
+
+ onChange(force) {
+ if (this.state.search && !force) {
+ return;
+ } else if (this.state.search) {
+ this.search(this.state.term);
+ return;
+ }
+
+ this.setState({
+ users: UserStore.getProfileListInChannel(),
+ teamMembers: Object.assign({}, TeamStore.getMembersInTeam()),
+ channelMembers: Object.assign({}, ChannelStore.getMembersInChannel())
+ });
+ }
+
+ onStatsChange() {
+ const stats = ChannelStore.getCurrentStats();
+ this.setState({total: stats.member_count});
+ }
+
+ nextPage(page) {
+ loadProfilesAndTeamMembersAndChannelMembers((page + 1) * USERS_PER_PAGE, USERS_PER_PAGE);
+ }
+
+ search(term) {
+ if (term === '') {
+ this.setState({
+ search: false,
+ term,
+ users: UserStore.getProfileListInChannel(),
+ teamMembers: Object.assign([], TeamStore.getMembersInTeam()),
+ channelMembers: Object.assign([], ChannelStore.getMembersInChannel())
+ });
+ return;
+ }
+
+ clearTimeout(this.searchTimeoutId);
+
+ const searchTimeoutId = setTimeout(
+ () => {
+ searchUsers(
+ term,
+ TeamStore.getCurrentId(),
+ {},
+ (users) => {
+ if (searchTimeoutId !== this.searchTimeoutId) {
+ return;
+ }
+
+ this.setState({
+ loading: true,
+ search: true,
+ users,
+ term,
+ teamMembers: Object.assign([], TeamStore.getMembersInTeam()),
+ channelMembers: Object.assign([], ChannelStore.getMembersInChannel())
+ });
+ loadTeamMembersAndChannelMembersForProfilesList(users, TeamStore.getCurrentId(), ChannelStore.getCurrentId(), this.loadComplete);
+ }
+ );
+ },
+ Constants.SEARCH_TIMEOUT_MILLISECONDS
+ );
+
+ this.searchTimeoutId = searchTimeoutId;
+ }
+
+ render() {
+ const teamMembers = this.state.teamMembers;
+ const channelMembers = this.state.channelMembers;
+ const users = this.state.users;
+ const actionUserProps = {};
+
+ let usersToDisplay;
+ if (this.state.loading) {
+ usersToDisplay = null;
+ } else {
+ usersToDisplay = [];
+
+ for (let i = 0; i < users.length; i++) {
+ const user = users[i];
+
+ if (teamMembers[user.id] && channelMembers[user.id]) {
+ usersToDisplay.push(user);
+ actionUserProps[user.id] = {
+ channel: this.props.channel,
+ teamMember: teamMembers[user.id],
+ channelMember: channelMembers[user.id]
+ };
+ }
+ }
+ }
+
+ return (
+ <SearchableUserList
+ users={usersToDisplay}
+ usersPerPage={USERS_PER_PAGE}
+ total={this.state.total}
+ nextPage={this.nextPage}
+ search={this.search}
+ actions={[ChannelMembersDropdown]}
+ actionUserProps={actionUserProps}
+ focusOnMount={!UserAgent.isMobile()}
+ />
+ );
+ }
+}
+
+MemberListChannel.propTypes = {
+ channel: React.PropTypes.object.isRequired
+};
diff --git a/webapp/components/member_list_team.jsx b/webapp/components/member_list_team.jsx
index a9db0e734..df17d7df2 100644
--- a/webapp/components/member_list_team.jsx
+++ b/webapp/components/member_list_team.jsx
@@ -23,6 +23,7 @@ export default class MemberListTeam extends React.Component {
super(props);
this.onChange = this.onChange.bind(this);
+ this.onTeamChange = this.onTeamChange.bind(this);
this.onStatsChange = this.onStatsChange.bind(this);
this.search = this.search.bind(this);
this.loadComplete = this.loadComplete.bind(this);
@@ -44,7 +45,7 @@ export default class MemberListTeam extends React.Component {
componentDidMount() {
UserStore.addInTeamChangeListener(this.onChange);
UserStore.addStatusesChangeListener(this.onChange);
- TeamStore.addChangeListener(this.onChange.bind(null, true));
+ TeamStore.addChangeListener(this.onTeamChange);
TeamStore.addStatsChangeListener(this.onStatsChange);
loadProfilesAndTeamMembers(0, Constants.PROFILE_CHUNK_SIZE, TeamStore.getCurrentId(), this.loadComplete);
@@ -54,7 +55,7 @@ export default class MemberListTeam extends React.Component {
componentWillUnmount() {
UserStore.removeInTeamChangeListener(this.onChange);
UserStore.removeStatusesChangeListener(this.onChange);
- TeamStore.removeChangeListener(this.onChange);
+ TeamStore.removeChangeListener(this.onTeamChange);
TeamStore.removeStatsChangeListener(this.onStatsChange);
}
@@ -62,6 +63,10 @@ export default class MemberListTeam extends React.Component {
this.setState({loading: false});
}
+ onTeamChange() {
+ this.onChange(true);
+ }
+
onChange(force) {
if (this.state.search && !force) {
return;
@@ -90,13 +95,16 @@ export default class MemberListTeam extends React.Component {
clearTimeout(this.searchTimeoutId);
- this.searchTimeoutId = setTimeout(
+ const searchTimeoutId = setTimeout(
() => {
searchUsers(
term,
TeamStore.getCurrentId(),
{},
(users) => {
+ if (searchTimeoutId !== this.searchTimeoutId) {
+ return;
+ }
this.setState({loading: true, search: true, users, term, teamMembers: Object.assign([], TeamStore.getMembersInTeam())});
loadTeamMembersForProfilesList(users, TeamStore.getCurrentId(), this.loadComplete);
}
@@ -104,6 +112,8 @@ export default class MemberListTeam extends React.Component {
},
Constants.SEARCH_TIMEOUT_MILLISECONDS
);
+
+ this.searchTimeoutId = searchTimeoutId;
}
render() {
diff --git a/webapp/components/mfa/mfa_controller.jsx b/webapp/components/mfa/mfa_controller.jsx
index 21b9737f8..cd9497985 100644
--- a/webapp/components/mfa/mfa_controller.jsx
+++ b/webapp/components/mfa/mfa_controller.jsx
@@ -1,6 +1,8 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
+import {emitUserLoggedOutEvent} from 'actions/global_actions.jsx';
+
import React from 'react';
import {FormattedMessage} from 'react-intl';
import {browserHistory, Link} from 'react-router/es6';
@@ -16,13 +18,32 @@ export default class MFAController extends React.Component {
render() {
let backButton;
- if (window.mm_config.EnforceMultifactorAuthentication !== 'true') {
+ if (window.mm_config.EnforceMultifactorAuthentication === 'true') {
+ backButton = (
+ <div className='signup-header'>
+ <a
+ href='#'
+ onClick={(e) => {
+ e.preventDefault();
+ emitUserLoggedOutEvent('/login');
+ }}
+ >
+ <span className='fa fa-chevron-left'/>
+ <FormattedMessage
+ id='web.header.logout'
+ defaultMessage='Logout'
+ />
+ </a>
+ </div>
+ );
+ } else {
backButton = (
<div className='signup-header'>
<Link to='/'>
<span className='fa fa-chevron-left'/>
<FormattedMessage
id='web.header.back'
+ defaultMessage='Back'
/>
</Link>
</div>
diff --git a/webapp/components/more_channels.jsx b/webapp/components/more_channels.jsx
index e4cff451d..d0b5f5399 100644
--- a/webapp/components/more_channels.jsx
+++ b/webapp/components/more_channels.jsx
@@ -107,17 +107,22 @@ export default class MoreChannels extends React.Component {
clearTimeout(this.searchTimeoutId);
- this.searchTimeoutId = setTimeout(
+ const searchTimeoutId = setTimeout(
() => {
searchMoreChannels(
term,
(channels) => {
+ if (searchTimeoutId !== this.searchTimeoutId) {
+ return;
+ }
this.setState({search: true, channels});
}
);
},
SEARCH_TIMEOUT_MILLISECONDS
);
+
+ this.searchTimeoutId = searchTimeoutId;
}
render() {
@@ -196,4 +201,4 @@ export default class MoreChannels extends React.Component {
MoreChannels.propTypes = {
onModalDismissed: React.PropTypes.func,
handleNewChannel: React.PropTypes.func
-}; \ No newline at end of file
+};
diff --git a/webapp/components/navbar.jsx b/webapp/components/navbar.jsx
index 338d4edd1..b54b8701e 100644
--- a/webapp/components/navbar.jsx
+++ b/webapp/components/navbar.jsx
@@ -22,8 +22,6 @@ import PreferenceStore from 'stores/preference_store.jsx';
import ChannelSwitchModal from './channel_switch_modal.jsx';
-import Client from 'client/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import * as ChannelUtils from 'utils/channel_utils.jsx';
import * as ChannelActions from 'actions/channel_actions.jsx';
@@ -37,7 +35,7 @@ import {FormattedMessage} from 'react-intl';
import {Popover, OverlayTrigger} from 'react-bootstrap';
-import {Link, browserHistory} from 'react-router/es6';
+import {Link} from 'react-router/es6';
import React from 'react';
@@ -111,23 +109,7 @@ export default class Navbar extends React.Component {
}
handleLeave() {
- var channelId = this.state.channel.id;
-
- Client.leaveChannel(channelId,
- () => {
- ChannelActions.loadChannelsForCurrentUser();
-
- if (this.state.isFavorite) {
- ChannelActions.unmarkFavorite(channelId);
- }
-
- const townsquare = ChannelStore.getByName('town-square');
- browserHistory.push(TeamStore.getCurrentTeamRelativeUrl() + '/channels/' + townsquare.name);
- },
- (err) => {
- AsyncClient.dispatchError(err, 'handleLeave');
- }
- );
+ ChannelActions.leaveChannel(this.state.channel.id);
}
hideSidebars(e) {
diff --git a/webapp/components/new_channel_modal.jsx b/webapp/components/new_channel_modal.jsx
index fc9fd0295..be97532dc 100644
--- a/webapp/components/new_channel_modal.jsx
+++ b/webapp/components/new_channel_modal.jsx
@@ -272,7 +272,7 @@ export default class NewChannelModal extends React.Component {
className='form-control no-resize'
ref='channel_purpose'
rows='4'
- placeholder={Utils.localizeMessage('channel_modal.purpose', 'Purpose')}
+ placeholder={Utils.localizeMessage('channel_modal.purposeEx', 'E.g.: "A channel to file bugs and improvements"')}
maxLength='250'
value={this.props.channelData.purpose}
onChange={this.handleChange}
@@ -309,7 +309,7 @@ export default class NewChannelModal extends React.Component {
className='form-control no-resize'
ref='channel_header'
rows='4'
- placeholder={Utils.localizeMessage('channel_modal.header', 'Header')}
+ placeholder={Utils.localizeMessage('channel_modal.headerEx', 'E.g.: "[Link Title](http://example.com)"')}
maxLength='128'
value={this.props.channelData.header}
onChange={this.handleChange}
diff --git a/webapp/components/password_reset_form.jsx b/webapp/components/password_reset_form.jsx
index b37e07f2d..c6fe2525f 100644
--- a/webapp/components/password_reset_form.jsx
+++ b/webapp/components/password_reset_form.jsx
@@ -2,12 +2,12 @@
// See License.txt for license information.
import ReactDOM from 'react-dom';
-import Client from 'client/web_client.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
import {FormattedMessage} from 'react-intl';
-import {browserHistory} from 'react-router/es6';
+
+import {resetPassword} from 'actions/user_actions.jsx';
import React from 'react';
@@ -42,12 +42,11 @@ class PasswordResetForm extends React.Component {
error: null
});
- Client.resetPassword(
+ resetPassword(
this.props.location.query.code,
password,
() => {
this.setState({error: null});
- browserHistory.push('/login?extra=' + Constants.PASSWORD_CHANGE);
},
(err) => {
this.setState({error: err.message});
diff --git a/webapp/components/popover_list_members.jsx b/webapp/components/popover_list_members.jsx
index 9cea3922a..5ffcb687a 100644
--- a/webapp/components/popover_list_members.jsx
+++ b/webapp/components/popover_list_members.jsx
@@ -5,6 +5,11 @@ import ProfilePicture from 'components/profile_picture.jsx';
import TeamStore from 'stores/team_store.jsx';
import UserStore from 'stores/user_store.jsx';
+import ChannelStore from 'stores/channel_store.jsx';
+
+import TeamMembersModal from './team_members_modal.jsx';
+import ChannelMembersModal from './channel_members_modal.jsx';
+import ChannelInviteModal from './channel_invite_modal.jsx';
import {openDirectChannelToUser} from 'actions/channel_actions.jsx';
@@ -22,10 +27,17 @@ export default class PopoverListMembers extends React.Component {
constructor(props) {
super(props);
+ this.showMembersModal = this.showMembersModal.bind(this);
+
this.handleShowDirectChannel = this.handleShowDirectChannel.bind(this);
this.closePopover = this.closePopover.bind(this);
- this.state = {showPopover: false};
+ this.state = {
+ showPopover: false,
+ showTeamMembersModal: false,
+ showChannelMembersModal: false,
+ showChannelInviteModal: false
+ };
}
componentDidUpdate() {
@@ -53,12 +65,31 @@ export default class PopoverListMembers extends React.Component {
this.setState({showPopover: false});
}
+ showMembersModal(e) {
+ e.preventDefault();
+
+ if (ChannelStore.isDefault(this.props.channel)) {
+ this.setState({
+ showPopover: false,
+ showTeamMembersModal: true
+ });
+ } else {
+ this.setState({
+ showPopover: false,
+ showChannelMembersModal: true
+ });
+ }
+ }
+
render() {
const popoverHtml = [];
const members = this.props.members;
const teamMembers = UserStore.getProfilesUsernameMap();
+ let isAdmin = false;
const currentUserId = UserStore.getCurrentId();
+ isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
+
if (members && teamMembers) {
members.sort((a, b) => {
const aName = Utils.displayUsername(a.id);
@@ -96,7 +127,7 @@ export default class PopoverListMembers extends React.Component {
key={'popover-member-' + i}
>
<ProfilePicture
- src={`${Client.getUsersRoute()}/${m.id}/image?time=${m.update_at}`}
+ src={`${Client.getUsersRoute()}/${m.id}/image?time=${m.last_picture_update}`}
width='26'
height='26'
/>
@@ -117,17 +148,37 @@ export default class PopoverListMembers extends React.Component {
}
});
+ let membersName = (
+ <FormattedMessage
+ id='members_popover.manageMembers'
+ defaultMessage='Manage Members'
+ />
+ );
+ if (!isAdmin && ChannelStore.isDefault(this.props.channel)) {
+ membersName = (
+ <FormattedMessage
+ id='members_popover.viewMembers'
+ defaultMessage='View Members'
+ />
+ );
+ }
+
popoverHtml.push(
<div
className='more-modal__row'
key={'popover-member-more'}
>
- <div className='col-sm-5'/>
+ <div className='col-sm-3'/>
<div className='more-modal__details'>
<div
className='more-modal__name'
>
- {'...'}
+ <a
+ href='#'
+ onClick={this.showMembersModal}
+ >
+ {membersName}
+ </a>
</div>
</div>
</div>
@@ -146,6 +197,38 @@ export default class PopoverListMembers extends React.Component {
defaultMessage='Members'
/>
);
+
+ let channelMembersModal;
+ if (this.state.showChannelMembersModal) {
+ channelMembersModal = (
+ <ChannelMembersModal
+ onModalDismissed={() => this.setState({showChannelMembersModal: false})}
+ showInviteModal={() => this.setState({showChannelInviteModal: true})}
+ channel={this.props.channel}
+ />
+ );
+ }
+
+ let teamMembersModal;
+ if (this.state.showTeamMembersModal) {
+ teamMembersModal = (
+ <TeamMembersModal
+ onHide={() => this.setState({showTeamMembersModal: false})}
+ isAdmin={isAdmin}
+ />
+ );
+ }
+
+ let channelInviteModal;
+ if (this.state.showChannelInviteModal) {
+ channelInviteModal = (
+ <ChannelInviteModal
+ onHide={() => this.setState({showChannelInviteModal: false})}
+ channel={this.props.channel}
+ />
+ );
+ }
+
return (
<div>
<div
@@ -181,6 +264,9 @@ export default class PopoverListMembers extends React.Component {
<div className='more-modal__list'>{popoverHtml}</div>
</Popover>
</Overlay>
+ {channelMembersModal}
+ {teamMembersModal}
+ {channelInviteModal}
</div>
);
}
diff --git a/webapp/components/post_view/components/post.jsx b/webapp/components/post_view/components/post.jsx
index f052ac4ae..896002a6c 100644
--- a/webapp/components/post_view/components/post.jsx
+++ b/webapp/components/post_view/components/post.jsx
@@ -150,10 +150,10 @@ export default class Post extends React.Component {
}
let timestamp = 0;
- if (!this.props.user || this.props.user.update_at == null) {
- timestamp = this.props.currentUser.update_at;
+ if (!this.props.user || this.props.user.last_picture_update == null) {
+ timestamp = this.props.currentUser.last_picture_update;
} else {
- timestamp = this.props.user.update_at;
+ timestamp = this.props.user.last_picture_update;
}
let sameUserClass = '';
@@ -250,7 +250,11 @@ export default class Post extends React.Component {
}
return (
- <div>
+ <div
+ ref={(div) => {
+ this.domNode = div;
+ }}
+ >
<div
id={'post_' + post.id}
className={'post ' + sameUserClass + ' ' + compactClass + ' ' + rootUser + ' ' + postType + ' ' + currentUserCss + ' ' + shouldHighlightClass + ' ' + systemMessageClass + ' ' + hideControls + ' ' + dropdownOpenedClass}
@@ -285,6 +289,7 @@ export default class Post extends React.Component {
compactDisplay={this.props.compactDisplay}
previewCollapsed={this.props.previewCollapsed}
isCommentMention={this.props.isCommentMention}
+ childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
/>
</div>
</div>
@@ -313,5 +318,6 @@ Post.propTypes = {
useMilitaryTime: React.PropTypes.bool.isRequired,
isFlagged: React.PropTypes.bool,
status: React.PropTypes.string,
- isBusy: React.PropTypes.bool
+ isBusy: React.PropTypes.bool,
+ childComponentDidUpdateFunction: React.PropTypes.func
};
diff --git a/webapp/components/post_view/components/post_attachment_oembed.jsx b/webapp/components/post_view/components/post_attachment_oembed.jsx
deleted file mode 100644
index 359c7cc35..000000000
--- a/webapp/components/post_view/components/post_attachment_oembed.jsx
+++ /dev/null
@@ -1,108 +0,0 @@
-// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
-// See License.txt for license information.
-
-import $ from 'jquery';
-import React from 'react';
-
-export default class PostAttachmentOEmbed extends React.Component {
- constructor(props) {
- super(props);
- this.fetchData = this.fetchData.bind(this);
-
- this.isLoading = false;
- }
-
- componentWillMount() {
- this.setState({data: {}});
- }
-
- componentWillReceiveProps(nextProps) {
- if (nextProps.link !== this.props.link) {
- this.isLoading = false;
- this.fetchData(nextProps.link);
- }
- }
-
- componentDidMount() {
- this.fetchData(this.props.link);
- }
-
- fetchData(link) {
- if (!this.isLoading) {
- this.isLoading = true;
- let url = 'https://noembed.com/embed?nowrap=on';
- url += '&url=' + encodeURIComponent(link);
- url += '&maxheight=' + this.props.provider.height;
- return $.ajax({
- url,
- dataType: 'jsonp',
- success: (result) => {
- this.isLoading = false;
- if (result.error) {
- this.setState({data: {}});
- } else {
- this.setState({data: result});
- }
- },
- error: () => {
- this.setState({data: {}});
- }
- });
- }
- return null;
- }
-
- render() {
- let data = {};
- let content;
- if ($.isEmptyObject(this.state.data)) {
- content = <div style={{height: this.props.provider.height}}/>;
- } else {
- data = this.state.data;
- content = (
- <div
- style={{height: this.props.provider.height}}
- dangerouslySetInnerHTML={{__html: data.html}}
- />
- );
- }
-
- return (
- <div
- className='attachment attachment--oembed'
- ref='attachment'
- >
- <div className='attachment__content'>
- <div
- className={'clearfix attachment__container'}
- >
- <h1
- className='attachment__title'
- >
- <a
- className='attachment__title-link'
- href={data.url}
- target='_blank'
- rel='noopener noreferrer'
- >
- {data.title}
- </a>
- </h1>
- <div >
- <div
- className={'attachment__body attachment__body--no_thumb'}
- >
- {content}
- </div>
- </div>
- </div>
- </div>
- </div>
- );
- }
-}
-
-PostAttachmentOEmbed.propTypes = {
- link: React.PropTypes.string.isRequired,
- provider: React.PropTypes.object.isRequired
-};
diff --git a/webapp/components/post_view/components/post_attachment_opengraph.jsx b/webapp/components/post_view/components/post_attachment_opengraph.jsx
new file mode 100644
index 000000000..20beaed51
--- /dev/null
+++ b/webapp/components/post_view/components/post_attachment_opengraph.jsx
@@ -0,0 +1,212 @@
+// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+import React from 'react';
+
+import OpenGraphStore from 'stores/opengraph_store.jsx';
+import * as Utils from 'utils/utils.jsx';
+import * as CommonUtils from 'utils/commons.jsx';
+import {requestOpenGraphMetadata} from 'actions/global_actions.jsx';
+
+export default class PostAttachmentOpenGraph extends React.Component {
+ constructor(props) {
+ super(props);
+ this.imageDimentions = { // Image dimentions in pixels.
+ height: 150,
+ width: 150
+ };
+ this.maxDescriptionLength = 300;
+ this.descriptionEllipsis = '...';
+ this.fetchData = this.fetchData.bind(this);
+ this.onOpenGraphMetadataChange = this.onOpenGraphMetadataChange.bind(this);
+ this.toggleImageVisibility = this.toggleImageVisibility.bind(this);
+ this.onImageLoad = this.onImageLoad.bind(this);
+ }
+
+ componentWillMount() {
+ this.setState({
+ data: {},
+ imageLoaded: false,
+ imageVisible: this.props.previewCollapsed.startsWith('false')
+ });
+ this.fetchData(this.props.link);
+ }
+
+ componentWillReceiveProps(nextProps) {
+ this.setState({imageVisible: nextProps.previewCollapsed.startsWith('false')});
+ if (!Utils.areObjectsEqual(nextProps.link, this.props.link)) {
+ this.fetchData(nextProps.link);
+ }
+ }
+
+ shouldComponentUpdate(nextProps, nextState) {
+ if (nextState.imageVisible !== this.state.imageVisible) {
+ return true;
+ }
+ if (nextState.imageLoaded !== this.state.imageLoaded) {
+ return true;
+ }
+ if (!Utils.areObjectsEqual(nextState.data, this.state.data)) {
+ return true;
+ }
+ return false;
+ }
+
+ componentDidMount() {
+ OpenGraphStore.addUrlDataChangeListener(this.onOpenGraphMetadataChange);
+ }
+
+ componentDidUpdate() {
+ if (this.props.childComponentDidUpdateFunction) {
+ this.props.childComponentDidUpdateFunction();
+ }
+ }
+
+ componentWillUnmount() {
+ OpenGraphStore.removeUrlDataChangeListener(this.onOpenGraphMetadataChange);
+ }
+
+ onOpenGraphMetadataChange(url) {
+ if (url === this.props.link) {
+ this.fetchData(url);
+ }
+ }
+
+ fetchData(url) {
+ const data = OpenGraphStore.getOgInfo(url);
+ this.setState({data, imageLoaded: false});
+ if (Utils.isEmptyObject(data)) {
+ requestOpenGraphMetadata(url);
+ }
+ }
+
+ getBestImageUrl() {
+ const nearestPointData = CommonUtils.getNearestPoint(this.imageDimentions, this.state.data.images, 'width', 'height');
+
+ const bestImage = nearestPointData.nearestPoint;
+ const bestImageLte = nearestPointData.nearestPointLte; // Best image <= 150px height and width
+
+ let finalBestImage;
+
+ if (
+ !Utils.isEmptyObject(bestImageLte) &&
+ bestImageLte.height <= this.imageDimentions.height &&
+ bestImageLte.width <= this.imageDimentions.width
+ ) {
+ finalBestImage = bestImageLte;
+ } else {
+ finalBestImage = bestImage;
+ }
+
+ return finalBestImage.secure_url || finalBestImage.url;
+ }
+
+ toggleImageVisibility() {
+ this.setState({imageVisible: !this.state.imageVisible});
+ }
+
+ onImageLoad() {
+ this.setState({imageLoaded: true});
+ }
+
+ loadImage(src) {
+ const img = new Image();
+ img.onload = this.onImageLoad;
+ img.src = src;
+ }
+
+ imageToggleAnchoreTag(imageUrl) {
+ if (imageUrl) {
+ return (
+ <a
+ className={'post__embed-visibility'}
+ data-expanded={this.state.imageVisible}
+ aria-label='Toggle Embed Visibility'
+ onClick={this.toggleImageVisibility}
+ />
+ );
+ }
+ return null;
+ }
+
+ imageTag(imageUrl) {
+ if (imageUrl && this.state.imageVisible) {
+ return (
+ <img
+ className={this.state.imageLoaded ? 'attachment__image' : 'attachment__image loading'}
+ src={this.state.imageLoaded ? imageUrl : null}
+ />
+ );
+ }
+ return null;
+ }
+
+ render() {
+ if (Utils.isEmptyObject(this.state.data) || Utils.isEmptyObject(this.state.data.description)) {
+ return null;
+ }
+
+ const data = this.state.data;
+ const imageUrl = this.getBestImageUrl();
+ var description = data.description;
+
+ if (description.length > this.maxDescriptionLength) {
+ description = description.substring(0, this.maxDescriptionLength - this.descriptionEllipsis.length) + this.descriptionEllipsis;
+ }
+
+ if (imageUrl && this.state.imageVisible) {
+ this.loadImage(imageUrl);
+ }
+
+ return (
+ <div
+ className='attachment attachment--oembed'
+ ref='attachment'
+ >
+ <div className='attachment__content'>
+ <div
+ className={'clearfix attachment__container'}
+ >
+ <span className='sitename'>{data.site_name}</span>
+ <h1
+ className='attachment__title has-link'
+ >
+ <a
+ className='attachment__title-link'
+ href={data.url || this.props.link}
+ target='_blank'
+ rel='noopener noreferrer'
+ title={data.title || data.url || this.props.link}
+ >
+ {data.title || data.url || this.props.link}
+ </a>
+ </h1>
+ <div >
+ <div
+ className={'attachment__body attachment__body--no_thumb'}
+ >
+ <div>
+ <div>
+ {description} &nbsp;
+ {this.imageToggleAnchoreTag(imageUrl)}
+ </div>
+ {this.imageTag(imageUrl)}
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+ }
+}
+
+PostAttachmentOpenGraph.defaultProps = {
+ previewCollapsed: 'false'
+};
+
+PostAttachmentOpenGraph.propTypes = {
+ link: React.PropTypes.string.isRequired,
+ childComponentDidUpdateFunction: React.PropTypes.func,
+ previewCollapsed: React.PropTypes.string
+};
diff --git a/webapp/components/post_view/components/post_body.jsx b/webapp/components/post_view/components/post_body.jsx
index 60e682e8d..10c24aab2 100644
--- a/webapp/components/post_view/components/post_body.jsx
+++ b/webapp/components/post_view/components/post_body.jsx
@@ -188,6 +188,7 @@ export default class PostBody extends React.Component {
message={messageWrapper}
compactDisplay={this.props.compactDisplay}
previewCollapsed={this.props.previewCollapsed}
+ childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
/>
);
}
@@ -221,5 +222,6 @@ PostBody.propTypes = {
handleCommentClick: React.PropTypes.func.isRequired,
compactDisplay: React.PropTypes.bool,
previewCollapsed: React.PropTypes.string,
- isCommentMention: React.PropTypes.bool
+ isCommentMention: React.PropTypes.bool,
+ childComponentDidUpdateFunction: React.PropTypes.func
};
diff --git a/webapp/components/post_view/components/post_body_additional_content.jsx b/webapp/components/post_view/components/post_body_additional_content.jsx
index a65b608d7..cad618de0 100644
--- a/webapp/components/post_view/components/post_body_additional_content.jsx
+++ b/webapp/components/post_view/components/post_body_additional_content.jsx
@@ -2,12 +2,11 @@
// See License.txt for license information.
import PostAttachmentList from './post_attachment_list.jsx';
-import PostAttachmentOEmbed from './post_attachment_oembed.jsx';
+import PostAttachmentOpenGraph from './post_attachment_opengraph.jsx';
import PostImage from './post_image.jsx';
import YoutubeVideo from 'components/youtube_video.jsx';
import Constants from 'utils/constants.jsx';
-import OEmbedProviders from './providers.json';
import * as Utils from 'utils/utils.jsx';
import React from 'react';
@@ -17,22 +16,24 @@ export default class PostBodyAdditionalContent extends React.Component {
super(props);
this.getSlackAttachment = this.getSlackAttachment.bind(this);
- this.getOEmbedProvider = this.getOEmbedProvider.bind(this);
this.generateToggleableEmbed = this.generateToggleableEmbed.bind(this);
this.generateStaticEmbed = this.generateStaticEmbed.bind(this);
this.toggleEmbedVisibility = this.toggleEmbedVisibility.bind(this);
this.isLinkToggleable = this.isLinkToggleable.bind(this);
+ this.handleLinkLoadError = this.handleLinkLoadError.bind(this);
this.state = {
embedVisible: props.previewCollapsed.startsWith('false'),
- link: Utils.extractFirstLink(props.post.message)
+ link: Utils.extractFirstLink(props.post.message),
+ linkLoadError: false
};
}
componentWillReceiveProps(nextProps) {
this.setState({
embedVisible: nextProps.previewCollapsed.startsWith('false'),
- link: Utils.extractFirstLink(nextProps.post.message)
+ link: Utils.extractFirstLink(nextProps.post.message),
+ linkLoadError: false
});
}
@@ -46,6 +47,9 @@ export default class PostBodyAdditionalContent extends React.Component {
if (nextState.embedVisible !== this.state.embedVisible) {
return true;
}
+ if (nextState.linkLoadError !== this.state.linkLoadError) {
+ return true;
+ }
return false;
}
@@ -66,25 +70,11 @@ export default class PostBodyAdditionalContent extends React.Component {
);
}
- getOEmbedProvider(link) {
- for (let i = 0; i < OEmbedProviders.length; i++) {
- for (let j = 0; j < OEmbedProviders[i].patterns.length; j++) {
- if (link.match(OEmbedProviders[i].patterns[j])) {
- return OEmbedProviders[i];
- }
- }
- }
-
- return null;
- }
-
isLinkImage(link) {
- for (let i = 0; i < Constants.IMAGE_TYPES.length; i++) {
- const imageType = Constants.IMAGE_TYPES[i];
- const suffix = link.substring(link.length - (imageType.length + 1));
- if (suffix === '.' + imageType || suffix === '=' + imageType) {
- return true;
- }
+ const regex = /.+\/(.+\.(?:jpg|gif|bmp|png|jpeg))(?:\?.*)?$/i;
+ const match = link.match(regex);
+ if (match && match[1]) {
+ return true;
}
return false;
@@ -107,6 +97,12 @@ export default class PostBodyAdditionalContent extends React.Component {
return false;
}
+ handleLinkLoadError() {
+ this.setState({
+ linkLoadError: true
+ });
+ }
+
generateToggleableEmbed() {
const link = this.state.link;
if (!link) {
@@ -128,6 +124,7 @@ export default class PostBodyAdditionalContent extends React.Component {
<PostImage
channelId={this.props.post.channel_id}
link={link}
+ onLinkLoadError={this.handleLinkLoadError}
/>
);
}
@@ -141,39 +138,21 @@ export default class PostBodyAdditionalContent extends React.Component {
}
const link = Utils.extractFirstLink(this.props.post.message);
- if (!link) {
- return null;
- }
-
- if (Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_PREVIEW)) {
- const provider = this.getOEmbedProvider(link);
-
- if (provider) {
- return (
- <PostAttachmentOEmbed
- provider={provider}
- link={link}
- />
- );
- }
+ if (link && Utils.isFeatureEnabled(Constants.PRE_RELEASE_FEATURES.EMBED_PREVIEW)) {
+ return (
+ <PostAttachmentOpenGraph
+ link={link}
+ childComponentDidUpdateFunction={this.props.childComponentDidUpdateFunction}
+ previewCollapsed={this.props.previewCollapsed}
+ />
+ );
}
return null;
}
render() {
- const staticEmbed = this.generateStaticEmbed();
-
- if (staticEmbed) {
- return (
- <div>
- {this.props.message}
- {staticEmbed}
- </div>
- );
- }
-
- if (this.isLinkToggleable()) {
+ if (this.isLinkToggleable() && !this.state.linkLoadError) {
const messageWithToggle = [];
// if message has only one line and starts with a link place toggle in this only line
@@ -213,6 +192,17 @@ export default class PostBodyAdditionalContent extends React.Component {
);
}
+ const staticEmbed = this.generateStaticEmbed();
+
+ if (staticEmbed) {
+ return (
+ <div>
+ {this.props.message}
+ {staticEmbed}
+ </div>
+ );
+ }
+
return this.props.message;
}
}
@@ -224,5 +214,6 @@ PostBodyAdditionalContent.propTypes = {
post: React.PropTypes.object.isRequired,
message: React.PropTypes.element.isRequired,
compactDisplay: React.PropTypes.bool,
- previewCollapsed: React.PropTypes.string
+ previewCollapsed: React.PropTypes.string,
+ childComponentDidUpdateFunction: React.PropTypes.func
};
diff --git a/webapp/components/post_view/components/post_image.jsx b/webapp/components/post_view/components/post_image.jsx
index d1d1a6c7a..9a761bfca 100644
--- a/webapp/components/post_view/components/post_image.jsx
+++ b/webapp/components/post_view/components/post_image.jsx
@@ -53,6 +53,9 @@ export default class PostImageEmbed extends React.Component {
errored: true,
loaded: true
});
+ if (this.props.onLinkLoadError) {
+ this.props.onLinkLoadError();
+ }
}
render() {
@@ -79,5 +82,6 @@ export default class PostImageEmbed extends React.Component {
}
PostImageEmbed.propTypes = {
- link: React.PropTypes.string.isRequired
+ link: React.PropTypes.string.isRequired,
+ onLinkLoadError: React.PropTypes.func
};
diff --git a/webapp/components/post_view/components/post_info.jsx b/webapp/components/post_view/components/post_info.jsx
index aa204add1..3f38bdffe 100644
--- a/webapp/components/post_view/components/post_info.jsx
+++ b/webapp/components/post_view/components/post_info.jsx
@@ -8,12 +8,10 @@ import PostTime from './post_time.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import * as PostActions from 'actions/post_actions.jsx';
-import TeamStore from 'stores/team_store.jsx';
-import UserStore from 'stores/user_store.jsx';
-
import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
import Constants from 'utils/constants.jsx';
+import DelayedAction from 'utils/delayed_action.jsx';
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
import React from 'react';
@@ -23,31 +21,42 @@ export default class PostInfo extends React.Component {
constructor(props) {
super(props);
- this.handleDropdownClick = this.handleDropdownClick.bind(this);
+ this.handleDropdownOpened = this.handleDropdownOpened.bind(this);
this.handlePermalink = this.handlePermalink.bind(this);
this.removePost = this.removePost.bind(this);
this.flagPost = this.flagPost.bind(this);
this.unflagPost = this.unflagPost.bind(this);
+
+ this.canEdit = false;
+ this.canDelete = false;
+ this.editDisableAction = new DelayedAction(this.handleEditDisable);
}
- handleDropdownClick(e) {
- var position = $('#post-list').height() - $(e.target).offset().top;
- var dropdown = $(e.target).closest('.col__reply').find('.dropdown-menu');
+ handleDropdownOpened() {
+ this.props.handleDropdownOpened(true);
+
+ const position = $('#post-list').height() - $(this.refs.dropdownToggle).offset().top;
+ const dropdown = $(this.refs.dropdown);
+
if (position < dropdown.height()) {
dropdown.addClass('bottom');
}
}
+ handleEditDisable() {
+ this.canEdit = false;
+ }
+
componentDidMount() {
- $('#post_dropdown' + this.props.post.id).on('shown.bs.dropdown', () => this.props.handleDropdownOpened(true));
+ $('#post_dropdown' + this.props.post.id).on('shown.bs.dropdown', this.handleDropdownOpened);
$('#post_dropdown' + this.props.post.id).on('hidden.bs.dropdown', () => this.props.handleDropdownOpened(false));
}
createDropdown() {
var post = this.props.post;
- var isOwner = this.props.currentUser.id === post.user_id;
- var isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
- const isSystemMessage = post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX);
+
+ this.canDelete = PostUtils.canDeletePost(post);
+ this.canEdit = PostUtils.canEditPost(post, this.editDisableAction);
if (post.state === Constants.POST_FAILED || post.state === Constants.POST_LOADING) {
return '';
@@ -139,7 +148,7 @@ export default class PostInfo extends React.Component {
</li>
);
- if (isOwner || isAdmin) {
+ if (this.canDelete) {
dropdownContents.push(
<li
key='deletePost'
@@ -162,12 +171,12 @@ export default class PostInfo extends React.Component {
);
}
- if (isOwner && !isSystemMessage) {
+ if (this.canEdit) {
dropdownContents.push(
<li
key='editPost'
role='presentation'
- className='dropdown-submenu'
+ className={this.canEdit ? 'dropdown-submenu' : 'dropdown-submenu hide'}
>
<a
href='#'
@@ -199,15 +208,16 @@ export default class PostInfo extends React.Component {
id={'post_dropdown' + this.props.post.id}
>
<a
+ ref='dropdownToggle'
href='#'
className='dropdown-toggle post__dropdown theme'
type='button'
data-toggle='dropdown'
aria-expanded='false'
- onClick={this.handleDropdownClick}
/>
<div className='dropdown-menu__content'>
<ul
+ ref='dropdown'
className='dropdown-menu'
role='menu'
>
diff --git a/webapp/components/post_view/components/post_list.jsx b/webapp/components/post_view/components/post_list.jsx
index 29358122b..7550db348 100644
--- a/webapp/components/post_view/components/post_list.jsx
+++ b/webapp/components/post_view/components/post_list.jsx
@@ -45,6 +45,7 @@ export default class PostList extends React.Component {
this.scrollToBottom = this.scrollToBottom.bind(this);
this.scrollToBottomAnimated = this.scrollToBottomAnimated.bind(this);
this.handleKeyDown = this.handleKeyDown.bind(this);
+ this.childComponentDidUpdate = this.childComponentDidUpdate.bind(this);
this.jumpToPostNode = null;
this.wasAtBottom = true;
@@ -159,7 +160,7 @@ export default class PostList extends React.Component {
const id = this.props.postList.order[i];
const element = this.refs[id];
- if (!element || element.offsetTop + element.clientHeight <= this.refs.postlist.scrollTop) {
+ if (!element || !element.domNode || element.domNode.offsetTop + element.domNode.clientHeight <= this.refs.postlist.scrollTop) {
// this post is off the top of the screen so the last one is at the top of the screen
let topPostId;
@@ -347,6 +348,7 @@ export default class PostList extends React.Component {
isFlagged={isFlagged}
status={status}
isBusy={this.props.isBusy}
+ childComponentDidUpdateFunction={this.childComponentDidUpdate}
/>
);
@@ -421,6 +423,11 @@ export default class PostList extends React.Component {
this.scrollToBottom();
}
});
+
+ // This avoids the scroll jumping from top to bottom after the page has rendered (PLT-5025).
+ if (!this.refs.newMessageSeparator) {
+ this.scrollToBottom();
+ }
} else if (this.props.scrollType === ScrollTypes.POST && this.props.scrollPostId) {
window.requestAnimationFrame(() => {
const postNode = ReactDOM.findDOMNode(this.refs[this.props.scrollPostId]);
@@ -487,6 +494,12 @@ export default class PostList extends React.Component {
);
}
+ checkAndUpdateScrolling() {
+ if (this.props.postList != null && this.refs.postlist) {
+ this.updateScrolling();
+ }
+ }
+
componentDidMount() {
if (this.props.postList != null) {
this.updateScrolling();
@@ -504,9 +517,11 @@ export default class PostList extends React.Component {
}
componentDidUpdate() {
- if (this.props.postList != null && this.refs.postlist) {
- this.updateScrolling();
- }
+ this.checkAndUpdateScrolling();
+ }
+
+ childComponentDidUpdate() {
+ this.checkAndUpdateScrolling();
}
render() {
diff --git a/webapp/components/post_view/components/post_message_container.jsx b/webapp/components/post_view/components/post_message_container.jsx
index 2d17e74c4..4e27cd29a 100644
--- a/webapp/components/post_view/components/post_message_container.jsx
+++ b/webapp/components/post_view/components/post_message_container.jsx
@@ -89,7 +89,7 @@ export default class PostMessageContainer extends React.Component {
return (
<PostMessageView
options={this.props.options}
- message={this.props.post.message}
+ post={this.props.post}
emojis={this.state.emojis}
enableFormatting={this.state.enableFormatting}
mentionKeys={this.state.mentionKeys}
diff --git a/webapp/components/post_view/components/post_message_view.jsx b/webapp/components/post_view/components/post_message_view.jsx
index 24f96a8d9..eff791aec 100644
--- a/webapp/components/post_view/components/post_message_view.jsx
+++ b/webapp/components/post_view/components/post_message_view.jsx
@@ -2,14 +2,16 @@
// See License.txt for license information.
import React from 'react';
+import {FormattedMessage} from 'react-intl';
import * as TextFormatting from 'utils/text_formatting.jsx';
import * as Utils from 'utils/utils.jsx';
+import * as PostUtils from 'utils/post_utils.jsx';
export default class PostMessageView extends React.Component {
static propTypes = {
options: React.PropTypes.object.isRequired,
- message: React.PropTypes.string.isRequired,
+ post: React.PropTypes.object.isRequired,
emojis: React.PropTypes.object.isRequired,
enableFormatting: React.PropTypes.bool.isRequired,
mentionKeys: React.PropTypes.arrayOf(React.PropTypes.string).isRequired,
@@ -23,7 +25,7 @@ export default class PostMessageView extends React.Component {
return true;
}
- if (nextProps.message !== this.props.message) {
+ if (nextProps.post.message !== this.props.post.message) {
return true;
}
@@ -47,9 +49,28 @@ export default class PostMessageView extends React.Component {
return false;
}
+ editedIndicator() {
+ return (
+ PostUtils.isEdited(this.props.post) ?
+ <span className='edited'>
+ <FormattedMessage
+ id='post_message_view.edited'
+ defaultMessage='(edited)'
+ />
+ </span> :
+ ''
+ );
+ }
+
render() {
if (!this.props.enableFormatting) {
- return <span>{this.props.message}</span>;
+ return (
+ <span>
+ {this.props.post.message}
+ &nbsp;
+ {this.editedIndicator()}
+ </span>
+ );
}
const options = Object.assign({}, this.props.options, {
@@ -62,10 +83,13 @@ export default class PostMessageView extends React.Component {
});
return (
- <span
- onClick={Utils.handleFormattedTextClick}
- dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.message, options)}}
- />
+ <div>
+ <span
+ onClick={Utils.handleFormattedTextClick}
+ dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.props.post.message, options)}}
+ />
+ {this.editedIndicator()}
+ </div>
);
}
}
diff --git a/webapp/components/post_view/components/post_time.jsx b/webapp/components/post_view/components/post_time.jsx
index c8e57f6a9..caad12d4a 100644
--- a/webapp/components/post_view/components/post_time.jsx
+++ b/webapp/components/post_view/components/post_time.jsx
@@ -27,7 +27,10 @@ export default class PostTime extends React.Component {
render() {
return (
- <time className='post__time'>
+ <time
+ className='post__time'
+ dateTime={getDateForUnixTicks(this.props.eventTime).toISOString()}
+ >
{getDateForUnixTicks(this.props.eventTime).toLocaleString('en', {hour: '2-digit', minute: '2-digit', hour12: !this.props.useMilitaryTime})}
</time>
);
diff --git a/webapp/components/post_view/components/providers.json b/webapp/components/post_view/components/providers.json
deleted file mode 100644
index b5899c225..000000000
--- a/webapp/components/post_view/components/providers.json
+++ /dev/null
@@ -1,376 +0,0 @@
-[
- {
- "patterns": [
- "http://(?:www\\.)?xkcd\\.com/\\d+/?"
- ],
- "name": "XKCD",
- "height": 110
- },
- {
- "patterns": [
- "https?://soundcloud.com/.*/.*"
- ],
- "name": "SoundCloud",
- "height": 140
- },
- {
- "patterns": [
- "https?://(?:www\\.)?flickr\\.com/.*",
- "https?://flic\\.kr/p/[a-zA-Z0-9]+"
- ],
- "name": "Flickr",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.ted\\.com/talks/.+\\.html"
- ],
- "name": "TED",
- "height": 110
- },
- {
- "patterns": [
- "http://(?:www\\.)?theverge\\.com/\\d{4}/\\d{1,2}/\\d{1,2}/\\d+/[^/]+/?$"
- ],
- "name": "The Verge",
- "height": 110
- },
- {
- "patterns": [
- "http://.*\\.viddler\\.com/.*"
- ],
- "name": "Viddler",
- "height": 110
- },
- {
- "patterns": [
- "https?://(?:www\\.)?avclub\\.com/article/[^/]+/?$"
- ],
- "name": "The AV Club",
- "height": 110
- },
- {
- "patterns": [
- "https?://(?:www\\.)?wired\\.com/([^/]+/)?\\d+/\\d+/[^/]+/?$"
- ],
- "name": "Wired",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.theonion\\.com/articles/[^/]+/?"
- ],
- "name": "The Onion",
- "height": 110
- },
- {
- "patterns": [
- "http://yfrog\\.com/[0-9a-zA-Z]+/?$"
- ],
- "name": "YFrog",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.duffelblog\\.com/\\d{4}/\\d{1,2}/[^/]+/?$"
- ],
- "name": "The Duffel Blog",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.clickhole\\.com/article/[^/]+/?"
- ],
- "name": "Clickhole",
- "height": 110
- },
- {
- "patterns": [
- "https?://(?:www.)?skitch.com/([^/]+)/[^/]+/.+",
- "http://skit.ch/[^/]+"
- ],
- "name": "Skitch",
- "height": 110
- },
- {
- "patterns": [
- "https?://(alpha|posts|photos)\\.app\\.net/.*"
- ],
- "name": "ADN",
- "height": 110
- },
- {
- "patterns": [
- "https?://gist\\.github\\.com/(?:[-0-9a-zA-Z]+/)?([0-9a-fA-f]+)"
- ],
- "name": "Gist",
- "height": 110
- },
- {
- "patterns": [
- "https?://www\\.(dropbox\\.com/s/.+\\.(?:jpg|png|gif))",
- "https?://db\\.tt/[a-zA-Z0-9]+"
- ],
- "name": "Dropbox",
- "height": 110
- },
- {
- "patterns": [
- "https?://[^\\.]+\\.wikipedia\\.org/wiki/(?!Talk:)[^#]+(?:#(.+))?"
- ],
- "name": "Wikipedia",
- "height": 110
- },
- {
- "patterns": [
- "http://www.traileraddict.com/trailer/[^/]+/trailer"
- ],
- "name": "TrailerAddict",
- "height": 110
- },
- {
- "patterns": [
- "http://lockerz\\.com/[sd]/\\d+"
- ],
- "name": "Lockerz",
- "height": 110
- },
- {
- "patterns": [
- "http://gifuk\\.com/s/[0-9a-f]{16}"
- ],
- "name": "GIFUK",
- "height": 110
- },
- {
- "patterns": [
- "http://trailers\\.apple\\.com/trailers/[^/]+/[^/]+"
- ],
- "name": "iTunes Movie Trailers",
- "height": 110
- },
- {
- "patterns": [
- "http://gfycat\\.com/([a-zA-Z]+)"
- ],
- "name": "Gfycat",
- "height": 110
- },
- {
- "patterns": [
- "http://bash\\.org/\\?(\\d+)"
- ],
- "name": "Bash.org",
- "height": 110
- },
- {
- "patterns": [
- "http://arstechnica\\.com/[^/]+/\\d+/\\d+/[^/]+/?$"
- ],
- "name": "Ars Technica",
- "height": 110
- },
- {
- "patterns": [
- "http://imgur\\.com/gallery/[0-9a-zA-Z]+"
- ],
- "name": "Imgur",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.asciiartfarts\\.com/[0-9]+\\.html"
- ],
- "name": "ASCII Art Farts",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.monoprice\\.com/products/product\\.asp\\?.*p_id=\\d+"
- ],
- "name": "Monoprice",
- "height": 110
- },
- {
- "patterns": [
- "http://boingboing\\.net/\\d{4}/\\d{2}/\\d{2}/[^/]+\\.html"
- ],
- "name": "Boing Boing",
- "height": 110
- },
- {
- "patterns": [
- "https?://github\\.com/([^/]+)/([^/]+)/commit/(.+)",
- "http://git\\.io/[_0-9a-zA-Z]+"
- ],
- "name": "Github Commit",
- "height": 110
- },
- {
- "patterns": [
- "https?://open\\.spotify\\.com/(track|album)/([0-9a-zA-Z]{22})"
- ],
- "name": "Spotify",
- "height": 110
- },
- {
- "patterns": [
- "https?://path\\.com/p/([0-9a-zA-Z]+)$"
- ],
- "name": "Path",
- "height": 110
- },
- {
- "patterns": [
- "http://www.funnyordie.com/videos/[^/]+/.+"
- ],
- "name": "Funny or Die",
- "height": 110
- },
- {
- "patterns": [
- "http://(?:www\\.)?twitpic\\.com/([^/]+)"
- ],
- "name": "Twitpic",
- "height": 110
- },
- {
- "patterns": [
- "https?://www\\.giantbomb\\.com/videos/[^/]+/\\d+-\\d+/?"
- ],
- "name": "GiantBomb",
- "height": 110
- },
- {
- "patterns": [
- "http://(?:www\\.)?beeradvocate\\.com/beer/profile/\\d+/\\d+"
- ],
- "name": "Beer Advocate",
- "height": 110
- },
- {
- "patterns": [
- "http://(?:www\\.)?imdb.com/title/(tt\\d+)"
- ],
- "name": "IMDB",
- "height": 110
- },
- {
- "patterns": [
- "http://cl\\.ly/(?:image/)?[0-9a-zA-Z]+/?$"
- ],
- "name": "CloudApp",
- "height": 110
- },
- {
- "patterns": [
- "http://clyp\\.it/.*"
- ],
- "name": "Clyp",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.hulu\\.com/watch/.*"
- ],
- "name": "Hulu",
- "height": 110
- },
- {
- "patterns": [
- "https?://(?:www|mobile\\.)?twitter\\.com/(?:#!/)?[^/]+/status(?:es)?/(\\d+)/?$",
- "https?://t\\.co/[a-zA-Z0-9]+"
- ],
- "name": "Twitter",
- "height": 110
- },
- {
- "patterns": [
- "https?://(?:www\\.)?vimeo\\.com/.+"
- ],
- "name": "Vimeo",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.amazon\\.com/(?:.+/)?[gd]p/(?:product/)?(?:tags-on-product/)?([a-zA-Z0-9]+)",
- "http://amzn\\.com/([^/]+)"
- ],
- "name": "Amazon",
- "height": 110
- },
- {
- "patterns": [
- "http://qik\\.com/video/.*"
- ],
- "name": "Qik",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.rdio\\.com/artist/[^/]+/album/[^/]+/?",
- "http://www\\.rdio\\.com/artist/[^/]+/album/[^/]+/track/[^/]+/?",
- "http://www\\.rdio\\.com/people/[^/]+/playlists/\\d+/[^/]+"
- ],
- "name": "Rdio",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.slideshare\\.net/.*/.*"
- ],
- "name": "SlideShare",
- "height": 110
- },
- {
- "patterns": [
- "http://imgur\\.com/([0-9a-zA-Z]+)$"
- ],
- "name": "Imgur",
- "height": 110
- },
- {
- "patterns": [
- "https?://instagr(?:\\.am|am\\.com)/p/.+"
- ],
- "name": "Instagram",
- "height": 110
- },
- {
- "patterns": [
- "http://www\\.twitlonger\\.com/show/[a-zA-Z0-9]+",
- "http://tl\\.gd/[^/]+"
- ],
- "name": "Twitlonger",
- "height": 110
- },
- {
- "patterns": [
- "https?://vine.co/v/[a-zA-Z0-9]+"
- ],
- "name": "Vine",
- "height": 490
- },
- {
- "patterns": [
- "http://www\\.urbandictionary\\.com/define\\.php\\?term=.+"
- ],
- "name": "Urban Dictionary",
- "height": 110
- },
- {
- "patterns": [
- "http://picplz\\.com/user/[^/]+/pic/[^/]+"
- ],
- "name": "Picplz",
- "height": 110
- },
- {
- "patterns": [
- "https?://(?:www\\.)?twitter\\.com/(?:#!/)?[^/]+/status(?:es)?/(\\d+)/photo/\\d+(?:/large|/)?$",
- "https?://pic\\.twitter\\.com/.+"
- ],
- "name": "Twitter",
- "height": 110
- }
-]
diff --git a/webapp/components/post_view/post_view_controller.jsx b/webapp/components/post_view/post_view_controller.jsx
index a18a73b86..a18d0ac38 100644
--- a/webapp/components/post_view/post_view_controller.jsx
+++ b/webapp/components/post_view/post_view_controller.jsx
@@ -229,8 +229,16 @@ export default class PostViewController extends React.Component {
onPostListScroll(atBottom) {
if (atBottom) {
+ let lastViewedBottom;
const lastPost = PostStore.getLatestPost(this.state.channel.id);
- this.setState({scrollType: ScrollTypes.BOTTOM, lastViewedBottom: lastPost.create_at || new Date().getTime()});
+
+ if (lastPost && lastPost.create_at) {
+ lastViewedBottom = lastPost.create_at;
+ } else {
+ lastViewedBottom = new Date().getTime();
+ }
+
+ this.setState({scrollType: ScrollTypes.BOTTOM, lastViewedBottom});
} else {
this.setState({scrollType: ScrollTypes.FREE});
}
diff --git a/webapp/components/profile_popover.jsx b/webapp/components/profile_popover.jsx
index 7cb2f7261..22cf60004 100644
--- a/webapp/components/profile_popover.jsx
+++ b/webapp/components/profile_popover.jsx
@@ -83,6 +83,9 @@ export default class ProfilePopover extends React.Component {
openDirectChannelToUser(
user,
(channel) => {
+ if (Utils.isMobile()) {
+ GlobalActions.emitCloseRightHandSide();
+ }
this.setState({loadingDMChannel: -1});
if (this.props.hide) {
this.props.hide();
@@ -185,34 +188,34 @@ export default class ProfilePopover extends React.Component {
const fullname = Utils.getFullName(this.props.user);
if (fullname) {
dataContent.push(
- <div
- data-toggle='tooltip'
- title={fullname}
- key='user-popover-fullname'
+ <OverlayTrigger
+ delayShow={Constants.WEBRTC_TIME_DELAY}
+ placement='top'
+ overlay={<Tooltip id='fullNameTooltip'>{fullname}</Tooltip>}
>
- <p
- className='text-nowrap'
+ <div
+ className='overflow--ellipsis text-nowrap padding-bottom'
>
{fullname}
- </p>
- </div>
+ </div>
+ </OverlayTrigger>
);
}
if (this.props.user.position) {
const position = this.props.user.position.substring(0, Constants.MAX_POSITION_LENGTH);
dataContent.push(
- <div
- data-toggle='tooltip'
- title={position}
- key='user-popover-position'
+ <OverlayTrigger
+ delayShow={Constants.WEBRTC_TIME_DELAY}
+ placement='top'
+ overlay={<Tooltip id='positionTooltip'>{position}</Tooltip>}
>
- <p
- className='text-nowrap'
+ <div
+ className='overflow--ellipsis text-nowrap padding-bottom'
>
{position}
- </p>
- </div>
+ </div>
+ </OverlayTrigger>
);
}
diff --git a/webapp/components/rhs_comment.jsx b/webapp/components/rhs_comment.jsx
index 8b7642fd8..26659c7a1 100644
--- a/webapp/components/rhs_comment.jsx
+++ b/webapp/components/rhs_comment.jsx
@@ -9,9 +9,6 @@ import ProfilePicture from 'components/profile_picture.jsx';
import ReactionListContainer from 'components/post_view/components/reaction_list_container.jsx';
import RhsDropdown from 'components/rhs_dropdown.jsx';
-import TeamStore from 'stores/team_store.jsx';
-import UserStore from 'stores/user_store.jsx';
-
import * as GlobalActions from 'actions/global_actions.jsx';
import {flagPost, unflagPost} from 'actions/post_actions.jsx';
@@ -19,6 +16,7 @@ import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
import Constants from 'utils/constants.jsx';
+import DelayedAction from 'utils/delayed_action.jsx';
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
@@ -36,6 +34,10 @@ export default class RhsComment extends React.Component {
this.flagPost = this.flagPost.bind(this);
this.unflagPost = this.unflagPost.bind(this);
+ this.canEdit = false;
+ this.canDelete = false;
+ this.editDisableAction = new DelayedAction(this.handleEditDisable);
+
this.state = {};
}
@@ -44,6 +46,10 @@ export default class RhsComment extends React.Component {
GlobalActions.showGetPostLinkModal(this.props.post);
}
+ handleEditDisable() {
+ this.canEdit = false;
+ }
+
removePost() {
GlobalActions.emitRemovePost(this.props.post);
}
@@ -110,8 +116,8 @@ export default class RhsComment extends React.Component {
return '';
}
- const isOwner = this.props.currentUser.id === post.user_id;
- var isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
+ this.canDelete = PostUtils.canDeletePost(post);
+ this.canEdit = PostUtils.canEditPost(post, this.editDisableAction);
var dropdownContents = [];
@@ -170,7 +176,7 @@ export default class RhsComment extends React.Component {
</li>
);
- if (isOwner || isAdmin) {
+ if (this.canDelete) {
dropdownContents.push(
<li
role='presentation'
@@ -193,11 +199,12 @@ export default class RhsComment extends React.Component {
);
}
- if (isOwner) {
+ if (this.canEdit) {
dropdownContents.push(
<li
role='presentation'
key='edit-button'
+ className={this.canEdit ? '' : 'hide'}
>
<a
href='#'
@@ -239,7 +246,7 @@ export default class RhsComment extends React.Component {
currentUserCss = 'current--user';
}
- var timestamp = this.props.currentUser.update_at;
+ var timestamp = this.props.currentUser.last_picture_update;
let status = this.props.status;
if (post.props && post.props.from_webhook === 'true') {
@@ -471,7 +478,10 @@ export default class RhsComment extends React.Component {
</li>
{botIndicator}
<li className='col'>
- <time className='post__time'>
+ <time
+ className='post__time'
+ dateTime={Utils.getDateForUnixTicks(post.create_at).toISOString()}
+ >
{Utils.getDateForUnixTicks(post.create_at).toLocaleString('en', timeOptions)}
</time>
{flagTrigger}
diff --git a/webapp/components/rhs_root_post.jsx b/webapp/components/rhs_root_post.jsx
index 95f5fc1ac..7d00e2322 100644
--- a/webapp/components/rhs_root_post.jsx
+++ b/webapp/components/rhs_root_post.jsx
@@ -11,7 +11,6 @@ import RhsDropdown from 'components/rhs_dropdown.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx';
-import TeamStore from 'stores/team_store.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import {flagPost, unflagPost} from 'actions/post_actions.jsx';
@@ -20,6 +19,7 @@ import * as Utils from 'utils/utils.jsx';
import * as PostUtils from 'utils/post_utils.jsx';
import Constants from 'utils/constants.jsx';
+import DelayedAction from 'utils/delayed_action.jsx';
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
import {FormattedMessage} from 'react-intl';
@@ -34,6 +34,10 @@ export default class RhsRootPost extends React.Component {
this.flagPost = this.flagPost.bind(this);
this.unflagPost = this.unflagPost.bind(this);
+ this.canEdit = false;
+ this.canDelete = false;
+ this.editDisableAction = new DelayedAction(this.handleEditDisable);
+
this.state = {};
}
@@ -42,6 +46,10 @@ export default class RhsRootPost extends React.Component {
GlobalActions.showGetPostLinkModal(this.props.post);
}
+ handleEditDisable() {
+ this.canEdit = false;
+ }
+
shouldComponentUpdate(nextProps) {
if (nextProps.status !== this.props.status) {
return true;
@@ -96,13 +104,13 @@ export default class RhsRootPost extends React.Component {
const post = this.props.post;
const user = this.props.user;
const mattermostLogo = Constants.MATTERMOST_ICON_SVG;
- var isOwner = this.props.currentUser.id === post.user_id;
- var isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
- const isSystemMessage = post.type && post.type.startsWith(Constants.SYSTEM_MESSAGE_PREFIX);
- var timestamp = user ? user.update_at : 0;
+ var timestamp = user ? user.last_picture_update : 0;
var channel = ChannelStore.get(post.channel_id);
const flagIcon = Constants.FLAG_ICON_SVG;
+ this.canDelete = PostUtils.canDeletePost(post);
+ this.canEdit = PostUtils.canEditPost(post, this.editDisableAction);
+
var type = 'Post';
if (post.root_id.length > 0) {
type = 'Comment';
@@ -189,7 +197,7 @@ export default class RhsRootPost extends React.Component {
</li>
);
- if (isOwner || isAdmin) {
+ if (this.canDelete) {
dropdownContents.push(
<li
key='rhs-root-delete'
@@ -209,11 +217,12 @@ export default class RhsRootPost extends React.Component {
);
}
- if (isOwner && !isSystemMessage) {
+ if (this.canEdit) {
dropdownContents.push(
<li
key='rhs-root-edit'
role='presentation'
+ className={this.canEdit ? '' : 'hide'}
>
<a
href='#'
@@ -408,7 +417,10 @@ export default class RhsRootPost extends React.Component {
<li className='col__name'>{userProfile}</li>
{botIndicator}
<li className='col'>
- <time className='post__time'>
+ <time
+ className='post__time'
+ dateTime={Utils.getDateForUnixTicks(post.create_at).toISOString()}
+ >
{Utils.getDateForUnixTicks(post.create_at).toLocaleString('en', timeOptions)}
</time>
<OverlayTrigger
diff --git a/webapp/components/root.jsx b/webapp/components/root.jsx
index be50c7d48..465df5d79 100644
--- a/webapp/components/root.jsx
+++ b/webapp/components/root.jsx
@@ -8,11 +8,12 @@ import Client from 'client/web_client.jsx';
import {IntlProvider} from 'react-intl';
import React from 'react';
-
import FastClick from 'fastclick';
+import $ from 'jquery';
import {browserHistory} from 'react-router/es6';
import UserStore from 'stores/user_store.jsx';
+import BrowserStore from 'stores/browser_store.jsx';
export default class Root extends React.Component {
constructor(props) {
@@ -35,6 +36,30 @@ export default class Root extends React.Component {
}
/*eslint-enable */
+ // Force logout of all tabs if one tab is logged out
+ $(window).bind('storage', (e) => {
+ // when one tab on a browser logs out, it sets __logout__ in localStorage to trigger other tabs to log out
+ if (e.originalEvent.key === '__logout__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
+ // make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
+ if (BrowserStore.isSignallingLogout(e.originalEvent.newValue)) {
+ return;
+ }
+
+ console.log('detected logout from a different tab'); //eslint-disable-line no-console
+ GlobalActions.emitUserLoggedOutEvent('/', false);
+ }
+
+ if (e.originalEvent.key === '__login__' && e.originalEvent.storageArea === localStorage && e.originalEvent.newValue) {
+ // make sure it isn't this tab that is sending the logout signal (only necessary for IE11)
+ if (BrowserStore.isSignallingLogin(e.originalEvent.newValue)) {
+ return;
+ }
+
+ console.log('detected login from a different tab'); //eslint-disable-line no-console
+ location.reload();
+ }
+ });
+
// Fastclick
FastClick.attach(document.body);
}
diff --git a/webapp/components/search_bar.jsx b/webapp/components/search_bar.jsx
index a7e9bfcac..c5fcd4697 100644
--- a/webapp/components/search_bar.jsx
+++ b/webapp/components/search_bar.jsx
@@ -2,9 +2,6 @@
// See License.txt for license information.
import $ from 'jquery';
-import ReactDOM from 'react-dom';
-import Client from 'client/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import SearchStore from 'stores/search_store.jsx';
import UserStore from 'stores/user_store.jsx';
@@ -15,7 +12,7 @@ import SearchSuggestionList from './suggestion/search_suggestion_list.jsx';
import SearchUserProvider from './suggestion/search_user_provider.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
-import {loadProfilesForPosts, getFlaggedPosts} from 'actions/post_actions.jsx';
+import {getFlaggedPosts, performSearch} from 'actions/post_actions.jsx';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
@@ -119,26 +116,18 @@ export default class SearchBar extends React.Component {
if (terms.length) {
this.setState({isSearching: true});
- Client.search(
+ performSearch(
terms,
isMentionSearch,
- (data) => {
+ () => {
this.setState({isSearching: false});
- if (Utils.isMobile()) {
- ReactDOM.findDOMNode(this.refs.search).value = '';
- }
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_SEARCH,
- results: data,
- is_mention_search: isMentionSearch
- });
- loadProfilesForPosts(data.posts);
+ if (Utils.isMobile() && this.search) {
+ this.search.value = '';
+ }
},
- (err) => {
+ () => {
this.setState({isSearching: false});
- AsyncClient.dispatchError(err, 'search');
}
);
}
@@ -147,7 +136,7 @@ export default class SearchBar extends React.Component {
handleSubmit(e) {
e.preventDefault();
this.performSearch(this.state.searchTerm.trim());
- $(ReactDOM.findDOMNode(this.refs.search)).find('input').blur();
+ $(this.search).find('input').blur();
this.clearFocus();
}
@@ -276,7 +265,9 @@ export default class SearchBar extends React.Component {
>
<span className='fa fa-search sidebar__search-icon'/>
<SuggestionBox
- ref='search'
+ ref={(search) => {
+ this.search = search;
+ }}
className='form-control search-bar'
placeholder={Utils.localizeMessage('search_bar.search', 'Search')}
value={this.state.searchTerm}
diff --git a/webapp/components/search_results.jsx b/webapp/components/search_results.jsx
index a0245b7e4..86d1bac1d 100644
--- a/webapp/components/search_results.jsx
+++ b/webapp/components/search_results.jsx
@@ -124,6 +124,12 @@ export default class SearchResults extends React.Component {
window.removeEventListener('resize', this.handleResize);
}
+ componentDidUpdate(prevProps, prevState) {
+ if (this.state.searchTerm !== prevState.searchTerm) {
+ this.resize();
+ }
+ }
+
handleResize() {
this.setState({
windowWidth: Utils.windowWidth(),
diff --git a/webapp/components/search_results_item.jsx b/webapp/components/search_results_item.jsx
index 76681959e..be62653c0 100644
--- a/webapp/components/search_results_item.jsx
+++ b/webapp/components/search_results_item.jsx
@@ -62,7 +62,7 @@ export default class SearchResultsItem extends React.Component {
render() {
let channelName = null;
const channel = this.props.channel;
- const timestamp = UserStore.getCurrentUser().update_at;
+ const timestamp = UserStore.getCurrentUser().last_picture_update;
const user = this.props.user || {};
const post = this.props.post;
const flagIcon = Constants.FLAG_ICON_SVG;
@@ -285,7 +285,7 @@ export default class SearchResultsItem extends React.Component {
</li>
{rhsControls}
</ul>
- <div className='search-item-snippet'>
+ <div className='search-item-snippet post__body'>
{message}
</div>
</div>
diff --git a/webapp/components/setting_item_max.jsx b/webapp/components/setting_item_max.jsx
index 904e6c8d1..5971ce584 100644
--- a/webapp/components/setting_item_max.jsx
+++ b/webapp/components/setting_item_max.jsx
@@ -49,7 +49,7 @@ export default class SettingItemMax extends React.Component {
submit = (
<input
type='submit'
- className='btn btn-sm btn-primary'
+ className='btn btn-sm btn-primary pull-right'
href='#'
onClick={this.props.submit}
value={Utils.localizeMessage('setting_item_max.save', 'Save')}
@@ -88,7 +88,7 @@ export default class SettingItemMax extends React.Component {
{clientError}
{submit}
<a
- className='btn btn-sm theme'
+ className='btn btn-sm pull-right'
href='#'
onClick={this.props.updateSection}
>
diff --git a/webapp/components/setting_picture.jsx b/webapp/components/setting_picture.jsx
index b74ee8eb7..d1ff60c6a 100644
--- a/webapp/components/setting_picture.jsx
+++ b/webapp/components/setting_picture.jsx
@@ -73,7 +73,7 @@ export default class SettingPicture extends React.Component {
/>
);
} else {
- var confirmButtonClass = 'btn btn-sm';
+ var confirmButtonClass = 'btn btn-sm pull-right';
if (this.props.submitActive) {
confirmButtonClass += ' btn-primary';
} else {
@@ -132,7 +132,7 @@ export default class SettingPicture extends React.Component {
</span>
{confirmButton}
<a
- className='btn btn-sm theme'
+ className='btn btn-sm theme pull-right'
href='#'
onClick={self.props.updateSection}
>
diff --git a/webapp/components/should_verify_email.jsx b/webapp/components/should_verify_email.jsx
index 5ac67e383..61edf9422 100644
--- a/webapp/components/should_verify_email.jsx
+++ b/webapp/components/should_verify_email.jsx
@@ -2,11 +2,12 @@
// See License.txt for license information.
import {FormattedMessage} from 'react-intl';
-import Client from 'client/web_client.jsx';
import React from 'react';
import {Link} from 'react-router/es6';
+import {resendVerification} from 'actions/user_actions.jsx';
+
export default class ShouldVerifyEmail extends React.Component {
constructor(props) {
super(props);
@@ -22,7 +23,7 @@ export default class ShouldVerifyEmail extends React.Component {
this.setState({resendStatus: 'sending'});
- Client.resendVerification(
+ resendVerification(
email,
() => {
this.setState({resendStatus: 'success'});
diff --git a/webapp/components/sidebar_header.jsx b/webapp/components/sidebar_header.jsx
index a5fbd2659..9bc4a5639 100644
--- a/webapp/components/sidebar_header.jsx
+++ b/webapp/components/sidebar_header.jsx
@@ -10,7 +10,7 @@ import * as Utils from 'utils/utils.jsx';
import SidebarHeaderDropdown from './sidebar_header_dropdown.jsx';
import {Tooltip, OverlayTrigger} from 'react-bootstrap';
-import {Preferences, TutorialSteps} from 'utils/constants.jsx';
+import {Preferences, TutorialSteps, Constants} from 'utils/constants.jsx';
import {createMenuTip} from 'components/tutorial/tutorial_tip.jsx';
export default class SidebarHeader extends React.Component {
@@ -59,7 +59,7 @@ export default class SidebarHeader extends React.Component {
profilePicture = (
<img
className='user__picture'
- src={Client.getUsersRoute() + '/' + me.id + '/image?time=' + me.update_at}
+ src={Client.getUsersRoute() + '/' + me.id + '/image?time=' + me.last_picture_update}
/>
);
}
@@ -78,7 +78,7 @@ export default class SidebarHeader extends React.Component {
teamNameWithToolTip = (
<OverlayTrigger
trigger={['hover', 'focus']}
- delayShow={1000}
+ delayShow={Constants.OVERLAY_TIME_DELAY}
placement='bottom'
overlay={<Tooltip id='team-name__tooltip'>{this.props.teamDescription}</Tooltip>}
ref='descriptionOverlay'
@@ -91,16 +91,13 @@ export default class SidebarHeader extends React.Component {
return (
<div className='team__header theme'>
{tutorialTip}
- <a
- href='#'
- onClick={this.toggleDropdown}
- >
+ <div>
{profilePicture}
<div className='header__info'>
<div className='user__name'>{'@' + me.username}</div>
{teamNameWithToolTip}
</div>
- </a>
+ </div>
<SidebarHeaderDropdown
ref='dropdown'
teamType={this.props.teamType}
diff --git a/webapp/components/sidebar_header_dropdown.jsx b/webapp/components/sidebar_header_dropdown.jsx
index 826d9a342..86432e3ab 100644
--- a/webapp/components/sidebar_header_dropdown.jsx
+++ b/webapp/components/sidebar_header_dropdown.jsx
@@ -351,7 +351,10 @@ export default class SidebarHeaderDropdown extends React.Component {
if (moreTeams) {
teams.push(
<li key='joinTeam_li'>
- <Link to='/select_team'>
+ <Link
+ onClick={this.handleClick}
+ to='/select_team'
+ >
<FormattedMessage
id='navbar_dropdown.join'
defaultMessage='Join Another Team'
diff --git a/webapp/components/signup/components/signup_email.jsx b/webapp/components/signup/components/signup_email.jsx
index aa3493c96..9ed10b94c 100644
--- a/webapp/components/signup/components/signup_email.jsx
+++ b/webapp/components/signup/components/signup_email.jsx
@@ -5,11 +5,10 @@ import LoadingScreen from 'components/loading_screen.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import {track} from 'actions/analytics_actions.jsx';
-
-import BrowserStore from 'stores/browser_store.jsx';
+import {getInviteInfo} from 'actions/team_actions.jsx';
+import {loginById, createUserWithInvite} from 'actions/user_actions.jsx';
import * as Utils from 'utils/utils.jsx';
-import Client from 'client/web_client.jsx';
import Constants from 'utils/constants.jsx';
import React from 'react';
@@ -58,7 +57,7 @@ export default class SignupEmail extends React.Component {
loading = false;
} else if (inviteId && inviteId.length > 0) {
loading = true;
- Client.getInviteInfo(
+ getInviteInfo(
inviteId,
(inviteData) => {
if (!inviteData) {
@@ -118,26 +117,12 @@ export default class SignupEmail extends React.Component {
handleSignupSuccess(user, data) {
track('signup', 'signup_user_02_complete');
- Client.loginById(
+ loginById(
data.id,
user.password,
'',
- () => {
- if (this.state.hash > 0) {
- BrowserStore.setGlobalItem(this.state.hash, JSON.stringify({usedBefore: true}));
- }
-
- GlobalActions.emitInitialLoad(
- () => {
- const query = this.props.location.query;
- if (query.redirect_to) {
- browserHistory.push(query.redirect_to);
- } else {
- GlobalActions.redirectUserToDefaultTeam();
- }
- }
- );
- },
+ this.state.hash,
+ null,
(err) => {
if (err.id === 'api.user.login.not_verified.app_error') {
browserHistory.push('/should_verify_email?email=' + encodeURIComponent(user.email) + '&teamname=' + encodeURIComponent(this.state.teamName));
@@ -241,7 +226,7 @@ export default class SignupEmail extends React.Component {
allow_marketing: true
};
- Client.createUserWithInvite(user,
+ createUserWithInvite(user,
this.state.data,
this.state.hash,
this.state.inviteId,
diff --git a/webapp/components/signup/components/signup_ldap.jsx b/webapp/components/signup/components/signup_ldap.jsx
index d80b27159..4c9afc8d6 100644
--- a/webapp/components/signup/components/signup_ldap.jsx
+++ b/webapp/components/signup/components/signup_ldap.jsx
@@ -5,9 +5,10 @@ import FormError from 'components/form_error.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
import {track} from 'actions/analytics_actions.jsx';
+import {addUserToTeamFromInvite} from 'actions/team_actions.jsx';
+import {webLoginByLdap} from 'actions/user_actions.jsx';
import * as Utils from 'utils/utils.jsx';
-import Client from 'client/web_client.jsx';
import React from 'react';
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
@@ -55,7 +56,7 @@ export default class SignupLdap extends React.Component {
this.setState({ldapError: ''});
- Client.webLoginByLdap(
+ webLoginByLdap(
this.state.ldapId,
this.state.ldapPassword,
null,
@@ -69,11 +70,15 @@ export default class SignupLdap extends React.Component {
}
handleLdapSignupSuccess() {
- if (this.props.location.query.id || this.props.location.query.h) {
- Client.addUserToTeamFromInvite(
- this.props.location.query.d,
- this.props.location.query.h,
- this.props.location.query.id,
+ const hash = this.props.location.query.h;
+ const data = this.props.location.query.d;
+ const inviteId = this.props.location.query.id;
+
+ if (inviteId || hash) {
+ addUserToTeamFromInvite(
+ data,
+ hash,
+ inviteId,
() => {
this.finishSignup();
},
diff --git a/webapp/components/signup/signup_controller.jsx b/webapp/components/signup/signup_controller.jsx
index 9bf5936be..737431926 100644
--- a/webapp/components/signup/signup_controller.jsx
+++ b/webapp/components/signup/signup_controller.jsx
@@ -12,6 +12,7 @@ import BrowserStore from 'stores/browser_store.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import Client from 'client/web_client.jsx';
import * as GlobalActions from 'actions/global_actions.jsx';
+import {addUserToTeamFromInvite, getInviteInfo} from 'actions/team_actions.jsx';
import logoImage from 'images/logo.png';
import ErrorBar from 'components/error_bar.jsx';
@@ -68,7 +69,7 @@ export default class SignupController extends React.Component {
const userLoggedIn = UserStore.getCurrentUser() != null;
if ((inviteId || hash) && userLoggedIn) {
- Client.addUserToTeamFromInvite(
+ addUserToTeamFromInvite(
data,
hash,
inviteId,
@@ -79,11 +80,16 @@ export default class SignupController extends React.Component {
}
);
},
- (e) => {
+ () => {
this.setState({ // eslint-disable-line react/no-did-mount-set-state
noOpenServerError: true,
loading: false,
- serverError: e.message
+ serverError: (
+ <FormattedMessage
+ id='signup_user_completed.invalid_invite'
+ defaultMessage='The invite link was invalid. Please speak with your Administrator to receive an invitation.'
+ />
+ )
});
}
);
@@ -92,7 +98,7 @@ export default class SignupController extends React.Component {
}
if (inviteId) {
- Client.getInviteInfo(
+ getInviteInfo(
inviteId,
(inviteData) => {
if (!inviteData) {
diff --git a/webapp/components/suggestion/at_mention_provider.jsx b/webapp/components/suggestion/at_mention_provider.jsx
index 9263c6e50..5f79e08ae 100644
--- a/webapp/components/suggestion/at_mention_provider.jsx
+++ b/webapp/components/suggestion/at_mention_provider.jsx
@@ -6,6 +6,7 @@ import Provider from './provider.jsx';
import ChannelStore from 'stores/channel_store.jsx';
import UserStore from 'stores/user_store.jsx';
+import SuggestionStore from 'stores/suggestion_store.jsx';
import {autocompleteUsersInChannel} from 'actions/user_actions.jsx';
@@ -70,7 +71,7 @@ class AtMentionSuggestion extends Suggestion {
icon = (
<img
className='mention__image'
- src={Client.getUsersRoute() + '/' + user.id + '/image?time=' + user.update_at}
+ src={Client.getUsersRoute() + '/' + user.id + '/image?time=' + user.last_picture_update}
/>
);
}
@@ -161,6 +162,8 @@ export default class AtMentionProvider extends Provider {
});
}
);
+ } else {
+ SuggestionStore.clearSuggestions(suggestionId);
}
}
}
diff --git a/webapp/components/suggestion/channel_mention_provider.jsx b/webapp/components/suggestion/channel_mention_provider.jsx
index 63e6944ac..f1d6d9e76 100644
--- a/webapp/components/suggestion/channel_mention_provider.jsx
+++ b/webapp/components/suggestion/channel_mention_provider.jsx
@@ -51,61 +51,83 @@ class ChannelMentionSuggestion extends Suggestion {
}
export default class ChannelMentionProvider extends Provider {
+ constructor() {
+ super();
+
+ this.lastCompletedWord = '';
+ }
+
handlePretextChanged(suggestionId, pretext) {
- const captured = (/(^|\s)(~([^~]*))$/i).exec(pretext.toLowerCase());
- if (captured) {
- const prefix = captured[3];
+ const captured = (/(^|\s)(~([^~\r\n]*))$/i).exec(pretext.toLowerCase());
- this.startNewRequest(prefix);
+ if (!captured) {
+ // Not a channel mention
+ return;
+ }
- autocompleteChannels(
- prefix,
- (data) => {
- if (this.shouldCancelDispatch(prefix)) {
- return;
- }
+ if (this.lastCompletedWord && captured[0].startsWith(this.lastCompletedWord)) {
+ // It appears we're still matching a channel handle that we already completed
+ return;
+ }
+
+ // Clear the last completed word since we've started to match new text
+ this.lastCompletedWord = '';
+
+ const prefix = captured[3];
+
+ this.startNewRequest(prefix);
+
+ autocompleteChannels(
+ prefix,
+ (data) => {
+ if (this.shouldCancelDispatch(prefix)) {
+ return;
+ }
+
+ const channels = data;
- const channels = data;
-
- // Wrap channels in an outer object to avoid overwriting the 'type' property.
- const wrappedChannels = [];
- const wrappedMoreChannels = [];
- const moreChannels = [];
- channels.forEach((item) => {
- if (ChannelStore.get(item.id)) {
- wrappedChannels.push({
- type: Constants.MENTION_CHANNELS,
- channel: item
- });
- return;
- }
-
- wrappedMoreChannels.push({
- type: Constants.MENTION_MORE_CHANNELS,
+ // Wrap channels in an outer object to avoid overwriting the 'type' property.
+ const wrappedChannels = [];
+ const wrappedMoreChannels = [];
+ const moreChannels = [];
+ channels.forEach((item) => {
+ if (ChannelStore.get(item.id)) {
+ wrappedChannels.push({
+ type: Constants.MENTION_CHANNELS,
channel: item
});
+ return;
+ }
- moreChannels.push(item);
+ wrappedMoreChannels.push({
+ type: Constants.MENTION_MORE_CHANNELS,
+ channel: item
});
- const wrapped = wrappedChannels.concat(wrappedMoreChannels);
- const mentions = wrapped.map((item) => '~' + item.channel.name);
-
- AppDispatcher.handleServerAction({
- type: ActionTypes.RECEIVED_MORE_CHANNELS,
- channels: moreChannels
- });
+ moreChannels.push(item);
+ });
+
+ const wrapped = wrappedChannels.concat(wrappedMoreChannels);
+ const mentions = wrapped.map((item) => '~' + item.channel.name);
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.RECEIVED_MORE_CHANNELS,
+ channels: moreChannels
+ });
+
+ AppDispatcher.handleServerAction({
+ type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS,
+ id: suggestionId,
+ matchedPretext: captured[2],
+ terms: mentions,
+ items: wrapped,
+ component: ChannelMentionSuggestion
+ });
+ }
+ );
+ }
- AppDispatcher.handleServerAction({
- type: ActionTypes.SUGGESTION_RECEIVED_SUGGESTIONS,
- id: suggestionId,
- matchedPretext: captured[2],
- terms: mentions,
- items: wrapped,
- component: ChannelMentionSuggestion
- });
- }
- );
- }
+ handleCompleteWord(term) {
+ this.lastCompletedWord = term;
}
}
diff --git a/webapp/components/suggestion/emoticon_provider.jsx b/webapp/components/suggestion/emoticon_provider.jsx
index 6bb0aee3b..6a4332e2f 100644
--- a/webapp/components/suggestion/emoticon_provider.jsx
+++ b/webapp/components/suggestion/emoticon_provider.jsx
@@ -14,7 +14,7 @@ const MIN_EMOTICON_LENGTH = 2;
class EmoticonSuggestion extends Suggestion {
render() {
const text = this.props.term;
- const emoticon = this.props.item;
+ const emoji = this.props.item.emoji;
let className = 'emoticon-suggestion';
if (this.props.isSelection) {
@@ -30,7 +30,7 @@ class EmoticonSuggestion extends Suggestion {
<img
alt={text}
className='emoticon-suggestion__image'
- src={EmojiStore.getEmojiImageUrl(emoticon)}
+ src={EmojiStore.getEmojiImageUrl(emoji)}
title={text}
/>
</div>
@@ -73,15 +73,24 @@ export default class EmoticonProvider {
// check for named emoji
for (const [name, emoji] of EmojiStore.getEmojis()) {
- if (name.indexOf(partialName) !== -1) {
- matched.push(emoji);
+ if (emoji.aliases) {
+ // This is a system emoji so it may have multiple names
+ for (const alias of emoji.aliases) {
+ if (alias.indexOf(partialName) !== -1) {
+ matched.push({name: alias, emoji});
+ break;
+ }
+ }
+ } else if (name.indexOf(partialName) !== -1) {
+ // This is a custom emoji so it only has one name
+ matched.push({name, emoji});
}
}
// sort the emoticons so that emoticons starting with the entered text come first
matched.sort((a, b) => {
- const aName = a.name || a.aliases[0];
- const bName = b.name || b.aliases[0];
+ const aName = a.name;
+ const bName = b.name;
const aPrefix = aName.startsWith(partialName);
const bPrefix = bName.startsWith(partialName);
@@ -95,7 +104,7 @@ export default class EmoticonProvider {
return 1;
});
- const terms = matched.map((emoticon) => ':' + (emoticon.name || emoticon.aliases[0]) + ':');
+ const terms = matched.map((item) => ':' + item.name + ':');
SuggestionStore.clearSuggestions(suggestionId);
if (terms.length > 0) {
diff --git a/webapp/components/suggestion/search_user_provider.jsx b/webapp/components/suggestion/search_user_provider.jsx
index bff59ace8..70808ca26 100644
--- a/webapp/components/suggestion/search_user_provider.jsx
+++ b/webapp/components/suggestion/search_user_provider.jsx
@@ -41,7 +41,7 @@ class SearchUserSuggestion extends Suggestion {
<i className='fa fa fa-plus-square'/>
<img
className='profile-img rounded'
- src={Client.getUsersRoute() + '/' + item.id + '/image?time=' + item.update_at}
+ src={Client.getUsersRoute() + '/' + item.id + '/image?time=' + item.last_picture_update}
/>
<div className='mention--align'>
<span>
diff --git a/webapp/components/suggestion/suggestion_box.jsx b/webapp/components/suggestion/suggestion_box.jsx
index e9f7c3699..29b9b2d8b 100644
--- a/webapp/components/suggestion/suggestion_box.jsx
+++ b/webapp/components/suggestion/suggestion_box.jsx
@@ -153,6 +153,12 @@ export default class SuggestionBox extends React.Component {
window.requestAnimationFrame(() => {
Utils.setCaretPosition(textbox, prefix.length + term.length + 1);
});
+
+ for (const provider of this.props.providers) {
+ if (provider.handleCompleteWord) {
+ provider.handleCompleteWord(term, matchedPretext);
+ }
+ }
}
handleKeyDown(e) {
diff --git a/webapp/components/suggestion/switch_channel_provider.jsx b/webapp/components/suggestion/switch_channel_provider.jsx
index 301974b9a..0bc30a79f 100644
--- a/webapp/components/suggestion/switch_channel_provider.jsx
+++ b/webapp/components/suggestion/switch_channel_provider.jsx
@@ -35,7 +35,7 @@ class SwitchChannelSuggestion extends Suggestion {
<div className='pull-left'>
<img
className='mention__image'
- src={Client.getUsersRoute() + '/' + item.id + '/image?time=' + item.update_at}
+ src={Client.getUsersRoute() + '/' + item.id + '/image?time=' + item.last_picture_update}
/>
</div>
);
diff --git a/webapp/components/team_general_tab.jsx b/webapp/components/team_general_tab.jsx
index 955a71ac5..0100cad64 100644
--- a/webapp/components/team_general_tab.jsx
+++ b/webapp/components/team_general_tab.jsx
@@ -8,60 +8,9 @@ import SettingItemMax from './setting_item_max.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
-import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage} from 'react-intl';
+import {FormattedMessage} from 'react-intl';
import {updateTeam} from 'actions/team_actions.jsx';
-const holders = defineMessages({
- dirDisabled: {
- id: 'general_tab.dirDisabled',
- defaultMessage: 'Team Directory has been disabled. Please ask a System Admin to enable the Team Directory in the System Console team settings.'
- },
- required: {
- id: 'general_tab.required',
- defaultMessage: 'This field is required'
- },
- chooseName: {
- id: 'general_tab.chooseName',
- defaultMessage: 'Please choose a new name for your team'
- },
- includeDirTitle: {
- id: 'general_tab.includeDirTitle',
- defaultMessage: 'Include this team in the Team Directory'
- },
- yes: {
- id: 'general_tab.yes',
- defaultMessage: 'Yes'
- },
- no: {
- id: 'general_tab.no',
- defaultMessage: 'No'
- },
- dirOff: {
- id: 'general_tab.dirOff',
- defaultMessage: 'Team directory is turned off for this system.'
- },
- openInviteTitle: {
- id: 'general_tab.openInviteTitle',
- defaultMessage: 'Allow any user with an account on this server to join this team'
- },
- codeTitle: {
- id: 'general_tab.codeTitle',
- defaultMessage: 'Invite Code'
- },
- codeDesc: {
- id: 'general_tab.codeDesc',
- defaultMessage: "Click 'Edit' to regenerate Invite Code."
- },
- teamNameInfo: {
- id: 'general_tab.teamNameInfo',
- defaultMessage: 'Set the name of the team as it appears on your sign-in screen and at the top of the left-hand sidebar.'
- },
- teamDescriptionInfo: {
- id: 'general_tab.teamDescriptionInfo',
- defaultMessage: 'Team description provides additional information to help users select the right team. Maximum of 50 characters.'
- }
-});
-
import React from 'react';
class GeneralTab extends React.Component {
@@ -156,13 +105,12 @@ class GeneralTab extends React.Component {
var state = {serverError: '', clientError: ''};
let valid = true;
- const {formatMessage} = this.props.intl;
const name = this.state.name.trim();
if (!name) {
- state.clientError = formatMessage(holders.required);
+ state.clientError = Utils.localizeMessage('general_tab.required', 'This field is required');
valid = false;
} else if (name === this.props.team.display_name) {
- state.clientError = formatMessage(holders.chooseName);
+ state.clientError = Utils.localizeMessage('general_tab.chooseName', 'Please choose a new name for your team');
valid = false;
} else {
state.clientError = '';
@@ -197,7 +145,7 @@ class GeneralTab extends React.Component {
if (inviteId) {
state.clientError = '';
} else {
- state.clientError = this.props.intl.fromatMessage(holders.required);
+ state.clientError = Utils.localizeMessage('general_tab.required', 'This field is required');
valid = false;
}
@@ -230,10 +178,9 @@ class GeneralTab extends React.Component {
var state = {serverError: '', clientError: ''};
let valid = true;
- const {formatMessage} = this.props.intl;
const description = this.state.description.trim();
if (description === this.props.team.description) {
- state.clientError = formatMessage(holders.chooseName);
+ state.clientError = Utils.localizeMessage('general_tab.chooseDescription', 'Please choose a new description for your team');
valid = false;
} else {
state.clientError = '';
@@ -324,8 +271,6 @@ class GeneralTab extends React.Component {
serverError = this.state.serverError;
}
- const {formatMessage} = this.props.intl;
-
let openInviteSection;
if (this.props.activeSection === 'open_invite') {
const inputs = [
@@ -372,7 +317,7 @@ class GeneralTab extends React.Component {
openInviteSection = (
<SettingItemMax
- title={formatMessage(holders.openInviteTitle)}
+ title={Utils.localizeMessage('general_tab.openInviteTitle', 'Allow any user with an account on this server to join this team')}
inputs={inputs}
submit={this.handleOpenInviteSubmit}
server_error={serverError}
@@ -382,14 +327,14 @@ class GeneralTab extends React.Component {
} else {
let describe = '';
if (this.state.allow_open_invite === true) {
- describe = formatMessage(holders.yes);
+ describe = Utils.localizeMessage('general_tab.yes', 'Yes');
} else {
- describe = formatMessage(holders.no);
+ describe = Utils.localizeMessage('general_tab.no', 'No');
}
openInviteSection = (
<SettingItemMin
- title={formatMessage(holders.openInviteTitle)}
+ title={Utils.localizeMessage('general_tab.openInviteTitle', 'Allow any user with an account on this server to join this team')}
describe={describe}
updateSection={this.onUpdateOpenInviteSection}
/>
@@ -427,9 +372,19 @@ class GeneralTab extends React.Component {
</div>
</div>
<div className='setting-list__hint'>
- <FormattedHTMLMessage
+ <FormattedMessage
id='general_tab.codeLongDesc'
- defaultMessage='The Invite Code is used as part of the URL in the team invitation link created by <strong>Get Team Invite Link</strong> in the main menu. Regenerating creates a new team invitation link and invalidates the previous link.'
+ defaultMessage='The Invite Code is used as part of the URL in the team invitation link created by {getTeamInviteLink} in the main menu. Regenerating creates a new team invitation link and invalidates the previous link.'
+ values={{
+ getTeamInviteLink: (
+ <strong>
+ <FormattedMessage
+ id='general_tab.getTeamInviteLink'
+ defaultMessage='Get Team Invite Link'
+ />
+ </strong>
+ )
+ }}
/>
</div>
</div>
@@ -437,7 +392,7 @@ class GeneralTab extends React.Component {
inviteSection = (
<SettingItemMax
- title={formatMessage(holders.codeTitle)}
+ title={Utils.localizeMessage('general_tab.codeTitle', 'Invite Code')}
inputs={inputs}
submit={this.handleInviteIdSubmit}
server_error={serverError}
@@ -448,8 +403,8 @@ class GeneralTab extends React.Component {
} else {
inviteSection = (
<SettingItemMin
- title={formatMessage(holders.codeTitle)}
- describe={formatMessage(holders.codeDesc)}
+ title={Utils.localizeMessage('general_tab.codeTitle', 'Invite Code')}
+ describe={Utils.localizeMessage('general_tab.codeDesc', "Click 'Edit' to regenerate Invite Code.")}
updateSection={this.onUpdateInviteIdSection}
/>
);
@@ -488,11 +443,11 @@ class GeneralTab extends React.Component {
</div>
);
- const nameExtraInfo = <span>{formatMessage(holders.teamNameInfo)}</span>;
+ const nameExtraInfo = <span>{Utils.localizeMessage('general_tab.teamNameInfo', 'Set the name of the team as it appears on your sign-in screen and at the top of the left-hand sidebar.')}</span>;
nameSection = (
<SettingItemMax
- title={formatMessage({id: 'general_tab.teamName'})}
+ title={Utils.localizeMessage('general_tab.teamName', 'Team Name')}
inputs={inputs}
submit={this.handleNameSubmit}
server_error={serverError}
@@ -506,7 +461,7 @@ class GeneralTab extends React.Component {
nameSection = (
<SettingItemMin
- title={formatMessage({id: 'general_tab.teamName'})}
+ title={Utils.localizeMessage('general_tab.teamName', 'Team Name')}
describe={describe}
updateSection={this.onUpdateNameSection}
/>
@@ -546,11 +501,11 @@ class GeneralTab extends React.Component {
</div>
);
- const descriptionExtraInfo = <span>{formatMessage(holders.teamDescriptionInfo)}</span>;
+ const descriptionExtraInfo = <span>{Utils.localizeMessage('general_tab.teamDescriptionInfo', 'Team description provides additional information to help users select the right team. Maximum of 50 characters.')}</span>;
descriptionSection = (
<SettingItemMax
- title={formatMessage({id: 'general_tab.teamDescription'})}
+ title={Utils.localizeMessage('general_tab.teamDescription', 'Team Description')}
inputs={inputs}
submit={this.handleDescriptionSubmit}
server_error={serverError}
@@ -574,7 +529,7 @@ class GeneralTab extends React.Component {
descriptionSection = (
<SettingItemMin
- title={formatMessage({id: 'general_tab.teamDescription'})}
+ title={Utils.localizeMessage('general_tab.teamDescription', 'Team Description')}
describe={describemsg}
updateSection={this.onUpdateDescriptionSection}
/>
@@ -633,10 +588,9 @@ class GeneralTab extends React.Component {
}
GeneralTab.propTypes = {
- intl: intlShape.isRequired,
updateSection: React.PropTypes.func.isRequired,
team: React.PropTypes.object.isRequired,
activeSection: React.PropTypes.string.isRequired
};
-export default injectIntl(GeneralTab);
+export default GeneralTab;
diff --git a/webapp/components/user_list_row.jsx b/webapp/components/user_list_row.jsx
index ff381a30b..3a13ccb66 100644
--- a/webapp/components/user_list_row.jsx
+++ b/webapp/components/user_list_row.jsx
@@ -64,7 +64,7 @@ export default function UserListRow({user, extraInfo, actions, actionProps, acti
className='more-modal__row'
>
<ProfilePicture
- src={`${Client.getUsersRoute()}/${user.id}/image?time=${user.update_at}`}
+ src={`${Client.getUsersRoute()}/${user.id}/image?time=${user.last_picture_update}`}
status={status}
width='32'
height='32'
diff --git a/webapp/components/user_profile.jsx b/webapp/components/user_profile.jsx
index d0267c0d8..d9bd5c378 100644
--- a/webapp/components/user_profile.jsx
+++ b/webapp/components/user_profile.jsx
@@ -56,7 +56,7 @@ export default class UserProfile extends React.Component {
let profileImg = '';
if (this.props.user) {
name = Utils.displayUsername(this.props.user.id);
- profileImg = Client.getUsersRoute() + '/' + this.props.user.id + '/image?time=' + this.props.user.update_at;
+ profileImg = Client.getUsersRoute() + '/' + this.props.user.id + '/image?time=' + this.props.user.last_picture_update;
}
if (this.props.overwriteName) {
diff --git a/webapp/components/user_settings/user_settings_advanced.jsx b/webapp/components/user_settings/user_settings_advanced.jsx
index 70306d871..dc6f4ac0c 100644
--- a/webapp/components/user_settings/user_settings_advanced.jsx
+++ b/webapp/components/user_settings/user_settings_advanced.jsx
@@ -332,7 +332,7 @@ export default class AdvancedSettingsDisplay extends React.Component {
return (
<FormattedMessage
id='user.settings.advance.embed_preview'
- defaultMessage='Show experimental previews of link content, when available'
+ defaultMessage='For the first web link in a message, display a preview of website content below the message, if available'
/>
);
case 'WEBRTC_PREVIEW':
diff --git a/webapp/components/user_settings/user_settings_display.jsx b/webapp/components/user_settings/user_settings_display.jsx
index 9ffc4f721..f51128b6f 100644
--- a/webapp/components/user_settings/user_settings_display.jsx
+++ b/webapp/components/user_settings/user_settings_display.jsx
@@ -191,7 +191,7 @@ export default class UserSettingsDisplay extends React.Component {
<br/>
<FormattedMessage
id='user.settings.display.collapseDesc'
- defaultMessage='Expand links to show a preview of content, when available.'
+ defaultMessage='Set whether previews of image links show as expanded or collapsed by default. This setting can also be controlled using the slash commands /expand and /collapse.'
/>
</div>
</div>
@@ -202,7 +202,7 @@ export default class UserSettingsDisplay extends React.Component {
title={
<FormattedMessage
id='user.settings.display.collapseDisplay'
- defaultMessage='Link previews'
+ defaultMessage='Default appearance of image link previews'
/>
}
inputs={inputs}
@@ -218,14 +218,14 @@ export default class UserSettingsDisplay extends React.Component {
describe = (
<FormattedMessage
id='user.settings.display.collapseOn'
- defaultMessage='On'
+ defaultMessage='Expanded'
/>
);
} else {
describe = (
<FormattedMessage
id='user.settings.display.collapseOff'
- defaultMessage='Off'
+ defaultMessage='Collapsed'
/>
);
}
@@ -239,7 +239,7 @@ export default class UserSettingsDisplay extends React.Component {
title={
<FormattedMessage
id='user.settings.display.collapseDisplay'
- defaultMessage='Link previews'
+ defaultMessage='Default appearance of image link previews'
/>
}
describe={describe}
diff --git a/webapp/components/user_settings/user_settings_general.jsx b/webapp/components/user_settings/user_settings_general.jsx
index 06fe31a9e..d9551dccc 100644
--- a/webapp/components/user_settings/user_settings_general.jsx
+++ b/webapp/components/user_settings/user_settings_general.jsx
@@ -15,7 +15,7 @@ import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import {intlShape, injectIntl, defineMessages, FormattedMessage, FormattedHTMLMessage, FormattedDate} from 'react-intl';
-import {updateUser} from 'actions/user_actions.jsx';
+import {updateUser, uploadProfileImage} from 'actions/user_actions.jsx';
const holders = defineMessages({
usernameReserved: {
@@ -241,11 +241,11 @@ class UserSettingsGeneralTab extends React.Component {
this.setState({loadingPicture: true});
- Client.uploadProfileImage(picture,
+ uploadProfileImage(
+ picture,
() => {
this.updateSection('');
this.submitActive = false;
- AsyncClient.getMe();
},
(err) => {
var state = this.setupInitialState(this.props);
diff --git a/webapp/components/user_settings/user_settings_notifications.jsx b/webapp/components/user_settings/user_settings_notifications.jsx
index 2ee33c092..672f8d6b7 100644
--- a/webapp/components/user_settings/user_settings_notifications.jsx
+++ b/webapp/components/user_settings/user_settings_notifications.jsx
@@ -8,10 +8,9 @@ import DesktopNotificationSettings from './desktop_notification_settings.jsx';
import UserStore from 'stores/user_store.jsx';
-import Client from 'client/web_client.jsx';
-import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
+import {updateUserNotifyProps} from 'actions/user_actions.jsx';
import EmailNotificationSetting from './email_notification_setting.jsx';
import {FormattedMessage} from 'react-intl';
@@ -143,10 +142,10 @@ export default class NotificationsTab extends React.Component {
data.first_name = this.state.firstNameKey.toString();
data.channel = this.state.channelKey.toString();
- Client.updateUserNotifyProps(data,
+ updateUserNotifyProps(
+ data,
() => {
this.props.updateSection('');
- AsyncClient.getMe();
$('.settings-modal .modal-body').scrollTop(0).perfectScrollbar('update');
},
(err) => {
diff --git a/webapp/components/user_settings/user_settings_security.jsx b/webapp/components/user_settings/user_settings_security.jsx
index 3484b8183..210e455b7 100644
--- a/webapp/components/user_settings/user_settings_security.jsx
+++ b/webapp/components/user_settings/user_settings_security.jsx
@@ -9,11 +9,12 @@ import ToggleModalButton from '../toggle_modal_button.jsx';
import PreferenceStore from 'stores/preference_store.jsx';
-import Client from 'client/web_client.jsx';
import * as AsyncClient from 'utils/async_client.jsx';
import * as Utils from 'utils/utils.jsx';
import Constants from 'utils/constants.jsx';
+import {updatePassword, getAuthorizedApps, deactivateMfa, deauthorizeOAuthApp} from 'actions/user_actions.jsx';
+
import $ from 'jquery';
import React from 'react';
import {FormattedMessage, FormattedTime, FormattedDate} from 'react-intl';
@@ -27,7 +28,7 @@ export default class SecurityTab extends React.Component {
this.submitPassword = this.submitPassword.bind(this);
this.setupMfa = this.setupMfa.bind(this);
- this.deactivateMfa = this.deactivateMfa.bind(this);
+ this.removeMfa = this.removeMfa.bind(this);
this.updateCurrentPassword = this.updateCurrentPassword.bind(this);
this.updateNewPassword = this.updateNewPassword.bind(this);
this.updateConfirmPassword = this.updateConfirmPassword.bind(this);
@@ -53,7 +54,7 @@ export default class SecurityTab extends React.Component {
componentDidMount() {
if (global.mm_config.EnableOAuthServiceProvider === 'true') {
- Client.getAuthorizedApps(
+ getAuthorizedApps(
(authorizedApps) => {
this.setState({authorizedApps, serverError: null}); //eslint-disable-line react/no-did-mount-set-state
},
@@ -91,7 +92,7 @@ export default class SecurityTab extends React.Component {
return;
}
- Client.updatePassword(
+ updatePassword(
user.id,
currentPassword,
newPassword,
@@ -118,10 +119,8 @@ export default class SecurityTab extends React.Component {
browserHistory.push('/mfa/setup');
}
- deactivateMfa() {
- Client.updateMfa(
- '',
- false,
+ removeMfa() {
+ deactivateMfa(
() => {
if (global.window.mm_license.MFA === 'true' &&
global.window.mm_config.EnableMultifactorAuthentication === 'true' &&
@@ -131,7 +130,6 @@ export default class SecurityTab extends React.Component {
}
this.props.updateSection('');
- AsyncClient.getMe();
this.setState(this.getDefaultState());
},
(err) => {
@@ -161,7 +159,7 @@ export default class SecurityTab extends React.Component {
deauthorizeApp(e) {
e.preventDefault();
const appId = e.currentTarget.getAttribute('data-app');
- Client.deauthorizeOAuthApp(
+ deauthorizeOAuthApp(
appId,
() => {
const authorizedApps = this.state.authorizedApps.filter((app) => {
@@ -221,7 +219,7 @@ export default class SecurityTab extends React.Component {
<a
className='btn btn-primary'
href='#'
- onClick={this.deactivateMfa}
+ onClick={this.removeMfa}
>
{mfaButtonText}
</a>
@@ -425,6 +423,34 @@ export default class SecurityTab extends React.Component {
</div>
</div>
);
+ } else if (this.props.user.auth_service === Constants.GOOGLE_SERVICE) {
+ inputs.push(
+ <div
+ key='oauthEmailInfo'
+ className='form-group'
+ >
+ <div className='setting-list__hint'>
+ <FormattedMessage
+ id='user.settings.security.passwordGoogleCantUpdate'
+ defaultMessage='Login occurs through Google Apps. Password cannot be updated.'
+ />
+ </div>
+ </div>
+ );
+ } else if (this.props.user.auth_service === Constants.OFFICE365_SERVICE) {
+ inputs.push(
+ <div
+ key='oauthEmailInfo'
+ className='form-group'
+ >
+ <div className='setting-list__hint'>
+ <FormattedMessage
+ id='user.settings.security.passwordOffice365CantUpdate'
+ defaultMessage='Login occurs through Office 365. Password cannot be updated.'
+ />
+ </div>
+ </div>
+ );
}
updateSectionStatus = function resetSection(e) {
@@ -502,6 +528,20 @@ export default class SecurityTab extends React.Component {
defaultMessage='Login done through SAML'
/>
);
+ } else if (this.props.user.auth_service === Constants.GOOGLE_SERVICE) {
+ describe = (
+ <FormattedMessage
+ id='user.settings.security.loginGoogle'
+ defaultMessage='Login done through Google Apps'
+ />
+ );
+ } else if (this.props.user.auth_service === Constants.OFFICE365_SERVICE) {
+ describe = (
+ <FormattedMessage
+ id='user.settings.security.loginOffice365'
+ defaultMessage='Login done through Office 365'
+ />
+ );
}
updateSectionStatus = function updateSection() {
diff --git a/webapp/components/webrtc/components/webrtc_notification.jsx b/webapp/components/webrtc/components/webrtc_notification.jsx
index 5456d6cb8..f69e731f8 100644
--- a/webapp/components/webrtc/components/webrtc_notification.jsx
+++ b/webapp/components/webrtc/components/webrtc_notification.jsx
@@ -197,7 +197,7 @@ export default class WebrtcNotification extends React.Component {
const user = this.state.userCalling;
if (user) {
const username = Utils.displayUsername(user.id);
- const profileImgSrc = Client.getUsersRoute() + '/' + user.id + '/image?time=' + (user.update_at || new Date().getTime());
+ const profileImgSrc = Client.getUsersRoute() + '/' + user.id + '/image?time=' + (user.last_picture_update || new Date().getTime());
const profileImg = (
<img
className='user-popover__image'
diff --git a/webapp/components/webrtc/webrtc_controller.jsx b/webapp/components/webrtc/webrtc_controller.jsx
index 94e5b3475..b8d3d4db6 100644
--- a/webapp/components/webrtc/webrtc_controller.jsx
+++ b/webapp/components/webrtc/webrtc_controller.jsx
@@ -81,14 +81,14 @@ export default class WebrtcController extends React.Component {
const currentUser = UserStore.getCurrentUser();
const remoteUser = UserStore.getProfile(props.userId);
- const remoteUserImage = Client.getUsersRoute() + '/' + remoteUser.id + '/image?time=' + remoteUser.update_at;
+ const remoteUserImage = Client.getUsersRoute() + '/' + remoteUser.id + '/image?time=' + remoteUser.last_picture_update;
this.state = {
windowWidth: Utils.windowWidth(),
windowHeight: Utils.windowHeight(),
channelId: ChannelStore.getCurrentId(),
currentUser,
- currentUserImage: Client.getUsersRoute() + '/' + currentUser.id + '/image?time=' + currentUser.update_at,
+ currentUserImage: Client.getUsersRoute() + '/' + currentUser.id + '/image?time=' + currentUser.last_picture_update,
remoteUserImage,
localMediaLoaded: false,
isPaused: false,
@@ -130,7 +130,7 @@ export default class WebrtcController extends React.Component {
(nextProps.userId !== this.props.userId) ||
(nextProps.isCaller !== this.props.isCaller)) {
const remoteUser = UserStore.getProfile(nextProps.userId);
- const remoteUserImage = Client.getUsersRoute() + '/' + remoteUser.id + '/image?time=' + remoteUser.update_at;
+ const remoteUserImage = Client.getUsersRoute() + '/' + remoteUser.id + '/image?time=' + remoteUser.last_picture_update;
this.setState({
error: null,
remoteUserImage
@@ -644,7 +644,7 @@ export default class WebrtcController extends React.Component {
}
onConnectCall() {
- Client.webrtcToken(
+ WebrtcActions.webrtcToken(
(info) => {
const connectingMsg = (
<FormattedMessage
diff --git a/webapp/i18n/de.json b/webapp/i18n/de.json
index f75631b97..2a95eee5a 100644
--- a/webapp/i18n/de.json
+++ b/webapp/i18n/de.json
@@ -28,6 +28,7 @@
"activity_log.sessionsDescription": "Sitzungen werden erstellt, sobald Sie sich in einem neuen Browser eines Gerätes anmelden. Sitzungen ermöglichen es Ihnen Mattermost ohne erneutes Anmelden nach einer vom System Administrator definierten Zeit zu verwenden. Um sich früher abzumelden, verwenden Sie den 'Abmelden' Button unten, um die Sitzung zu beenden.",
"activity_log_modal.android": "Android",
"activity_log_modal.androidNativeApp": "Android App",
+ "activity_log_modal.desktop": "Native Desktop App",
"activity_log_modal.iphoneNativeApp": "iPhone App",
"add_command.autocomplete": "Auto-Vervollständigung",
"add_command.autocomplete.help": "(Optional) Zeige Slash-Befehle in Autovervollständigungsliste.",
@@ -260,6 +261,7 @@
"admin.email.notificationEmailTitle": "Absenderadresse:",
"admin.email.notificationOrganization": "Adresse in der Fußzeile von Benachrichtigungen:",
"admin.email.notificationOrganizationDescription": "Name und Adresse der Organisation bzw. des Unternehmens, wie sie in E-Mail-Benachrichtigungen von Mattermost angezeigt werden sollen, z.B. \"© Musterfirma GmbH, Musterstraße 23, 59424 Musterhausen, Deutschland\". Wenn dieses Feld leer bleibt, werden der Name und Adresse der Organisation nicht angezeigt.",
+ "admin.email.notificationOrganizationExample": "z.B. \"© Musterfirma GmbH, Musterstraße 23, 59424 Musterhausen, Deutschland\"",
"admin.email.notificationsDescription": "Normalerweise wahr in Produktionsumgebungen. Wenn wahr versucht Mattermost E-Mail Benachrichtigungen zu versenden. Entwickler sollten dies auf falsch für eine schnellere Entwicklung setzen.<br />Durch setzen auf wahr wird der Vorschau Modus Banner entfernt (benötigt aus- und einloggen nach Änderung).",
"admin.email.notificationsTitle": "Aktiviere E-Mail Benachrichtigungen: ",
"admin.email.passwordSaltDescription": "32 Zeichen langer Salt der zum Signieren von Passwort zurücksetzen E-Mails hinzugefügt wird. Zufallsgeneriert bei Installation. \"Neu generieren\" klicken um einen neuen Salt zu erstellen.",
@@ -308,10 +310,20 @@
"admin.general.localization.serverLocaleTitle": "Standardsprache Server:",
"admin.general.log": "Protokollierung",
"admin.general.policy": "Richtlinie",
+ "admin.general.policy.allowEditPostAlways": "Any time",
+ "admin.general.policy.allowEditPostDescription": "Set policy on the length of time authors have to edit their messages after posting.",
+ "admin.general.policy.allowEditPostNever": "Nie",
+ "admin.general.policy.allowEditPostTimeLimit": "seconds after posting",
+ "admin.general.policy.allowEditPostTitle": "Allow users to edit their messages:",
"admin.general.policy.permissionsAdmin": "Team- und Systemadministratoren",
"admin.general.policy.permissionsAll": "Alle Teammitglieder",
"admin.general.policy.permissionsAllChannel": "Alle Kanalmitglieder",
+ "admin.general.policy.permissionsDeletePostAdmin": "Team- und Systemadministratoren",
+ "admin.general.policy.permissionsDeletePostAll": "Message authors can delete their own messages, and Administrators can delete any message",
+ "admin.general.policy.permissionsDeletePostSystemAdmin": "Systemadministratoren",
"admin.general.policy.permissionsSystemAdmin": "Systemadministratoren",
+ "admin.general.policy.restrictPostDeleteDescription": "Set policy on who has permission to delete messages.",
+ "admin.general.policy.restrictPostDeleteTitle": "Allow which users to delete messages:",
"admin.general.policy.restrictPrivateChannelCreationDescription": "Regel festlegen, wer private Gruppen erstellen darf.",
"admin.general.policy.restrictPrivateChannelCreationTitle": "Erlaube Erstellung privater Gruppen für:",
"admin.general.policy.restrictPrivateChannelDeletionCommandLineToolLink": "Kommandozeilenwerkzeug",
@@ -363,6 +375,7 @@
"admin.image.amazonS3BucketExample": "z.B.: \"mattermost-media\"",
"admin.image.amazonS3BucketTitle": "Amazon S3 Bucket:",
"admin.image.amazonS3EndpointDescription": "Hostname Ihres S3 kompatiblen Speicheranbieters. Standardmäßig 's3.amazonaws.com'.",
+ "admin.image.amazonS3EndpointExample": "E.g.: \"s3.amazonaws.com\"",
"admin.image.amazonS3EndpointTitle": "Amazon S3 Endpunkt:",
"admin.image.amazonS3IdDescription": "Erhalten Sie diesen Wert von Ihrem Amazon EC2 Administrator.",
"admin.image.amazonS3IdExample": "z.B.: \"AKIADTOVBGERKLCBV\"",
@@ -513,7 +526,7 @@
"admin.metrics.enableDescription": "Wenn wahr, wird Mattermost Performance Daten sammeln und profilieren. Bitte schauen Sie in die <a href=\"http://docs.mattermost.com/deployment/metrics.html\" target='_blank'>Dokumentation</a> um mehr über die Konfiguration von Performanceüberwachung für Mattermost zu erfahren.",
"admin.metrics.enableTitle": "Performance Monitoring aktivieren:",
"admin.metrics.listenAddressDesc": "Die Adresse auf die der Server hören wird um die Performancemetriken auszugeben.",
- "admin.metrics.listenAddressEx": "z.B.: \":8067\"",
+ "admin.metrics.listenAddressEx": "z.B.: \":8065\"",
"admin.metrics.listenAddressTitle": "Empfangs-Adresse:",
"admin.mfa.bannerDesc": "Multi-Faktor-Authentifizierung ist nur verfügbar für Zugänge mit LDAP und E-Mail Logins. Wenn es Benutzer in Ihrem System mit anderen Loginmethoden gibt, wird es empfohlen das Sie Multi-Faktor-Authentifizierung direkt in SSO oder SAML implementieren.",
"admin.mfa.cluster": "Hoch",
@@ -597,7 +610,7 @@
"admin.saml.assertionConsumerServiceURLDesc": "Geben Sie https://<your-mattermost-url>/login/sso/saml ein. Stellen Sie sicher Sie verwenden HTTP oder HTTPS in Ihrer URL entsprechend Ihrer Serverkonfiguration. Dieses Feld ist auch bekannt als die \"Assertion Consumer Service URL\".",
"admin.saml.assertionConsumerServiceURLEx": "z.B.: \"https://<ihre-mattermost-url>/login/sso/saml\"",
"admin.saml.assertionConsumerServiceURLTitle": "Service Provider Login URL:",
- "admin.saml.bannerDesc": "User attributes in SAML server, including user deactivation or removal, are updated in Mattermost during user login. Learn more at: <a href=\"https://docs.mattermost.com/deployment/sso-saml.html\">https://docs.mattermost.com/deployment/sso-saml.html</a>",
+ "admin.saml.bannerDesc": "Benutzerattribute des SAML-Servers, inklusive Benutzerdeaktivierung oder -entfernung, werden in Mattermost bei der Benutzeranmeldung übernommen. Erfahren Sie mehr darüber in der <a href=\"https://docs.mattermost.com/deployment/sso-saml.html\">Dokumentation</a>.",
"admin.saml.emailAttrDesc": "Das Attribut des LDAP Servers, welches benutzt wird um die E-Mailadressen der Nutzer in Mattermost auszfüllen.",
"admin.saml.emailAttrEx": "z.B.: \"Email\" oder \"PrimaryEmail\"",
"admin.saml.emailAttrTitle": "E-Mail Attribut:",
@@ -834,7 +847,7 @@
"admin.team.maxChannelsExample": "z.B.: \"100\"",
"admin.team.maxChannelsTitle": "Maximal Kanäle pro Team:",
"admin.team.maxNotificationsPerChannelDescription": "Maximale Anzahl von Benutzern in einem Kanal bis Benutzer mit @all,@here und @channel keine Benachrichtigungen aufgrund von Performance auslösen können.",
- "admin.team.maxNotificationsPerChannelExample": "z.B.: \"1000\"",
+ "admin.team.maxNotificationsPerChannelExample": "z.B.: \"10000\"",
"admin.team.maxNotificationsPerChannelTitle": "Maximale Benachrichtigungen pro Kanal:",
"admin.team.maxUsersDescription": "Maximale Anzahl an Benutzern pro Team, inklusive Aktive und Inaktive Benutzer.",
"admin.team.maxUsersExample": "z.B.: \"25\"",
@@ -923,12 +936,14 @@
"analytics.chart.meaningful": "Nicht genügend Daten für eine aussagekräftige Darstellung.",
"analytics.system.activeUsers": "Aktive Nutzer mit Beiträgen",
"analytics.system.channelTypes": "Kanal Typen",
+ "analytics.system.dailyActiveUsers": "Daily Active Users",
"analytics.system.expiredBanner": "Die Enterprise Lizenz im am {date} abgelaufen. Sie haben 15 Tage vom genannten Datum an Zeit die Lizenz zu erneuern, bitte kontaktieren Sie <a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>.",
"analytics.system.expiringBanner": "Die Enterprise Lizenz wird am {date} ablaufen. Um sie zu erneuern kontaktieren Sie bitte <a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>.",
+ "analytics.system.monthlyActiveUsers": "Monthly Active Users",
"analytics.system.postTypes": "Nachrichten, Dateien und Hashtags",
"analytics.system.privateGroups": "Private Gruppen",
"analytics.system.publicChannels": "Öffentliche Kanäle",
- "analytics.system.skippedIntensiveQueries": "To maximize performance, some statistics are disabled. You can re-enable them in config.json. See: <a href='https://docs.mattermost.com/administration/statistics.html' target='_blank'>https://docs.mattermost.com/administration/statistics.html</a>",
+ "analytics.system.skippedIntensiveQueries": "Um die Performance zu maximieren sind einige Statistiken deaktiviert. Sie können diese in der config.json wieder reaktivieren. Sie erfahren mehr in der <a href='https://docs.mattermost.com/administration/statistics.html' target='_blank'>Dokumentation</a>.",
"analytics.system.textPosts": "Nur-Text Beiträge",
"analytics.system.title": "System Statistiken",
"analytics.system.totalChannels": "Kanäle Gesamt",
@@ -1022,7 +1037,6 @@
"backstage_sidebar.integrations.outgoing_webhooks": "Ausgehende Webhooks",
"calling_screen": "Rufe an",
"center_panel.recent": "Klicken Sie hier um zu vorherigen Mittelungen zurückzukehren. ",
- "chanel_header.addMembers": "Mitglieder hinzufügen",
"change_url.close": "Schließen",
"change_url.endWithLetter": "Muss mit einem Buchstaben oder einer Nummer enden",
"change_url.invalidUrl": "Ungültige URL",
@@ -1040,6 +1054,7 @@
"channel_flow.handleTooShort": "Kanal-URL muss zwei oder mehr alphanumerische Zeichen enthalten",
"channel_flow.invalidName": "Ungültiger Kanal Name",
"channel_flow.set_url_title": "Setze {term} URL",
+ "channel_header.addMembers": "Mitglieder hinzufügen",
"channel_header.addToFavorites": "Zu Favoriten hinzufügen",
"channel_header.channel": "Kanal",
"channel_header.channelHeader": "Bearbeite Kanaltitel",
@@ -1079,10 +1094,14 @@
"channel_loader.uploadedFile": " hat eine Datei hochgeladen",
"channel_loader.uploadedImage": " hat ein Bild hochgeladen",
"channel_loader.wrote": " schrieb: ",
+ "channel_members_dropdown.channel_admin": "Channel Admin",
+ "channel_members_dropdown.channel_member": "Kanalmitglieder",
+ "channel_members_dropdown.make_channel_admin": "Make Channel Admin",
+ "channel_members_dropdown.make_channel_member": "Make Channel Member",
+ "channel_members_dropdown.remove_from_channel": "Remove From Channel",
+ "channel_members_dropdown.remove_member": "Remove Member",
"channel_members_modal.addNew": " Füge neue Mitglieder hinzu",
- "channel_members_modal.close": "Schließen",
- "channel_members_modal.remove": "Entfernen",
- "channel_memebers_modal.members": " Mitglieder",
+ "channel_members_modal.members": " Mitglieder",
"channel_modal.cancel": "Abbrechen",
"channel_modal.channel": "Kanal",
"channel_modal.createNew": "Erstelle neue(n) ",
@@ -1091,6 +1110,7 @@
"channel_modal.edit": "Bearbeiten",
"channel_modal.group": "Gruppe",
"channel_modal.header": "Überschrift",
+ "channel_modal.headerEx": "E.g.: \"[Link Title](http://example.com)\"",
"channel_modal.headerHelp": "Der Text der in der Kopfzeile der/des {term} neben dem Namen steht. Zum Beispiel könnten Sie häufig genutzte Links durch Hinzufügen von [Link Titel](http://example.de) anzeigen lassen.",
"channel_modal.modalTitle": "Neue(r) ",
"channel_modal.name": "Name",
@@ -1101,6 +1121,7 @@
"channel_modal.publicChannel1": "Erstelle einen öffentlichen Kanal",
"channel_modal.publicChannel2": "Erstelle einen neuen öffentlichen Kanal dem jeder beitreten kann. ",
"channel_modal.purpose": "Zweck",
+ "channel_modal.purposeEx": "E.g.: \"A channel to file bugs and improvements\"",
"channel_notifications.allActivity": "Für alle Aktivitäten",
"channel_notifications.allUnread": "Für alle ungelesenen Nachrichten",
"channel_notifications.globalDefault": "Globaler Standard ({notifyLevel})",
@@ -1113,6 +1134,7 @@
"channel_notifications.unreadInfo": "Der Kanalname wird fettgedruckt dargestellt wenn es ungelesene Nachrichten gibt. Auswählen von \"Nur für Erwähnungen\" wird die Fettschreibung der Kanäle nur durchführen wenn Sie erwähnt werden.",
"channel_select.placeholder": "--- Wählen Sie einen Kanal ---",
"channel_switch_modal.dm": "(Direktnachricht)",
+ "channel_switch_modal.failed_to_open": "Failed to open channel.",
"channel_switch_modal.help": "Geben Sie den Kanalnamen ein. Verwenden Sie ↑↓ zum Browsen, TAB zum auswählen, ↵ zum Bestätigen, ESC zum verwerfen",
"channel_switch_modal.not_found": "Keine Treffer.",
"channel_switch_modal.submit": "Wechseln",
@@ -1285,15 +1307,14 @@
"find_team.submitError": "Bitte eine gültige E-Mail-Adresse eingeben",
"flag_post.flag": "zur Nachverfolgung markieren",
"flag_post.unflag": "Demarkieren",
+ "general_tab.chooseDescription": "Bitte wählen Sie einen neuen Namen für Ihr Team",
"general_tab.chooseName": "Bitte wählen Sie einen neuen Namen für Ihr Team",
"general_tab.codeDesc": "Auf 'Bearbeiten' klicken um den Einladungscode neu zu generieren.",
"general_tab.codeLongDesc": "Der Einladungscode ist Teil der URL im Team Einladungslink, welcher über <strong>Team Einladungslink erhalten</strong> im Hauptmenü generiert wird. Eine Neugenerierung erstellt einen neuen Team Einladungslink und macht den vorherigen Link ungültig.",
"general_tab.codeTitle": "Einladungscode",
- "general_tab.dirDisabled": "Teamverzeichnis wurde deaktiviert. Bitten Sie einen Systemadministrator das Teamverzeichnis in der Systemkonsole des Teams zu aktivieren.",
- "general_tab.dirOff": "Team Verzeichnis ist für dieses System ausgeschaltet.",
"general_tab.emptyDescription": "Klicken Sie auf 'Bearbeiten' um eine Teambeschreibung hinzuzufügen.",
+ "general_tab.getTeamInviteLink": "Team Einladungslink erhalten",
"general_tab.includeDirDesc": "Dieses Team mit aufzuführen wird den Team Namen im Teamverzeichnis auf der Startseite zeigen und einen Link zur Registrierungsseite bereitstellen.",
- "general_tab.includeDirTitle": "Team im Teamverzeichnis aufführen",
"general_tab.no": "Nein",
"general_tab.openInviteDesc": "Wenn erlaubt wird ein Link zu diesem Team auf der Startseite eingefügt und erlaubt jedem diesem Team beizutreten.",
"general_tab.openInviteTitle": "Erlaube jedem Benutzer mit einem Account auf diesem Server diesem Team beizutreten",
@@ -1395,10 +1416,10 @@
"help.link.formatting": "Nachrichten mit Markdown formatieren",
"help.link.mentioning": "Teammitglieder erwähnen",
"help.link.messaging": "Generelles zur Nachrichtenerstellung",
- "help.mentioning.channel": "#### @Channel\nDie können den kompletten Kanal erwähnen indem Sie `@channel` verwenden. Alle Mitglieder des Kanals erhalten so eine Benachrichtigung die genauso funktioniert als würden sie persönlich erwähnt worden sein.",
+ "help.mentioning.channel": "#### @Channel\nSie können alle Mitglieder eines Kanals erwähnen indem Sie `@channel` verwenden. Alle Mitglieder des Kanals erhalten eine Benachrichtigung als wären sie persönlich erwähnt worden.",
"help.mentioning.channelExample": "@channel Super Arbeit bei den Vorstellungsgesprächen. Ich glaube wir haben einige exzellente potentielle Kandidaten gefunden!",
"help.mentioning.mentions": "## @Erwähnungen\nVerwenden Sie @Erwähnungen um die Aufmerksamkeit eines spezifischen Teammitgliedes zu erhalten.",
- "help.mentioning.recent": "## Letzte Erwähnungen\nKlicken Sie auf das `@`neben dem Suchfeld um die letzten @Erwähnungen und Wörter, die eine Erwähnungen auslösen, aufzurufen. Klicken Sie auf **Sprung** neben dem Suchergebnis in der rechten Seitenleiste um im mittleren Feld an die Stelle der Mittteilung mit der Erwähnungen zu springen.",
+ "help.mentioning.recent": "## Letzte Erwähnungen\nKlicken Sie auf das `@`neben dem Suchfeld um die letzten @Erwähnungen und Wörter, die eine Erwähnungen auslösen, aufzurufen. Klicken Sie auf **Anzeigen** neben dem Suchergebnis in der rechten Seitenleiste um im mittleren Feld an die Stelle der Mittteilung mit der Erwähnungen zu springen.",
"help.mentioning.title": "# Teammitglieder erwähnen\n",
"help.mentioning.triggers": "## Wörter welche Erwähnungen auslösen\nZusätzlich zur Benachrichtigung durch @Benutzername und @channel können Sie eigene Wörter unter **Kontoeinstellungen** > **Benachrichtigungen** > **Wörter, welche Erwähnungen auslösen** definieren welche eine Erwähnungsbenachrichtigung auslösen. Standardmäßig erhalten Sie Erwähnungsmeldungen für Ihren Vornamen und Sie können weitere Wörter durch Eingabe in das Feld separiert durch Kommas hinzufügen. Dies ist hilfreich wenn Sie wegen bestimmter Themen benachrichtig werden wollen, wie zum Beispiel \"Vorstellungsgespräche\" oder \"Marketing\".",
"help.mentioning.username": "#### @Benutzername\nSie können einen Teammitglied Erwähnen indem Sie das `@` Symbol plus seinen Benutzernamen verwenden um ihm eine Benachrichtigung zu senden.\n\nTippen Sie `@` um eine Liste der Mitglieder aufzurufen welche erwähnt werden können. Um die Liste zu filtern tippen Sie die ersten Buchstaben des Benutzernamens, Vornamens, Nachnamens oder Spitznamens. Die **Hoch** und **Runter** Pfeiltasten können zum Scrollen durch die Liste verwendet werden und durch drücken von **Enter** wird die gewählte Erwähnungen übernommen. Sobald dieser ausgewählt wurde wird der volle Name oder Spitzname durch den Benutzernamen ersetzt.\nDas folgende Beispiel sendet eine Erwähnungsnachricht an **alice* welche Sie über den Kanal und die Mitteilung informiert in der sie erwähnt wurde. Wenn **alice** von Mattermost abwesend ist und [E-Mail Benachrichtigung](http://docs.mattermost.com/help/getting-started/configuring-notifications.html#email-notifications) aktiviert hat, erhält sie eine E-Mail mit der Mitteilung.",
@@ -1550,6 +1571,7 @@
"login.passwordChanged": " Passwort erfolgreich aktualisiert",
"login.session_expired": " Ihre Sitzung ist abgelaufen. Bitte melden Sie sich erneut an.",
"login.signIn": "Anmelden",
+ "login.signInLoading": "Signing in...",
"login.signInWith": "Anmelden mit:",
"login.userNotFound": "Ein existierender Zugang mit Ihrem Benutzernamen wurde in diesem Team nicht gefunden.",
"login.username": "Benutzername",
@@ -1561,8 +1583,10 @@
"member_item.makeAdmin": "Zum Admin machen",
"member_item.member": "Mitglied",
"member_list.noUsersAdd": "Keine Benutzer zum Hinzufügen.",
+ "members_popover.manageMembers": "Mitglieder verwalten",
"members_popover.msg": "Nachricht",
"members_popover.title": "Mitglieder",
+ "members_popover.viewMembers": "Zeige Mitglieder",
"mfa.confirm.complete": "<strong>Einrichtung abgeschlossen!</strong>",
"mfa.confirm.okay": "OK",
"mfa.confirm.secure": "Ihr Zugang ist nun abgesichert. Wenn Sie sich das nächste mal anmelden werden Sie gefragt den Code aus der Google Authenticator App von Ihrem Smartphone einzugeben.",
@@ -1665,6 +1689,7 @@
"post_info.mobile.unflag": "Demarkieren",
"post_info.permalink": "Dauerhafter Link",
"post_info.reply": "Antworten",
+ "post_message_view.edited": "(edited)",
"posts_view.loadMore": "Weitere Nachrichten laden",
"posts_view.newMsg": "Neue Nachrichten",
"posts_view.newMsgBelow": "{count} neue {count, plural, one {Mitteilung} other {Mitteilungen}} weiter unten",
@@ -1723,7 +1748,7 @@
"search_header.title2": "Letzte Erwähnungen",
"search_header.title3": "Markierte Nachrichten",
"search_item.direct": "Direktnachricht (mit {username})",
- "search_item.jump": "Sprung",
+ "search_item.jump": "Anzeigen",
"search_results.because": "<ul><li>Wenn Sie nach einem Textteil suchen(z.b. suche nach \"rea\", wenn Sie \"Realität\" oder \"Reaktion\" finden möchten), hängen Sie ein * an Ihren Suchbegriff</li><li>Wegen zu vieler Treffer werden Suchen mit nur zwei Buchstaben oder häufigen Wörtern wie \"this\", \"a\" und \"is\" im Suchergebnis nicht angezeigt.</li></ul>",
"search_results.noResults": "Keine Ergebnisse gefunden. Nochmal versuchen?",
"search_results.usage": "<ul><li>Verwenden Sie <b>\"Anführungszeichen\"</b> zur Suche nach Phrasen</li><li>Verwenden Sie <b>from:</b> um nach Nachrichten eines bestimmten Absenders zu suchen und <b>in:</b> für Nachrichten in einem bestimmten Kanal</li></ul>",
@@ -1899,7 +1924,7 @@
"update_command.question": "Ihre Änderungen könnten einen existierenden Slash-Befehl außer Kraft setzen. Sind Sie sich sicher dass Sie ihn aktualisieren möchten?",
"update_command.update": "Aktualisieren",
"upload_overlay.info": "Datei zum Hochladen hier ablegen.",
- "user.settings.advance.embed_preview": "Zeige experimentelle Vorschauen von Linkinhalten, sofern verfügbar",
+ "user.settings.advance.embed_preview": "For the first web link in a message, display a preview of website content below the message, if available",
"user.settings.advance.embed_toggle": "Zeige Umschalter für alle eingebetteten Vorschauen",
"user.settings.advance.enabledFeatures": "{count, number} {count, plural, one {Feature} other {Features}} aktiviert",
"user.settings.advance.formattingDesc": "Wenn aktiviert werden Nachrichten formatiert, sodass Links erstellt, Emojis angezeigt, Text formatiert und Zeilenumbrüche hinzugefügt werden. Standardmäßig ist dies aktiviert. Ändern der Einstellung erfordert ein Neuladen der Seite.",
@@ -1944,10 +1969,10 @@
"user.settings.display.channelDisplayTitle": "Kanalanzeige-Modus",
"user.settings.display.channeldisplaymode": "Wählen Sie die Breite den mittleren Kanals.",
"user.settings.display.clockDisplay": "Uhrzeit Format",
- "user.settings.display.collapseDesc": "Erweitere Links um eine Vorschau des Inhaltes zu sehen, sofern verfügbar.",
- "user.settings.display.collapseDisplay": "Link Vorschau",
- "user.settings.display.collapseOff": "Aus",
- "user.settings.display.collapseOn": "Ein",
+ "user.settings.display.collapseDesc": "Set whether previews of image links show as expanded or collapsed by default. This setting can also be controlled using the slash commands /expand and /collapse.",
+ "user.settings.display.collapseDisplay": "Default appearance of image link previews",
+ "user.settings.display.collapseOff": "Collapsed",
+ "user.settings.display.collapseOn": "Expanded",
"user.settings.display.fixedWidthCentered": "Feste Breite, zentriert",
"user.settings.display.fontDesc": "Wählen Sie die Schriftart zur Anzeige im Mattermost Interface aus.",
"user.settings.display.fontTitle": "Schriftart",
@@ -2130,7 +2155,9 @@
"user.settings.security.lastUpdated": "Zuletzt aktualisiert am {date} um {time}",
"user.settings.security.ldap": "AD/LDAP",
"user.settings.security.loginGitlab": "Anmeldung durchgeführt durch GitLab",
+ "user.settings.security.loginGoogle": "Login done through Google Apps",
"user.settings.security.loginLdap": "Anmeldung via AD/LDAP",
+ "user.settings.security.loginOffice365": "Login done through Office 365",
"user.settings.security.loginSaml": "Anmeldung durchgeführt durch SAML",
"user.settings.security.logoutActiveSessions": "Zeigen und beenden von aktiven Sitzungen",
"user.settings.security.method": "Anmeldemethode",
@@ -2159,9 +2186,11 @@
"user.settings.security.passwordErrorUppercaseNumberSymbol": "Ihr Passwort muss aus mindestens {min} Zeichen bestehen und mindestens einen Großbuchstaben, eine Ziffer und ein Symbol (wie \"~!@#$%^&*()\") enthalten.",
"user.settings.security.passwordErrorUppercaseSymbol": "Ihr Passwort muss aus mindestens {min} Zeichen bestehen und mindestens einen Großbuchstaben und ein Symbol (wie \"~!@#$%^&*()\") enthalten.",
"user.settings.security.passwordGitlabCantUpdate": "Einloggen findet über GitLab statt. Die Adresse kann nicht aktualisiert werden.",
+ "user.settings.security.passwordGoogleCantUpdate": "Einloggen findet über GitLab statt. Die Adresse kann nicht aktualisiert werden.",
"user.settings.security.passwordLdapCantUpdate": "Einloggen findet über AD/LDAP statt. Die Adresse kann nicht aktualisiert werden.",
"user.settings.security.passwordMatchError": "Die eingegebenen neuen Passwörter stimmen nicht überein.",
"user.settings.security.passwordMinLength": "Ungültige minimale Länge, kann keine Vorschau anzeigen.",
+ "user.settings.security.passwordOffice365CantUpdate": "Einloggen findet über GitLab statt. Die Adresse kann nicht aktualisiert werden.",
"user.settings.security.passwordSamlCantUpdate": "Dieses Feld wird durch Ihren Login Provider festgelegt. Wenn Sie es ändern möchten, müssen Sie dies durch den Login Provider tun.",
"user.settings.security.retypePassword": "Passwort erneut eingeben",
"user.settings.security.saml": "SAML",
diff --git a/webapp/i18n/en.json b/webapp/i18n/en.json
index c2ec7a4e9..6aa994bf3 100644
--- a/webapp/i18n/en.json
+++ b/webapp/i18n/en.json
@@ -28,6 +28,7 @@
"activity_log.sessionsDescription": "Sessions are created when you log in to a new browser on a device. Sessions let you use Mattermost without having to log in again for a time period specified by the System Admin. If you want to log out sooner, use the 'Logout' button below to end a session.",
"activity_log_modal.android": "Android",
"activity_log_modal.androidNativeApp": "Android Native App",
+ "activity_log_modal.desktop": "Native Desktop App",
"activity_log_modal.iphoneNativeApp": "iPhone Native App",
"add_command.autocomplete": "Autocomplete",
"add_command.autocomplete.help": "(Optional) Show slash command in autocomplete list.",
@@ -251,15 +252,16 @@
"admin.email.mhpnsHelp": "Download <a href=\"https://itunes.apple.com/us/app/mattermost/id984966508?mt=8\" target='_blank'>Mattermost iOS app</a> from iTunes. Download <a href=\"https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en\" target='_blank'>Mattermost Android app</a> from Google Play. Learn more about <a href=\"http://docs.mattermost.com/deployment/push.html#hosted-push-notifications-service-hpns\" target='_blank'>HPNS</a>.",
"admin.email.mtpns": "Use iOS and Android apps on iTunes and Google Play with TPNS",
"admin.email.mtpnsHelp": "Download <a href=\"https://itunes.apple.com/us/app/mattermost/id984966508?mt=8\" target='_blank'>Mattermost iOS app</a> from iTunes. Download <a href=\"https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en\" target='_blank'>Mattermost Android app</a> from Google Play. Learn more about <a href=\"http://docs.mattermost.com/deployment/push.html#test-push-notifications-service-tpns\" target='_blank'>TPNS</a>.",
- "admin.email.nofificationOrganizationExample": "Ex. \"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\"",
+ "admin.email.nofificationOrganizationExample": "E.g.: \"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\"",
"admin.email.notificationDisplayDescription": "Display name on email account used when sending notification emails from Mattermost.",
- "admin.email.notificationDisplayExample": "Ex: \"Mattermost Notification\", \"System\", \"No-Reply\"",
+ "admin.email.notificationDisplayExample": "E.g.: \"Mattermost Notification\", \"System\", \"No-Reply\"",
"admin.email.notificationDisplayTitle": "Notification Display Name:",
"admin.email.notificationEmailDescription": "Email address displayed on email account used when sending notification emails from Mattermost.",
- "admin.email.notificationEmailExample": "Ex: \"mattermost@yourcompany.com\", \"admin@yourcompany.com\"",
+ "admin.email.notificationEmailExample": "E.g.: \"mattermost@yourcompany.com\", \"admin@yourcompany.com\"",
"admin.email.notificationEmailTitle": "Notification From Address:",
"admin.email.notificationOrganization": "Notification Footer Mailing Address:",
"admin.email.notificationOrganizationDescription": "Organization name and address displayed on email notifications from Mattermost, such as \"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\". If the field is left empty, the organization name and address will not be displayed.",
+ "admin.email.notificationOrganizationExample": "E.g.: \"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\"",
"admin.email.notificationsDescription": "Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.<br />Setting this to true removes the Preview Mode banner (requires logging out and logging back in after setting is changed).",
"admin.email.notificationsTitle": "Enable Email Notifications: ",
"admin.email.passwordSaltDescription": "32-character salt added to signing of password reset emails. Randomly generated on install. Click \"Regenerate\" to create new salt.",
@@ -278,16 +280,16 @@
"admin.email.requireVerificationTitle": "Require Email Verification: ",
"admin.email.selfPush": "Manually enter Push Notification Service location",
"admin.email.smtpPasswordDescription": " Obtain this credential from administrator setting up your email server.",
- "admin.email.smtpPasswordExample": "Ex: \"yourpassword\", \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
+ "admin.email.smtpPasswordExample": "E.g.: \"yourpassword\", \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
"admin.email.smtpPasswordTitle": "SMTP Server Password:",
"admin.email.smtpPortDescription": "Port of SMTP email server.",
- "admin.email.smtpPortExample": "Ex: \"25\", \"465\", \"587\"",
+ "admin.email.smtpPortExample": "E.g.: \"25\", \"465\", \"587\"",
"admin.email.smtpPortTitle": "SMTP Server Port:",
"admin.email.smtpServerDescription": "Location of SMTP email server.",
- "admin.email.smtpServerExample": "Ex: \"smtp.yourcompany.com\", \"email-smtp.us-east-1.amazonaws.com\"",
+ "admin.email.smtpServerExample": "E.g.: \"smtp.yourcompany.com\", \"email-smtp.us-east-1.amazonaws.com\"",
"admin.email.smtpServerTitle": "SMTP Server:",
"admin.email.smtpUsernameDescription": " Obtain this credential from administrator setting up your email server.",
- "admin.email.smtpUsernameExample": "Ex: \"admin@yourcompany.com\", \"AKIADTOVBGERKLCBV\"",
+ "admin.email.smtpUsernameExample": "E.g.: \"admin@yourcompany.com\", \"AKIADTOVBGERKLCBV\"",
"admin.email.smtpUsernameTitle": "SMTP Server Username:",
"admin.email.testing": "Testing...",
"admin.false": "false",
@@ -308,10 +310,20 @@
"admin.general.localization.serverLocaleTitle": "Default Server Language:",
"admin.general.log": "Logging",
"admin.general.policy": "Policy",
+ "admin.general.policy.allowEditPostAlways": "Any time",
+ "admin.general.policy.allowEditPostDescription": "Set policy on the length of time authors have to edit their messages after posting.",
+ "admin.general.policy.allowEditPostNever": "Never",
+ "admin.general.policy.allowEditPostTimeLimit": "seconds after posting",
+ "admin.general.policy.allowEditPostTitle": "Allow users to edit their messages:",
"admin.general.policy.permissionsAdmin": "Team and System Admins",
"admin.general.policy.permissionsAll": "All team members",
"admin.general.policy.permissionsAllChannel": "All channel members",
+ "admin.general.policy.permissionsDeletePostAdmin": "Team Admins and System Admins",
+ "admin.general.policy.permissionsDeletePostAll": "Message authors can delete their own messages, and Administrators can delete any message",
+ "admin.general.policy.permissionsDeletePostSystemAdmin": "System Admins",
"admin.general.policy.permissionsSystemAdmin": "System Admins",
+ "admin.general.policy.restrictPostDeleteDescription": "Set policy on who has permission to delete messages.",
+ "admin.general.policy.restrictPostDeleteTitle": "Allow which users to delete messages:",
"admin.general.policy.restrictPrivateChannelCreationDescription": "Set policy on who can create private groups.",
"admin.general.policy.restrictPrivateChannelCreationTitle": "Enable private group creation for:",
"admin.general.policy.restrictPrivateChannelDeletionCommandLineToolLink": "command line tool",
@@ -363,6 +375,7 @@
"admin.image.amazonS3BucketExample": "E.g.: \"mattermost-media\"",
"admin.image.amazonS3BucketTitle": "Amazon S3 Bucket:",
"admin.image.amazonS3EndpointDescription": "Hostname of your S3 Compatible Storage provider. Defaults to `s3.amazonaws.com`.",
+ "admin.image.amazonS3EndpointExample": "E.g.: \"s3.amazonaws.com\"",
"admin.image.amazonS3EndpointTitle": "Amazon S3 Endpoint:",
"admin.image.amazonS3IdDescription": "Obtain this credential from your Amazon EC2 administrator.",
"admin.image.amazonS3IdExample": "E.g.: \"AKIADTOVBGERKLCBV\"",
@@ -467,7 +480,7 @@
"admin.ldap.testSuccess": "AD/LDAP Test Successful",
"admin.ldap.uernameAttrDesc": "The attribute in the AD/LDAP server that will be used to populate the username field in Mattermost. This may be the same as the ID Attribute.",
"admin.ldap.userFilterDisc": "(Optional) Enter an AD/LDAP Filter to use when searching for user objects. Only the users selected by the query will be able to access Mattermost. For Active Directory, the query to filter out disabled users is (&(objectCategory=Person)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))).",
- "admin.ldap.userFilterEx": "Ex. \"(objectClass=user)\"",
+ "admin.ldap.userFilterEx": "E.g.: \"(objectClass=user)\"",
"admin.ldap.userFilterTitle": "User Filter:",
"admin.ldap.usernameAttrEx": "E.g.: \"sAMAccountName\"",
"admin.ldap.usernameAttrTitle": "Username Attribute:",
@@ -513,7 +526,7 @@
"admin.metrics.enableDescription": "When true, Mattermost will enable performance monitoring collection and profiling. Please see <a href=\"http://docs.mattermost.com/deployment/metrics.html\" target='_blank'>documentation</a> to learn more about configuring performance monitoring for Mattermost.",
"admin.metrics.enableTitle": "Enable Performance Monitoring:",
"admin.metrics.listenAddressDesc": "The address the server will listen on to expose performance metrics.",
- "admin.metrics.listenAddressEx": "Ex \":8067\"",
+ "admin.metrics.listenAddressEx": "E.g.: \":8067\"",
"admin.metrics.listenAddressTitle": "Listen Address:",
"admin.mfa.bannerDesc": "Multi-factor authentication is only available for accounts with LDAP and email login methods. If there are users on your system with other login methods, it is recommended you set up multi-factor authentication directly with the SSO or SAML provider.",
"admin.mfa.cluster": "High",
@@ -564,7 +577,7 @@
"admin.rate.enableLimiterTitle": "Enable Rate Limiting: ",
"admin.rate.httpHeaderDescription": "When filled in, vary rate limiting by HTTP header field specified (e.g. when configuring NGINX set to \"X-Real-IP\", when configuring AmazonELB set to \"X-Forwarded-For\").",
"admin.rate.httpHeaderExample": "E.g.: \"X-Real-IP\", \"X-Forwarded-For\"",
- "admin.rate.httpHeaderTitle": "Vary rate limit by HTTP header",
+ "admin.rate.httpHeaderTitle": "Vary rate limit by HTTP header:",
"admin.rate.maxBurst": "Maximum Burst Size:",
"admin.rate.maxBurstDescription": "Maximum number of requests allowed beyond the per second query limit.",
"admin.rate.maxBurstExample": "E.g.: \"100\"",
@@ -831,10 +844,10 @@
"admin.team.dirDesc": "When true, teams that are configured to show in team directory will show on main page inplace of creating a new team.",
"admin.team.dirTitle": "Enable Team Directory: ",
"admin.team.maxChannelsDescription": "Maximum total number of channels per team, including both active and deleted channels.",
- "admin.team.maxChannelsExample": "Ex \"100\"",
+ "admin.team.maxChannelsExample": "E.g.: \"100\"",
"admin.team.maxChannelsTitle": "Max Channels Per Team:",
"admin.team.maxNotificationsPerChannelDescription": "Maximum total number of users in a channel before users typing messages, @all, @here, and @channel no longer send notifications because of performance.",
- "admin.team.maxNotificationsPerChannelExample": "Ex \"1000\"",
+ "admin.team.maxNotificationsPerChannelExample": "E.g.: \"1000\"",
"admin.team.maxNotificationsPerChannelTitle": "Max Notifications Per Channel:",
"admin.team.maxUsersDescription": "Maximum total number of users per team, including both active and inactive users.",
"admin.team.maxUsersExample": "E.g.: \"25\"",
@@ -901,13 +914,13 @@
"admin.webrtc.gatewayWebsocketUrlTitle": "Gateway WebSocket URL:",
"admin.webrtc.stunUriDescription": "Enter your STUN URI as stun:<your-stun-url>:<port>. STUN is a standardized network protocol to allow an end host to assist devices to access its public IP address if it is located behind a NAT.",
"admin.webrtc.stunUriExample": "E.g.: \"stun:webrtc.mattermost.com:5349\"",
- "admin.webrtc.stunUriTitle": "STUN URI",
+ "admin.webrtc.stunUriTitle": "STUN URI:",
"admin.webrtc.turnSharedKeyDescription": "Enter your TURN Server Shared Key. This is used to created dynamic passwords to establish the connection. Each password is valid for a short period of time.",
"admin.webrtc.turnSharedKeyExample": "E.g.: \"bXdkOWQxc3d0Ynk3emY5ZmsxZ3NtazRjaWg=\"",
"admin.webrtc.turnSharedKeyTitle": "TURN Shared Key:",
"admin.webrtc.turnUriDescription": "Enter your TURN URI as turn:<your-turn-url>:<port>. TURN is a standardized network protocol to allow an end host to assist devices to establish a connection by using a relay public IP address if it is located behind a symmetric NAT.",
"admin.webrtc.turnUriExample": "E.g.: \"turn:webrtc.mattermost.com:5349\"",
- "admin.webrtc.turnUriTitle": "TURN URI",
+ "admin.webrtc.turnUriTitle": "TURN URI:",
"admin.webrtc.turnUsernameDescription": "Enter your TURN Server Username.",
"admin.webrtc.turnUsernameExample": "E.g.: \"myusername\"",
"admin.webrtc.turnUsernameTitle": "TURN Username:",
@@ -923,8 +936,10 @@
"analytics.chart.meaningful": "Not enough data for a meaningful representation.",
"analytics.system.activeUsers": "Active Users With Posts",
"analytics.system.channelTypes": "Channel Types",
+ "analytics.system.dailyActiveUsers": "Daily Active Users",
"analytics.system.expiredBanner": "The Enterprise license expired on {date}. You have 15 days from this date to renew the license, please contact <a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>.",
"analytics.system.expiringBanner": "The Enterprise license is expiring on {date}. To renew your license, please contact <a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>.",
+ "analytics.system.monthlyActiveUsers": "Monthly Active Users",
"analytics.system.postTypes": "Posts, Files and Hashtags",
"analytics.system.privateGroups": "Private Groups",
"analytics.system.publicChannels": "Public Channels",
@@ -1022,7 +1037,6 @@
"backstage_sidebar.integrations.outgoing_webhooks": "Outgoing Webhooks",
"calling_screen": "Calling",
"center_panel.recent": "Click here to jump to recent messages. ",
- "chanel_header.addMembers": "Add Members",
"change_url.close": "Close",
"change_url.endWithLetter": "Must end with a letter or number",
"change_url.invalidUrl": "Invalid URL",
@@ -1040,6 +1054,7 @@
"channel_flow.handleTooShort": "Channel URL must be 2 or more lowercase alphanumeric characters",
"channel_flow.invalidName": "Invalid Channel Name",
"channel_flow.set_url_title": "Set {term} URL",
+ "channel_header.addMembers": "Add Members",
"channel_header.addToFavorites": "Add to Favorites",
"channel_header.channel": "Channel",
"channel_header.channelHeader": "Edit Channel Header",
@@ -1079,10 +1094,14 @@
"channel_loader.uploadedFile": " uploaded a file",
"channel_loader.uploadedImage": " uploaded an image",
"channel_loader.wrote": " wrote: ",
+ "channel_members_dropdown.channel_admin": "Channel Admin",
+ "channel_members_dropdown.channel_member": "Channel Member",
+ "channel_members_dropdown.make_channel_admin": "Make Channel Admin",
+ "channel_members_dropdown.make_channel_member": "Make Channel Member",
+ "channel_members_dropdown.remove_from_channel": "Remove From Channel",
+ "channel_members_dropdown.remove_member": "Remove Member",
"channel_members_modal.addNew": " Add New Members",
- "channel_members_modal.close": "Close",
- "channel_members_modal.remove": "Remove",
- "channel_memebers_modal.members": " Members",
+ "channel_members_modal.members": " Members",
"channel_modal.cancel": "Cancel",
"channel_modal.channel": "Channel",
"channel_modal.createNew": "Create New ",
@@ -1091,6 +1110,7 @@
"channel_modal.edit": "Edit",
"channel_modal.group": "Group",
"channel_modal.header": "Header",
+ "channel_modal.headerEx": "E.g.: \"[Link Title](http://example.com)\"",
"channel_modal.headerHelp": "Set text that will appear in the header of the {term} beside the {term} name. For example, include frequently used links by typing [Link Title](http://example.com).",
"channel_modal.modalTitle": "New ",
"channel_modal.name": "Name",
@@ -1101,6 +1121,7 @@
"channel_modal.publicChannel1": "Create a public channel",
"channel_modal.publicChannel2": "Create a new public channel anyone can join. ",
"channel_modal.purpose": "Purpose",
+ "channel_modal.purposeEx": "E.g.: \"A channel to file bugs and improvements\"",
"channel_notifications.allActivity": "For all activity",
"channel_notifications.allUnread": "For all unread messages",
"channel_notifications.globalDefault": "Global default ({notifyLevel})",
@@ -1113,6 +1134,7 @@
"channel_notifications.unreadInfo": "The channel name is bolded in the sidebar when there are unread messages. Selecting \"Only for mentions\" will bold the channel only when you are mentioned.",
"channel_select.placeholder": "--- Select a channel ---",
"channel_switch_modal.dm": "(Direct Message)",
+ "channel_switch_modal.failed_to_open": "Failed to open channel.",
"channel_switch_modal.help": "Type channel name. Use ↑↓ to browse, TAB to select, ↵ to confirm, ESC to dismiss",
"channel_switch_modal.not_found": "No matches found.",
"channel_switch_modal.submit": "Switch",
@@ -1285,15 +1307,14 @@
"find_team.submitError": "Please enter a valid email address",
"flag_post.flag": "Flag for follow up",
"flag_post.unflag": "Unflag",
+ "general_tab.chooseDescription": "Please choose a new description for your team",
"general_tab.chooseName": "Please choose a new name for your team",
"general_tab.codeDesc": "Click 'Edit' to regenerate Invite Code.",
- "general_tab.codeLongDesc": "The Invite Code is used as part of the URL in the team invitation link created by <strong>Get Team Invite Link</strong> in the main menu. Regenerating creates a new team invitation link and invalidates the previous link.",
+ "general_tab.codeLongDesc": "The Invite Code is used as part of the URL in the team invitation link created by {getTeamInviteLink} in the main menu. Regenerating creates a new team invitation link and invalidates the previous link.",
"general_tab.codeTitle": "Invite Code",
- "general_tab.dirDisabled": "Team Directory has been disabled. Please ask a System Admin to enable the Team Directory in the System Console team settings.",
- "general_tab.dirOff": "Team directory is turned off for this system.",
"general_tab.emptyDescription": "Click 'Edit' to add a team description.",
+ "general_tab.getTeamInviteLink": "Get Team Invite Link",
"general_tab.includeDirDesc": "Including this team will display the team name from the Team Directory section of the Home Page, and provide a link to the sign-in page.",
- "general_tab.includeDirTitle": "Include this team in the Team Directory",
"general_tab.no": "No",
"general_tab.openInviteDesc": "When allowed, a link to this team will be included on the landing page allowing anyone with an account to join this team.",
"general_tab.openInviteTitle": "Allow any user with an account on this server to join this team",
@@ -1550,6 +1571,7 @@
"login.passwordChanged": " Password updated successfully",
"login.session_expired": " Your session has expired. Please login again.",
"login.signIn": "Sign in",
+ "login.signInLoading": "Signing in...",
"login.signInWith": "Sign in with:",
"login.userNotFound": "We couldn't find an account matching your login credentials.",
"login.username": "Username",
@@ -1561,8 +1583,10 @@
"member_item.makeAdmin": "Make Admin",
"member_item.member": "Member",
"member_list.noUsersAdd": "No users to add.",
+ "members_popover.manageMembers": "Manage Members",
"members_popover.msg": "Message",
"members_popover.title": "Members",
+ "members_popover.viewMembers": "View Members",
"mfa.confirm.complete": "<strong>Set up complete!</strong>",
"mfa.confirm.okay": "Okay",
"mfa.confirm.secure": "Your account is now secure. Next time you sign in, you will be asked to enter a code from the Google Authenticator app on your phone.",
@@ -1665,6 +1689,7 @@
"post_info.mobile.unflag": "Unflag",
"post_info.permalink": "Permalink",
"post_info.reply": "Reply",
+ "post_message_view.edited": "(edited)",
"posts_view.loadMore": "Load more messages",
"posts_view.newMsg": "New Messages",
"posts_view.newMsgBelow": "New {count, plural, one {message} other {messages}} below",
@@ -1899,7 +1924,7 @@
"update_command.question": "Your changes may break the existing slash command. Are you sure you would like to update it?",
"update_command.update": "Update",
"upload_overlay.info": "Drop a file to upload it.",
- "user.settings.advance.embed_preview": "Show experimental previews of link content, when available",
+ "user.settings.advance.embed_preview": "For the first web link in a message, display a preview of website content below the message, if available",
"user.settings.advance.embed_toggle": "Show toggle for all embed previews",
"user.settings.advance.enabledFeatures": "{count, number} {count, plural, one {Feature} other {Features}} Enabled",
"user.settings.advance.formattingDesc": "If enabled, posts will be formatted to create links, show emoji, style the text, and add line breaks. By default, this setting is enabled. Changing this setting requires the page to be refreshed.",
@@ -1944,10 +1969,10 @@
"user.settings.display.channelDisplayTitle": "Channel Display Mode",
"user.settings.display.channeldisplaymode": "Select the width of the center channel.",
"user.settings.display.clockDisplay": "Clock Display",
- "user.settings.display.collapseDesc": "Expand links to show a preview of content, when available.",
- "user.settings.display.collapseDisplay": "Link previews",
- "user.settings.display.collapseOff": "Off",
- "user.settings.display.collapseOn": "On",
+ "user.settings.display.collapseDesc": "Set whether previews of image links show as expanded or collapsed by default. This setting can also be controlled using the slash commands /expand and /collapse.",
+ "user.settings.display.collapseDisplay": "Default appearance of image link previews",
+ "user.settings.display.collapseOff": "Collapsed",
+ "user.settings.display.collapseOn": "Expanded",
"user.settings.display.fixedWidthCentered": "Fixed width, centered",
"user.settings.display.fontDesc": "Select the font displayed in the Mattermost user interface.",
"user.settings.display.fontTitle": "Display Font",
@@ -2130,7 +2155,9 @@
"user.settings.security.lastUpdated": "Last updated {date} at {time}",
"user.settings.security.ldap": "AD/LDAP",
"user.settings.security.loginGitlab": "Login done through GitLab",
+ "user.settings.security.loginGoogle": "Login done through Google Apps",
"user.settings.security.loginLdap": "Login done through AD/LDAP",
+ "user.settings.security.loginOffice365": "Login done through Office 365",
"user.settings.security.loginSaml": "Login done through SAML",
"user.settings.security.logoutActiveSessions": "View and Logout of Active Sessions",
"user.settings.security.method": "Sign-in Method",
@@ -2159,9 +2186,11 @@
"user.settings.security.passwordErrorUppercaseNumberSymbol": "Your password must contain at least {min} characters made up of at least one uppercase letter, at least one number, and at least one symbol (e.g. \"~!@#$%^&*()\").",
"user.settings.security.passwordErrorUppercaseSymbol": "Your password must contain at least {min} characters made up of at least one uppercase letter and at least one symbol (e.g. \"~!@#$%^&*()\").",
"user.settings.security.passwordGitlabCantUpdate": "Login occurs through GitLab. Password cannot be updated.",
+ "user.settings.security.passwordGoogleCantUpdate": "Login occurs through Google Apps. Password cannot be updated.",
"user.settings.security.passwordLdapCantUpdate": "Login occurs through AD/LDAP. Password cannot be updated.",
"user.settings.security.passwordMatchError": "The new passwords you entered do not match.",
"user.settings.security.passwordMinLength": "Invalid minimum length, cannot show preview.",
+ "user.settings.security.passwordOffice365CantUpdate": "Login occurs through Office 365. Password cannot be updated.",
"user.settings.security.passwordSamlCantUpdate": "This field is handled through your login provider. If you want to change it, you need to do so through your login provider.",
"user.settings.security.retypePassword": "Retype New Password",
"user.settings.security.saml": "SAML",
diff --git a/webapp/i18n/es.json b/webapp/i18n/es.json
index c9adb3c48..7b3e4fb76 100644
--- a/webapp/i18n/es.json
+++ b/webapp/i18n/es.json
@@ -28,6 +28,7 @@
"activity_log.sessionsDescription": "Las sesiones son creadas cuando inicias sesión desde un nuevo navegador en un dispositivo. Las Sesiones te permiten utilizar Mattermost sin tener que volver a iniciar sesión por un período de tiempo especificado por el Administrador de Sistema. Si deseas cerrar sesión antes de que se cumpla este tiempo, Utiliza el botón de 'Cerrar Sesión' en la parte de abajo.",
"activity_log_modal.android": "Android",
"activity_log_modal.androidNativeApp": "Android App Nativa",
+ "activity_log_modal.desktop": "App Nativa de Escritorio",
"activity_log_modal.iphoneNativeApp": "iPhone App Nativa",
"add_command.autocomplete": "Autocompletar",
"add_command.autocomplete.help": "(Opcional) Mostrar el comando de barra en la lista de autocompletado.",
@@ -232,7 +233,7 @@
"admin.email.allowEmailSignInTitle": "Habilitar el inicio de sesión con el correo electrónico: ",
"admin.email.allowSignupDescription": "Cuando está en verdadero, Mattermost permite la creación de equipos y cuentas utilizando el correo electrónico y contraseña. Este valor debe estar en falso sólo cuando quieres limitar el inicio de sesión a través de servicios tipo OAuth o AD/LDAP.",
"admin.email.allowSignupTitle": "Habilitar la creación de la cuenta con el correo electrónico: ",
- "admin.email.allowUsernameSignInDescription": "Cuando es verdadero, Mattermost permite a los usuarios iniciar sesión con el nombre de usuario y contraseña. Esta opción normalmente se utiliza cuando la verificación de correo electrónico está deshabilitada.",
+ "admin.email.allowUsernameSignInDescription": "Cuando es verdadero, Mattermost permite a los usuarios iniciar sesión con el nombre de usuario y contraseña. Esta opción normalmente se utiliza cuando la verificación de correo electrónico está inhabilitada.",
"admin.email.allowUsernameSignInTitle": "Habilitar el inicio de sesión con nombre de usuario: ",
"admin.email.connectionSecurityTest": "Probar Conexión",
"admin.email.easHelp": "Conoce más acerca de como compilar y desplegar tus propias aplicaciones moviles desde un <a href=\"http://docs.mattermost.com/deployment/push.html#enterprise-app-store-eas\" target='_blank'>App Store de Empresa</a>.",
@@ -260,6 +261,7 @@
"admin.email.notificationEmailTitle": "Dirección para Notificación de correo electrónico:",
"admin.email.notificationOrganization": "Dirección del pie de página",
"admin.email.notificationOrganizationDescription": "Nombre de la organización y dirección que se muestra en las notificaciones de correo electrónico enviadas desde Mattermost, como \"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\". Si este campo se deja en blanco, el nombre dela organización y su dirección no serán mostrados.",
+ "admin.email.notificationOrganizationExample": "Ej: \"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\"",
"admin.email.notificationsDescription": "Normalmente se asigna como verdadero en producción. Cuando es verdadero, Mattermost intenta enviar las notificaciones por correo electrónico. Los desarrolladores puede que quieran dejar esta opción en falso para saltarse la configuración de correos para desarrollar más rápido.<br/>Asignar está opción como verdadero remueve la notificación de Modo de Prueba (requiere cerrar la sesión y abrirla nuevamente para que los cambios surjan efecto).",
"admin.email.notificationsTitle": "Habilitar Notificaciones por Correo Electrónico: ",
"admin.email.passwordSaltDescription": "Un salt de 32-caracteres es añadido a la firma de correos para restablecer la contraseña. Aleatoriamente generado en la instalación. Haz clic en \"Regenerar\" para crear un nuevo salt.",
@@ -308,10 +310,20 @@
"admin.general.localization.serverLocaleTitle": "Idioma predeterminado para el Servidor:",
"admin.general.log": "Registro de actividad",
"admin.general.policy": "Política",
+ "admin.general.policy.allowEditPostAlways": "Cualquier momento",
+ "admin.general.policy.allowEditPostDescription": "Establecer la política sobre la cantidad de tiempo que los autores tienen para editar sus mensajes después de la publicación.",
+ "admin.general.policy.allowEditPostNever": "Nunca",
+ "admin.general.policy.allowEditPostTimeLimit": "segundos después de la publicación",
+ "admin.general.policy.allowEditPostTitle": "Permitir a los siguientes usuarios editar sus mensajes:",
"admin.general.policy.permissionsAdmin": "Administradores de Equipo y Sistema",
"admin.general.policy.permissionsAll": "Todos los miembros del equipo",
"admin.general.policy.permissionsAllChannel": "Todos los miembros del canal",
+ "admin.general.policy.permissionsDeletePostAdmin": "Administradores de Equipo y Sistema",
+ "admin.general.policy.permissionsDeletePostAll": "Los autores de los mensajes pueden borrar sus propios mensajes, y los Administradores pueden borrar cualquier mensaje",
+ "admin.general.policy.permissionsDeletePostSystemAdmin": "Administradores de Sistema",
"admin.general.policy.permissionsSystemAdmin": "Administradores de Sistema",
+ "admin.general.policy.restrictPostDeleteDescription": "Establecer la política sobre quién tiene permiso para eliminar los mensajes.",
+ "admin.general.policy.restrictPostDeleteTitle": "Permitir a los siguientes usuarios borrar los mensajes:",
"admin.general.policy.restrictPrivateChannelCreationDescription": "Establece la política de quién puede crear grupos privados.",
"admin.general.policy.restrictPrivateChannelCreationTitle": "Habilitar la creación de grupos privados a:",
"admin.general.policy.restrictPrivateChannelDeletionCommandLineToolLink": "herramienta de línea de comandos",
@@ -363,6 +375,7 @@
"admin.image.amazonS3BucketExample": "Ej.: \"mattermost-media\"",
"admin.image.amazonS3BucketTitle": "Amazon S3 Bucket:",
"admin.image.amazonS3EndpointDescription": "Nombre de host del proveedor de Almacenamiento compatible con S3. Por defecto `s3.amazonaws.com`.",
+ "admin.image.amazonS3EndpointExample": "Ej: \"s3.amazonaws.com\"",
"admin.image.amazonS3EndpointTitle": "Extremo de Amazon S3:",
"admin.image.amazonS3IdDescription": "Obetener esta credencial del administrador de tu Amazon EC2",
"admin.image.amazonS3IdExample": "Ej.: \"AKIADTOVBGERKLCBV\"",
@@ -458,7 +471,7 @@
"admin.ldap.skipCertificateVerification": "Omitir la Verificación del Certificado:",
"admin.ldap.skipCertificateVerificationDesc": "Omite la verificación del certificado para las conexiones TLS o STARTTLS. No recomendado para ambientes de producción donde TLS es requerido. Utilizalo sólamente para pruebas.",
"admin.ldap.syncFailure": "Error de Sincronización: {error}",
- "admin.ldap.syncIntervalHelpText": "La Sincronización AD/LDAP actualiza la información de usuarios en Mattermost para reflejar las actualizaciones en el servidor AD/LDAP. Por ejemplo, cuando un el nombre de usuario cambia en el servidor AD/LDAP, el cambio es reflejado en Mattermost cuando se realiza la sincronización. Las cuentas eliminadas o deshabilitadas en el servidor AD/LDAP tendrán sus cuentas en Mattermost como \"Inactiva\" y las sesiones revocadas. Mattermost realiza la sincronización en el intervalo de ingresado. Por ejemplo, si se introduce 60, Mattermost sincroniza cada 60 minutos.",
+ "admin.ldap.syncIntervalHelpText": "La Sincronización AD/LDAP actualiza la información de usuarios en Mattermost para reflejar las actualizaciones en el servidor AD/LDAP. Por ejemplo, cuando un el nombre de usuario cambia en el servidor AD/LDAP, el cambio es reflejado en Mattermost cuando se realiza la sincronización. Las cuentas eliminadas o inhabilitadas en el servidor AD/LDAP tendrán sus cuentas en Mattermost como \"Inactiva\" y las sesiones revocadas. Mattermost realiza la sincronización en el intervalo de ingresado. Por ejemplo, si se introduce 60, Mattermost sincroniza cada 60 minutos.",
"admin.ldap.syncIntervalTitle": "Intervalo de Sincronización (minutos):",
"admin.ldap.syncNowHelpText": "Inicia una sincronización AD/LDAP inmediatamente.",
"admin.ldap.sync_button": "Sincronizar AD/LDAP Ahora",
@@ -466,7 +479,7 @@
"admin.ldap.testHelpText": "Comprueba si el servidor Mattermost puede conectar con el servidor AD/LDAP especificado. Consulta el archivo de registro de mensajes de error para más detalles.",
"admin.ldap.testSuccess": "Prueba de AD/LDAP Satisfactoria",
"admin.ldap.uernameAttrDesc": "El atributo en el servidor AD/LDAP que se utilizará para poblar el nombre de usuario en Mattermost. Este puede ser igual al Atributo Id.",
- "admin.ldap.userFilterDisc": "(Opcional) Introduce un Filtro AD/LDAP a utilizar para buscar los objetos de usuario. Sólo los usuarios seleccionados por la consulta será capaz de acceder a Mattermost. Para Active Directory, la consulta para filtrar a los usuarios con deshabilitados es (&(objectCategory=Persona)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))).",
+ "admin.ldap.userFilterDisc": "(Opcional) Introduce un Filtro AD/LDAP a utilizar para buscar los objetos de usuario. Sólo los usuarios seleccionados por la consulta será capaz de acceder a Mattermost. Para Active Directory, la consulta para filtrar a los usuarios con inhabilitados es (&(objectCategory=Persona)(!(UserAccountControl:1.2.840.113556.1.4.803:=2))).",
"admin.ldap.userFilterEx": "Ej: \"(objectClass=user)\"",
"admin.ldap.userFilterTitle": "Filtro de Usuario:",
"admin.ldap.usernameAttrEx": "Ej.: \"sAMAccountName\"",
@@ -513,7 +526,7 @@
"admin.metrics.enableDescription": "Cuando es verdadero, Mattermost habilitará el monitoreo de desempeño, la recolección y la elaboración de perfiles. Por favor, consulta la <a href=\"http://docs.mattermost.com/deployment/metrics.html\" target='_blank'>documentación</a> para obtener más información acerca de la configuración del monitoreo del rendimiento para Mattermost.",
"admin.metrics.enableTitle": "Habilitar el Monitoreo de Desempeño:",
"admin.metrics.listenAddressDesc": "La dirección del servidor que escuchará para mostrar las métricas de rendimiento.",
- "admin.metrics.listenAddressEx": "Ej \":8067\"",
+ "admin.metrics.listenAddressEx": "Ej: \":8067\"",
"admin.metrics.listenAddressTitle": "Dirección de escucha:",
"admin.mfa.bannerDesc": "La autenticación de múltiples factores sólo está disponible para los métodos de inicio de sesión con cuentas LDAP y de correo electrónico. Si hay usuarios en su sistema con otros métodos de inicio de sesión, se recomienda configurar la autenticación de múltiples factores directamente con el SSO o proveedor SAML.",
"admin.mfa.cluster": "Alta",
@@ -653,12 +666,12 @@
"admin.save": "Guardar",
"admin.saving": "Guardando...",
"admin.security.connection": "Conexiones",
- "admin.security.inviteSalt.disabled": "El salt para invitaciones no puede ser cambiado, mientras que el envío de correos electrónicos está deshabilitado.",
+ "admin.security.inviteSalt.disabled": "El salt para invitaciones no puede ser cambiado, mientras que el envío de correos electrónicos está inhabilitado.",
"admin.security.login": "Inicio de Sesión",
"admin.security.password": "Contraseña",
- "admin.security.passwordResetSalt.disabled": "El salt para restablecer contraseñas no puede ser cambiado, mientras que el envío de correos electrónicos está deshabilitado.",
+ "admin.security.passwordResetSalt.disabled": "El salt para restablecer contraseñas no puede ser cambiado, mientras que el envío de correos electrónicos está inhabilitado.",
"admin.security.public_links": "Enlaces Públicos",
- "admin.security.requireEmailVerification.disabled": "Verificación de correo electrónico no puede ser cambiado, mientras que el envío de correos electrónicos está deshabilitado.",
+ "admin.security.requireEmailVerification.disabled": "Verificación de correo electrónico no puede ser cambiado, mientras que el envío de correos electrónicos está inhabilitado.",
"admin.security.session": "Sesiones",
"admin.security.signup": "Registro",
"admin.select_team.close": "Cerrar",
@@ -669,7 +682,7 @@
"admin.service.attemptTitle": "Máximo de intentos de conexión:",
"admin.service.cmdsDesc": "Cuando es verdadero, los comandos de barra serán permitidos. Revisa la <a href='http://docs.mattermost.com/developer/slash-commands.html' target='_blank'>documentación</a> para obtener más información.",
"admin.service.cmdsTitle": "Habilitar Comandos de Barra Personalizados: ",
- "admin.service.corsDescription": "Habilitar solicitudes HTTP de origen cruzado desde un dominio específico. Utiliza \"*\" si quieres permitir CORS desde cualquier dominio o dejalo en blanco para deshabilitarlo.",
+ "admin.service.corsDescription": "Habilitar solicitudes HTTP de origen cruzado desde un dominio específico. Utiliza \"*\" si quieres permitir CORS desde cualquier dominio o déjalo en blanco para inhabilitarlo.",
"admin.service.corsEx": "http://ejemplo.com",
"admin.service.corsTitle": "Habilitar la procedencia de las solicitudes cruzadas de:",
"admin.service.developerDesc": "Cuando es verdadero, los errores de JavaScript se muestran en una barra roja en la parte superior de la interfaz de usuario. No se recomienda su uso en producción. ",
@@ -831,10 +844,10 @@
"admin.team.dirDesc": "Cuando es verdadero, Los equipos que esten configurados para mostrarse en el directorio de equipos se mostrarán en vez de crear un nuevo equipo.",
"admin.team.dirTitle": "Habilitar Directorio de Equipos: ",
"admin.team.maxChannelsDescription": "Máximo número total de canales por equipo, incluyendo canales activos y borrados.",
- "admin.team.maxChannelsExample": "Ej \"100\"",
+ "admin.team.maxChannelsExample": "Ej: \"100\"",
"admin.team.maxChannelsTitle": "Max Canales Por Equipo:",
"admin.team.maxNotificationsPerChannelDescription": "Cantidad máxima de usuarios en un canal para que las notificaciones de \"escribiendo un mensaje\", @all, @here y @channel no sean enviadas para mejorar el rendimiento.",
- "admin.team.maxNotificationsPerChannelExample": "Ej \"1000\"",
+ "admin.team.maxNotificationsPerChannelExample": "Ej: \"1000\"",
"admin.team.maxNotificationsPerChannelTitle": "Cantidad Max de Notificaciones por Canal:",
"admin.team.maxUsersDescription": "Número máximo de usuarios por equipo, incluyendo usuarios activos e inactivos.",
"admin.team.maxUsersExample": "Ej.: \"25\"",
@@ -860,7 +873,7 @@
"admin.team.uploadDesc": "Personalizar la experiencia de usuario mediante la adición de una imagen personalizada en la pantalla de inicio de sesión. Se Recomienda que el tamaño máximo de la imagen sea menos de 2 MB.",
"admin.team.uploaded": "Subida!",
"admin.team.uploading": "Subiendo..",
- "admin.team.userCreationDescription": "Cuando es falso, a posibilidad de crear cuentas es deshabilitada. El botón crear cuentas arrojará error cuando sea presionado.",
+ "admin.team.userCreationDescription": "Cuando es falso, a posibilidad de crear cuentas es inhabilitada. El botón crear cuentas arrojará error cuando sea presionado.",
"admin.team.userCreationTitle": "Permitir la Creación de Cuentas: ",
"admin.team_analytics.activeUsers": "Active Users With Posts",
"admin.team_analytics.totalPosts": "Total de Mensajes",
@@ -923,12 +936,14 @@
"analytics.chart.meaningful": "No hay suficiente data para tener una representación significativa.",
"analytics.system.activeUsers": "Usuarios Activos con Mensajes",
"analytics.system.channelTypes": "Tipos de Canales",
+ "analytics.system.dailyActiveUsers": "Usuarios Activos Diarios",
"analytics.system.expiredBanner": "La licencia Empresarial vence el {date}. Tienes 15 días desde esta notificación para renovar la licencia, por favor contacta a <a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>.",
"analytics.system.expiringBanner": "La licencia Empresarial vence el {date}. Para renovar tu licencia, por favor contacta a <a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>.",
+ "analytics.system.monthlyActiveUsers": "Usuarios Activos Mensuales",
"analytics.system.postTypes": "Mesajes, Archivos y Hashtags",
"analytics.system.privateGroups": "Grupos Privados",
"analytics.system.publicChannels": "Canales Públicos",
- "analytics.system.skippedIntensiveQueries": "Para maximizar el rendimiento, algunas estadísticas están deshabilitadas. Puedes volver a activarlas en el archivo config.json. Ver: <a href='https://docs.mattermost.com/administration/statistics.html' target='_blank'>https://docs.mattermost.com/administration/statistics.html</a>",
+ "analytics.system.skippedIntensiveQueries": "Para maximizar el rendimiento, algunas estadísticas están inhabilitadas. Puedes volver a activarlas en el archivo config.json. Ver: <a href='https://docs.mattermost.com/administration/statistics.html' target='_blank'>https://docs.mattermost.com/administration/statistics.html</a>",
"analytics.system.textPosts": "Mensajes de sólo Texto",
"analytics.system.title": "Estadísticas del Sistema",
"analytics.system.totalChannels": "Total de Canales",
@@ -1022,7 +1037,6 @@
"backstage_sidebar.integrations.outgoing_webhooks": "Webhooks de Salida",
"calling_screen": "Llamando",
"center_panel.recent": "Clic aquí para ir a los mensajes más recientes. ",
- "chanel_header.addMembers": "Agregar Miembros",
"change_url.close": "Cerrar",
"change_url.endWithLetter": "Debe terminar con una letra o número",
"change_url.invalidUrl": "URL Inválida",
@@ -1040,6 +1054,7 @@
"channel_flow.handleTooShort": "Dirección URL del canal debe ser de 2 o más caracteres alfanuméricos en minúsculas",
"channel_flow.invalidName": "Nombre de Canal Inválido",
"channel_flow.set_url_title": "Asignar URL de {term}",
+ "channel_header.addMembers": "Agregar Miembros",
"channel_header.addToFavorites": "Añadir a favoritos",
"channel_header.channel": "Canal",
"channel_header.channelHeader": "Editar encabezado del canal",
@@ -1079,10 +1094,14 @@
"channel_loader.uploadedFile": " subió un archivo",
"channel_loader.uploadedImage": " subió una imagen",
"channel_loader.wrote": " escribió: ",
+ "channel_members_dropdown.channel_admin": "Admin del Canal",
+ "channel_members_dropdown.channel_member": "Miembro del Canal",
+ "channel_members_dropdown.make_channel_admin": "Hacer Admin de Canal",
+ "channel_members_dropdown.make_channel_member": "Hacer Miembro del Canal",
+ "channel_members_dropdown.remove_from_channel": "Remover del Canal",
+ "channel_members_dropdown.remove_member": "Remover Miembro",
"channel_members_modal.addNew": " Agregar nuevos Miembros",
- "channel_members_modal.close": "Cerrar",
- "channel_members_modal.remove": "Eliminar",
- "channel_memebers_modal.members": " Miembros",
+ "channel_members_modal.members": " Miembros",
"channel_modal.cancel": "Cancelar",
"channel_modal.channel": "Canal",
"channel_modal.createNew": "Crear Nuevo ",
@@ -1091,6 +1110,7 @@
"channel_modal.edit": "Editar",
"channel_modal.group": "Grupo",
"channel_modal.header": "Encabezado",
+ "channel_modal.headerEx": "Ej: \"[Título del enlace](http://example.com)\"",
"channel_modal.headerHelp": "Establece el texto que aparecerá en el encabezado del {term} al lado del nombre del {term}. Por ejemplo, incluye enlaces que se usan con frecuencia escribiendo [Título del Enlace](http://example.com).",
"channel_modal.modalTitle": "Nuevo ",
"channel_modal.name": "Nombre",
@@ -1101,6 +1121,7 @@
"channel_modal.publicChannel1": "Crear un canal público",
"channel_modal.publicChannel2": "Crear un canal público al que cualquiera puede unirse. ",
"channel_modal.purpose": "Propósito",
+ "channel_modal.purposeEx": "Ej: \"Un canal para describir errores y mejoras\"",
"channel_notifications.allActivity": "Para toda actividad",
"channel_notifications.allUnread": "Para todos los mensajes sin leer",
"channel_notifications.globalDefault": "Predeterminada ({notifyLevel})",
@@ -1113,6 +1134,7 @@
"channel_notifications.unreadInfo": "El nombre del canal está en negritas en la barra lateral cuando hay mensajes sin leer. Al elegir \"Sólo para menciones\" sólo lo dejará en negritas cuando seas mencionado.",
"channel_select.placeholder": "--- Selecciona un canal ---",
"channel_switch_modal.dm": "(Mensaje Directo)",
+ "channel_switch_modal.failed_to_open": "No se pudo abrir el canal.",
"channel_switch_modal.help": "Escribe el nombre del canal. Utiliza ↑↓ para navegar, TAB para seleccionar, ↵ para confirmar, ESC para descartar",
"channel_switch_modal.not_found": "No se encontró ninguna coincidencia.",
"channel_switch_modal.submit": "Cambiar",
@@ -1285,15 +1307,14 @@
"find_team.submitError": "Por favor ingresa una dirección válida",
"flag_post.flag": "Indicador de seguimiento",
"flag_post.unflag": "Desmarcar",
+ "general_tab.chooseDescription": "Por favor escoge una nueva descripción para tu equipo",
"general_tab.chooseName": "Por favor escoge otro nombre para tu equipo",
"general_tab.codeDesc": "Haz clic en 'Editar' para regenerar el código de Invitación.",
- "general_tab.codeLongDesc": "El Código de Invitación es utilizado como parte del URL del enlace creado por la opción <strong>Obtener Enlace de invitación</strong> en el menú principal. Regenerar este código crea un nuevo enlace e invalida los enlaces anteriores.",
+ "general_tab.codeLongDesc": "El Código de invitación se utiliza como parte de la URL en el enlace de invitación al equipo creado por {getTeamInviteLink} en el menú principal. La regeneración se crea un nuevo enlace de invitación al equipo e invalida el enlace anterior.",
"general_tab.codeTitle": "Código de Invitación",
- "general_tab.dirDisabled": "El directorio de Equipos ha sido deshabilitado. Por favor solicita a un Administrador de Sistema que habilite la opción de Directorio de Equipos en la Consola del Sistema.",
- "general_tab.dirOff": "El directorio de Equipos ha sido deshabilitado para este sistema.",
"general_tab.emptyDescription": "Clic en 'Editar' para añadir una descripción del equipo.",
+ "general_tab.getTeamInviteLink": "Enlace invitación al equipo",
"general_tab.includeDirDesc": "Incluir este equipo mostrará el nombre del equipo en la sección de Directorio de Equipos en la página de inicio, y proveerá un enlace para la página de inicio de sesión.",
- "general_tab.includeDirTitle": "Incluir este Equipo en el Directorio de Equipos",
"general_tab.no": "No",
"general_tab.openInviteDesc": "Cuando está permitido, un enlace para este equipo será incluido en la página de inicio permitiendo a cualquier persona con una cuenta unirse a este equipo.",
"general_tab.openInviteTitle": "Permitir que se una al equipo cualquier usuario con una cuenta en este servidor",
@@ -1340,7 +1361,7 @@
"help.commands.builtin": "## Comandos integrados\nLos siguientes comandos de barra están disponibles en todas las instalaciones de Mattermost:",
"help.commands.builtin2": "Empezieza escribiendo `/ ` y una lista de opciones de comandos de barra aparecerá encima del campo de texto. Las sugerencias de autocompletado ayudan al proveer un ejemplo formateado en texto de color negro y una breve descripción del comando de barra en texto de color gris.",
"help.commands.custom": "## Comandos Personalizados\nComandos de barra peronalizados se integran con aplicaciones externas. Por ejemplo, un equipo puede configurar comandos de barra personalizados para comprobar registros internos sobre la salud del `/paciente joe smith` o compruebe semanalmente el clima en una ciudad usando `/clima de toronto en la semana`. Consulte con su Administrador del Sistema o abra la lista de autocompletado escribiendo `/` para determinar si su equipo tiene configurado algunos comandos de barra personalizados.",
- "help.commands.custom2": "Los comandos de barra Personalizados están deshabilitados de forma predeterminada y pueden ser activados por el Administrador del Sistema en la **Consola del Sistema** > **Integraciones** > **Integraciones Personalizadas**. Aprende más acerca de la configuración personalizada de comandos de barra en la [documentación para desarrollar comandos de barra](http://docs.mattermost.com/developer/slash-commands.html).",
+ "help.commands.custom2": "Los comandos de barra Personalizados están inhabilitados de forma predeterminada y pueden ser activados por el Administrador del Sistema en la **Consola del Sistema** > **Integraciones** > **Integraciones Personalizadas**. Aprende más acerca de la configuración personalizada de comandos de barra en la [documentación para desarrollar comandos de barra](http://docs.mattermost.com/developer/slash-commands.html).",
"help.commands.intro": "Los comandos de barra realizan operaciones en Mattermost escribiendo en el campo de texto. Introduzca un `/` seguido por un comando y algunos argumentos para realizar acciones.\n\nTodas las instalaciones de Mattermost vienen con comandos de barra integrados y los comandos de barra personalizados se pueden configurar para interactuar con aplicaciones externas. Aprende más acerca de la configuración personalizada de comandos de barra en la [documentación para desarrollar comandos de barra](http://docs.mattermost.com/developer/slash-commands.html).",
"help.commands.title": "# Ejecutar Comandos\n___",
"help.composing.deleting": "## Eliminar un mensaje\nElimina un mensaje haciendo clic en el icono **[...]** junto a cualquier mensaje de texto que has compuesto, a continuación, haz clic en **Borrar**. Los Administradores de Sistema y Equipo pueden borrar cualquier mensaje en su sistema o equipo.",
@@ -1550,6 +1571,7 @@
"login.passwordChanged": " La contraseña ha sido actualizada",
"login.session_expired": " Tu sesión ha expirado. Por favor inicia sesión nuevamente.",
"login.signIn": "Entrar",
+ "login.signInLoading": "Iniciando sesión…",
"login.signInWith": "Iniciar sesión con:",
"login.userNotFound": "No pudimos encontrar una cuenta que coincida con tus credenciales.",
"login.username": "Nombre de usuario",
@@ -1561,8 +1583,10 @@
"member_item.makeAdmin": "Convertir en Admin de Equipo",
"member_item.member": "Miembro",
"member_list.noUsersAdd": "No hay usuarios que agregar.",
+ "members_popover.manageMembers": "Administrar Miembros",
"members_popover.msg": "Mensaje",
"members_popover.title": "Miembros",
+ "members_popover.viewMembers": "Ver Miembros",
"mfa.confirm.complete": "<strong>¡Configuración completada!</strong>",
"mfa.confirm.okay": "Aceptar",
"mfa.confirm.secure": "Tu cuenta ahora está segura. La próxima vez que inicies sesión, se te solicitará que ingreses el código de la app de Google Authenticator de tu teléfono.",
@@ -1665,6 +1689,7 @@
"post_info.mobile.unflag": "Desmarcar",
"post_info.permalink": "Enlace permanente",
"post_info.reply": "Responder",
+ "post_message_view.edited": "(editado)",
"posts_view.loadMore": "Cargar más mensajes",
"posts_view.newMsg": "Nuevos Mensajes",
"posts_view.newMsgBelow": "{count, plural, one {Nuevo mensaje} other {Nuevos mensajes}} ▼",
@@ -1777,9 +1802,9 @@
"signup.office365": "Office 365",
"signup.title": "Crea una cuenta con:",
"signup_team.createTeam": "O Crea un Equipo",
- "signup_team.disabled": "La creación de Equipos ha sido deshabilitada.",
+ "signup_team.disabled": "La creación de Equipos ha sido inhabilitada.",
"signup_team.join_open": "Equipos a los que te puedes unir: ",
- "signup_team.noTeams": "No hay equipos en el Directorio de Equipos y la creación de equipos ha sido deshabilitada.",
+ "signup_team.noTeams": "No hay equipos en el Directorio de Equipos y la creación de equipos ha sido inhabilitada.",
"signup_team.no_open_teams": "No hay equipos están disponibles para unirse. Por favor, solicita una invitación a tu administrador.",
"signup_team.no_open_teams_canCreate": "No hay equipos están disponibles para unirse. Por favor, crear un nuevo equipo, o solicita una invitación a tu administrador.",
"signup_team.no_teams": "No se han creado equipos. Por favor contacta a tu administrador.",
@@ -1899,7 +1924,7 @@
"update_command.question": "Tus cambios puede que rompan el comando de barra existente. ¿Estás seguro de que deseas actualizar?",
"update_command.update": "Actualizar",
"upload_overlay.info": "Arrastra un archivo para subirlo.",
- "user.settings.advance.embed_preview": "Mostrar vistas previas experimentales del contenido de los enlaces, cuando esté disponible",
+ "user.settings.advance.embed_preview": "Para lel primer enlace web en un mensaje, se mostrará una vista previa del contenido del sitio web a continuación del mensaje, si está disponible",
"user.settings.advance.embed_toggle": "Capacidad de Mostrar/Esconder las previsualizaciones",
"user.settings.advance.enabledFeatures": "{count, number} {count, plural, one {Característica} other {Caracteristicas}} Habilitadas",
"user.settings.advance.formattingDesc": "Si está activada, se dará formato a los mensajes, creando enlaces, mostrando emoticones, el estilo del texto, y añadir saltos de línea. De forma predeterminada, esta opción está habilitada. El cambio de esta configuración requiere que la página se actualice.",
@@ -1944,10 +1969,10 @@
"user.settings.display.channelDisplayTitle": "Modo en que se muestra el Canal",
"user.settings.display.channeldisplaymode": "Selecciona el ancho de la vista central.",
"user.settings.display.clockDisplay": "Visualización del Reloj",
- "user.settings.display.collapseDesc": "Ampliar los vínculos para mostrar una vista previa del contenido, cuando esté disponible.",
- "user.settings.display.collapseDisplay": "Vista previa del enlace",
- "user.settings.display.collapseOff": "Apagado",
- "user.settings.display.collapseOn": "Encendido",
+ "user.settings.display.collapseDesc": "Establecer si las vistas previas de los enlaces con imagen se deben mostrar como expandidos o contraídos de forma predeterminada. Este ajuste también puede ser controlado mediante el uso del los comandos de barra /expand y /collapse.",
+ "user.settings.display.collapseDisplay": "Apariencia predeterminada de lasa vistas previas de los enlaces con imagenes",
+ "user.settings.display.collapseOff": "Colapsado",
+ "user.settings.display.collapseOn": "Expandido",
"user.settings.display.fixedWidthCentered": "De ancho fijo, centrado",
"user.settings.display.fontDesc": "Selecciona la fuente con la que quieres ver la interfaz de Mattermost.",
"user.settings.display.fontTitle": "Fuente de Visualización",
@@ -2107,8 +2132,8 @@
"user.settings.push_notification.allActivityOffline": "Para toda la actividad cuando esté desconectado",
"user.settings.push_notification.allActivityOnline": "De toda la actividad cuando esté en línea, ausente o desconectado",
"user.settings.push_notification.away": "Ausente o desconectado",
- "user.settings.push_notification.disabled": "Deshabilitado por el Administrador de Sistema",
- "user.settings.push_notification.disabled_long": "Las Notificaciones Push para dispositivos móviles han sido deshabilitadas por el Administrador del Sistema.",
+ "user.settings.push_notification.disabled": "Inhabilitado por el Administrador de Sistema",
+ "user.settings.push_notification.disabled_long": "Las Notificaciones Push para dispositivos móviles han sido inhabilitadas por el Administrador del Sistema.",
"user.settings.push_notification.info": "Se enviarán notificaciones a tu dispositivo móvil cuando haya actividad en Mattermost.",
"user.settings.push_notification.off": "Apagado",
"user.settings.push_notification.offline": "Desconectado",
@@ -2130,7 +2155,9 @@
"user.settings.security.lastUpdated": "Última actualización {date} a las {time}",
"user.settings.security.ldap": "AD/LDAP",
"user.settings.security.loginGitlab": "Inicio de sesión realizado a través de GitLab",
+ "user.settings.security.loginGoogle": "Inicio de sesión realizado a través de Google Apps",
"user.settings.security.loginLdap": "Inicio de sesión realizado a través de AD/LDAP",
+ "user.settings.security.loginOffice365": "Inicio de sesión realizado a través de Office 365",
"user.settings.security.loginSaml": "Inicio de sesión realizado a través de SAML",
"user.settings.security.logoutActiveSessions": "Visualizar y cerrar las sesiones activas",
"user.settings.security.method": "Método de inicio de sesión",
@@ -2159,9 +2186,11 @@
"user.settings.security.passwordErrorUppercaseNumberSymbol": "La contraseña debe contener al menos {min} caracteres compuesta de al menos una letra mayúscula, al menos un número, y al menos un símbolo (por ejemplo,\"~!@#$%^&*()\").",
"user.settings.security.passwordErrorUppercaseSymbol": "La contraseña debe contener al menos {min} caracteres compuesta de al menos una letra mayúscula y al menos un símbolo (por ejemplo,\"~!@#$%^&*()\").",
"user.settings.security.passwordGitlabCantUpdate": "El inicio de sesión ocurre a través GitLab. La contraseña no se puede actualizar.",
+ "user.settings.security.passwordGoogleCantUpdate": "El inicio de sesión ocurre a través Google Apps. La contraseña no se puede actualizar.",
"user.settings.security.passwordLdapCantUpdate": "El inicio de sesión ocurre a través AD/LDAP. La contraseña no se puede actualizar.",
"user.settings.security.passwordMatchError": "La nuevas contraseñas que ingresaste no coinciden.",
"user.settings.security.passwordMinLength": "Longitud mínima no válida, no se puede mostrar la vista previa.",
+ "user.settings.security.passwordOffice365CantUpdate": "El inicio de sesión ocurre a través Office 365. La contraseña no se puede actualizar.",
"user.settings.security.passwordSamlCantUpdate": "La contraseña es manejada a través del proveedor de inicio de sesión. Si deseas cambiarla, deberás hacerlo a través del proveedor de identidad.",
"user.settings.security.retypePassword": "Reescribe la Nueva Contraseña",
"user.settings.security.saml": "SAML",
diff --git a/webapp/i18n/fr.json b/webapp/i18n/fr.json
index e1f6e6afe..b464930b9 100644
--- a/webapp/i18n/fr.json
+++ b/webapp/i18n/fr.json
@@ -3,7 +3,7 @@
"about.copyright": "Copyright 2016 Mattermost, Inc. Tous droits réservés",
"about.database": "Base de données :",
"about.date": "Date de compilation :",
- "about.enterpriseEditionLearn": "Apprenez-en plus sur l’Édition Entreprise à : ",
+ "about.enterpriseEditionLearn": "Pour en savoir davantage sur l’Édition Entreprise : ",
"about.enterpriseEditionSt": "Communication moderne derrière votre pare-feu.",
"about.enterpriseEditione1": "Édition Entreprise",
"about.hash": "Clé de compilation :",
@@ -28,6 +28,7 @@
"activity_log.sessionsDescription": "Les sessions sont créées lorsque vous vous connectez depuis un nouveau navigateur. Les sessions vous permettent d'utiliser Mattermost sans devoir vous connecter à nouveau durant un temps défini par l'administrateur système. Si vous souhaitez vous déconnecter plus tôt, utilisez le bouton \"Se déconnecter\" ci-dessous pour mettre fin à votre session.",
"activity_log_modal.android": "Android",
"activity_log_modal.androidNativeApp": "Application Android",
+ "activity_log_modal.desktop": "Native Desktop App",
"activity_log_modal.iphoneNativeApp": "Application pour iPhone",
"add_command.autocomplete": "Auto-complétion",
"add_command.autocomplete.help": "(Facultatif) Afficher les commandes slash dans la liste d'auto-complétion.",
@@ -58,8 +59,8 @@
"add_command.trigger.helpReserved": "Réservé : {link}",
"add_command.trigger.helpReservedLinkText": "voir la liste des commandes slash incluses",
"add_command.trigger.placeholder": "Mot-clé de déclenchement par exemple \"hello\"",
- "add_command.triggerInvalidLength": "un mot-clé doit contenir entre {min} à {max} caractères",
- "add_command.triggerInvalidSlash": "Un mot-clé ne peut commencer par un /",
+ "add_command.triggerInvalidLength": "Un mot-clé déclencheur doit contenir de {min} à {max} caractères",
+ "add_command.triggerInvalidSlash": "Un mot-clé déclencheur ne peut commencer par un /",
"add_command.triggerInvalidSpace": "Un mot-clé ne doit pas contenir d’espaces",
"add_command.triggerRequired": "Un mot-clé est requis",
"add_command.url": "URL de requête",
@@ -156,7 +157,7 @@
"admin.cluster.interNodeUrlsTitle": "URLs de communication entre noeuds :",
"admin.cluster.loadedFrom": "Le fichier de configuration a été chargé par le noeud ID {clusterId}. Veuillez vous référer au guide de résolution des problèmes de notre <a href=\"http://docs.mattermost.com/deployment/cluster.html\" target=\"_blank\">documentation</a> si vous accédez à la Console Système à partir d'un répartiteur de charge et que vous rencontrez des problèmes.",
"admin.cluster.noteDescription": "Changer les propriétés de cette section requiert un redémarrage du serveur avant de prendre effet. Lorsque le mode Haute Disponibilité est activé, la Console Système est mise en lecture-seule et ne peut être changée que par le fichier de configuration.",
- "admin.cluster.should_not_change": "ATTENTION : Ces paramètres ne se synchroniseront pas avec les autres serveurs du cluster. La communication entre nœuds en mode Haute Disponibilité ne démarrera pas tant que le fichier config.json ne sera pas identique sur chacun des serveurs et que Mattermost n'aura pas été redémarré. Veuillez vous référer à la <a href=\"http://docs.mattermost.com/deployment/cluster.html\" target=\"_blank\">documentation</a> pour savoir comment ajouter ou supprimer un serveur d'un cluster. Si vous accédez à la Console Système à partir d'un répartiteur de charges et que vous rencontrez des problèmes, veuillez vous référer au guide de résolution des problèmes de notre <a href=\"http://docs.mattermost.com/deployment/cluster.html\" target=\"_blank\">documentation</a>.",
+ "admin.cluster.should_not_change": "ATTENTION : Ces paramètres ne se synchroniseront pas avec les autres serveurs du cluster. La communication entre nœuds en mode Haute Disponibilité ne démarrera pas tant que le fichier config.json ne sera pas identique sur chacun des serveurs et que Mattermost n'aura pas été redémarré. Veuillez vous référer à la <a href=\"http://docs.mattermost.com/deployment/cluster.html\" target=\"_blank\">documentation</a> pour savoir comment ajouter ou supprimer un serveur d'un cluster. Si vous accédez à la Console système à partir d'un répartiteur de charges et que vous rencontrez des problèmes, veuillez vous référer au guide de résolution des problèmes de notre <a href=\"http://docs.mattermost.com/deployment/cluster.html\" target=\"_blank\">documentation</a>.",
"admin.cluster.status_table.config_hash": "MD5 du fichier de configuration",
"admin.cluster.status_table.hostname": "Nom d'hôte",
"admin.cluster.status_table.id": "ID du nœud",
@@ -172,14 +173,14 @@
"admin.compliance.enableDesc": "Si activé, Mattermost autorise les rapports de conformité depuis l'onglet <strong>Conformité et vérification</strong>. Consultez la <a href=\"https://docs.mattermost.com/administration/compliance.html\" target=\"_blank\">documentation</a> pour en apprendre davantage.",
"admin.compliance.enableTitle": "Activer le rapport de conformité :",
"admin.compliance.false": "non",
- "admin.compliance.noLicense": "<h4 class=\"banner__heading\">Note :</h4><p>La conformité est une option disponible sur l'édition Entreprise. Votre licence ne permet pas d'utiliser cette fonction. Cliquez <a href=\"http://mattermost.com\" target=\"_blank\">ici</a> pour des informations sur la licence Entreprise et connaître son prix.</p>",
+ "admin.compliance.noLicense": "<h4 class=\"banner__heading\">Note :</h4><p>La conformité est une option disponible sur l'édition Entreprise. Votre licence ne permet pas d'utiliser cette fonction. Veuillez cliquer <a href=\"http://mattermost.com\" target=\"_blank\">ici</a> pour des informations sur la licence Entreprise et connaître son prix.</p>",
"admin.compliance.save": "Enregistrer",
"admin.compliance.saving": "Enregistrement des paramètres...",
"admin.compliance.title": "Configuration de la conformité",
"admin.compliance.true": "vrai",
"admin.compliance_reports.desc": "Profession :",
"admin.compliance_reports.desc_placeholder": "Ex : \"Audit 445 pour les RH\"",
- "admin.compliance_reports.emails": "Adresses électroniques :",
+ "admin.compliance_reports.emails": "Adresses e-mail :",
"admin.compliance_reports.emails_placeholder": "Ex : \"bill@example.com, bob@example.com »",
"admin.compliance_reports.from": "De :",
"admin.compliance_reports.from_placeholder": "Ex : \"2016-03-18\"",
@@ -228,14 +229,14 @@
"admin.database.title": "Paramètres de base de données",
"admin.developer.title": "Paramètres de développement",
"admin.email.agreeHPNS": " J’ai compris et j’accepte les <a href=\"https://about.mattermost.com/hpns-terms/\" target=\"_blank\">termes de service</a> et la <a href=\"https://about.mattermost.com/hpns-privacy/\" target=\"_blank\">politique de protection de la vie privée</a> du service de notifications Push hébergé par Mattermost.",
- "admin.email.allowEmailSignInDescription": "Si activé, les utilisateurs pourront se connecter avec leur adresse électronique et mot de passe.",
- "admin.email.allowEmailSignInTitle": "Activer la connexion avec adresse électronique : ",
- "admin.email.allowSignupDescription": "Si activé, la création d'équipes et de comptes en utilisant une adresse électronique et un mot de passe sera autorisée. Cette valeur devrait être fausse seulement si vous souhaitez limiter les connexions via un service SSO comme OAuth ou LDAP.",
- "admin.email.allowSignupTitle": "Activer la création de compte avec adresse électronique : ",
- "admin.email.allowUsernameSignInDescription": "Si activé, les utilisateurs seront autorisés à se connecter avec leur nom d'utilisateur et leur mot de passe. Cette option n'est généralement utilisée que lorsque la vérification de l'adresse électronique est désactivée.",
+ "admin.email.allowEmailSignInDescription": "Si activé, les utilisateurs pourront se connecter avec leur adresse e-mail et leur mot de passe.",
+ "admin.email.allowEmailSignInTitle": "Activer la connexion avec une adresse e-mail : ",
+ "admin.email.allowSignupDescription": "Si activé, la création d'équipes et de comptes en utilisant une adresse e-mail et un mot de passe sera autorisée. Cette valeur devrait être fausse seulement si vous souhaitez limiter les connexions via un service SSO comme OAuth ou LDAP.",
+ "admin.email.allowSignupTitle": "Activer la création de comptes avec une adresse e-mail : ",
+ "admin.email.allowUsernameSignInDescription": "Si activé, les utilisateurs seront autorisés à se connecter avec leur nom d'utilisateur et leur mot de passe. Cette option n'est généralement utilisée que lorsque la vérification de l'adresse e-mail est désactivée.",
"admin.email.allowUsernameSignInTitle": "Activer la connexion avec nom d'utilisateur : ",
"admin.email.connectionSecurityTest": "Tester la connexion",
- "admin.email.easHelp": "En savoir plus sur la compilation et le déploiement de vos propres applications mobiles à partir de l'<a href=\"http://docs.mattermost.com/deployment/push.html#enterprise-app-store-eas\" target=\"_blank\">Enterprise App Store</a>.",
+ "admin.email.easHelp": "En savoir plus sur la compilation et le déploiement de vos propres applications mobiles à partir de l'<a href=\"http://docs.mattermost.com/deployment/push.html#enterprise-app-store-eas\" target=\"_blank\">App Store Entreprise</a>.",
"admin.email.emailFail": "Echec de la connexion : {error}",
"admin.email.emailSuccess": "Aucune erreur signalée lors de l'envoi de l'e-mail. Vérifiez votre boîte de réception pour vous en assurer.",
"admin.email.enableEmailBatching.clusterEnabled": "L'envoi d'e-mails par lot ne peut pas être activé lorsque le mode Haute Disponibilité est activé.",
@@ -244,14 +245,14 @@
"admin.email.enableEmailBatchingTitle": "Activer l'envoi d'e-mails par lot :",
"admin.email.fullPushNotification": "Envoyer un extrait du message complet",
"admin.email.genericPushNotification": "Envoyer une description générale avec les noms des utilisateurs et des canaux",
- "admin.email.inviteSaltDescription": "Clé de salage de 32 caractères ajouté aux e-mails d'invitation. Générée aléatoirement lors de l'installation. Cliquez sur \"Regénérer\" pour créer une nouvelle clé de salage.",
+ "admin.email.inviteSaltDescription": "Clé de salage de 32 caractères ajouté aux e-mails d'invitation. Générée aléatoirement lors de l'installation. Veuillez cliquer sur \"Regénérer\" pour créer une nouvelle clé de salage.",
"admin.email.inviteSaltExample": "Ex. : \"bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo\"",
"admin.email.inviteSaltTitle": "Clé de salage des e-mails d'invitation :",
"admin.email.mhpns": "La connexion à iOS et aux applications Android est cryptée",
"admin.email.mhpnsHelp": "Téléchargez l'<a href=\"https://itunes.apple.com/us/app/mattermost/id984966508?mt=8\" target=\"_blank\">application iOS Mattermost</a> depuis iTunes. Téléchargez l'<a href=\"https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en\" target=\"_blank\">application Android Mattermost</a> depuis le Google Play. Apprenez-en plus sur <a href=\"http://docs.mattermost.com/deployment/push.html#hosted-push-notifications-service-hpns\" target=\"_blank\">HPNS</a>.",
"admin.email.mtpns": "Utilisez iOS et Android sur iTunes et Google Play avec TPNS",
"admin.email.mtpnsHelp": "Téléchargez l'<a href=\"https://itunes.apple.com/us/app/mattermost/id984966508?mt=8\" target=\"_blank\">application iOS Mattermost</a> depuis iTunes. Téléchargez l'<a href=\"https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en\" target=\"_blank\">application Android Mattermost</a> depuis le Google Play. Apprenez-en plus sur <a href=\"http://docs.mattermost.com/deployment/push.html#test-push-notifications-service-tpns\" target='_blank'>TPNS</a>.",
- "admin.email.nofificationOrganizationExample": "Ex. \"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\"",
+ "admin.email.nofificationOrganizationExample": "Exemple : \"(c)MonEntreprise, 12 avenue Niel, 75017 Paris, France\"",
"admin.email.notificationDisplayDescription": "Afficher le nom du compte de messagerie utilisé lors de l'envoi de notifications par Mattermost.",
"admin.email.notificationDisplayExample": "Ex. : \"Notification Mattermost\", \"Système\", \"No-reply\"",
"admin.email.notificationDisplayTitle": "Nom affiché dans les notifications :",
@@ -259,10 +260,11 @@
"admin.email.notificationEmailExample": "Ex. : \"mattermost@yourcompany.com\", \"admin@yourcompany.com\"",
"admin.email.notificationEmailTitle": "Notification depuis l'adresse :",
"admin.email.notificationOrganization": "Adresse e-mail du pied de page pour les notifications e-mail :",
- "admin.email.notificationOrganizationDescription": "Nom de société et adresse affichés sur les notifications par courrier électronique, comme \"(c) MaSociété, 21 avenue Niel, 75017 Paris, France\". Si ce champ est vide, il ne sera pas inclus dans les courriers électroniques.",
+ "admin.email.notificationOrganizationDescription": "Nom de société et adresse affichés sur les notifications envoyées par e-mail, comme \"© MonEntreprise, 55 Rue du Faubourg Saint-Honoré, 75008 Paris, France\". Si ce champ est vide, il ne sera pas inclus dans les e-mails.",
+ "admin.email.notificationOrganizationExample": "Exemple : \"(c)MonEntreprise, 12 avenue Niel, 75017 Paris, France\"",
"admin.email.notificationsDescription": "En général, activé en production. Si activé, Mattermost essaye d'envoyer des notifications par courrier électronique. Les développeurs peuvent en revanche désactiver cette option pour gagner du temps.<br />Activer cette option retire la bannière \"Mode découverte\" (cela nécessite de se déconnecter puis se re-connecter après avoir activé l'option).",
"admin.email.notificationsTitle": "Activer les notifications par e-mail :",
- "admin.email.passwordSaltDescription": "Clé de salage de 32 caractères utilisé pour générer la clé de réinitialisation du mot de passe. Générée aléatoirement à l'installation. Cliquez sur \"re-générer\" pour créer une nouvelle clé de salage.",
+ "admin.email.passwordSaltDescription": "Clé de salage de 32 caractères utilisé pour générer la clé de réinitialisation du mot de passe. Générée aléatoirement à l'installation. Veuillez cliquer sur \"regénérer\" pour créer une nouvelle clé de salage.",
"admin.email.passwordSaltExample": "Ex. : \"bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo\"",
"admin.email.passwordSaltTitle": "Clé de salage de réinitialisation du mot de passe :",
"admin.email.pushContentDesc": "Choisir \"Envoyer une description générale avec les noms des utilisateurs et des canaux\" permet aux notifications push d'envoyer des messages sans détail, en incluant juste les noms d'utilisateurs et de canaux.<br /><br />Choisir \"Envoyer un extrait du message complet\" envoie des extraits des messages qui déclenchent les notifications, et peuvent inclure des informations confidentielles visibles sur le terminal des utilisateurs notifiés. Si votre serveur de notifications Push est en dehors de votre pare-feu, il est HAUTEMENT RECOMMANDÉ d'utiliser cette option uniquement avec le protocole \"https\".",
@@ -274,9 +276,9 @@
"admin.email.pushServerEx": "Exemple : \"http://push-test.mattermost.com\"",
"admin.email.pushServerTitle": "Serveur de notifications push :",
"admin.email.pushTitle": "Envoyer des notifications push : ",
- "admin.email.requireVerificationDescription": "En général activé en production. Si activé, Mattermost impose une vérification de l'adresse électronique avant d'autoriser la connexion. Vous pouvez désactiver cette option en développement.",
- "admin.email.requireVerificationTitle": "Imposer la vérification de l'adresse électronique : ",
- "admin.email.selfPush": "Configuration manuel du service de notification Push",
+ "admin.email.requireVerificationDescription": "En général activé en production. Si activé, Mattermost impose une vérification de l'adresse e-mail avant d'autoriser la connexion. Vous pouvez désactiver cette option en développement.",
+ "admin.email.requireVerificationTitle": "Imposer la vérification de l'adresse e-mail : ",
+ "admin.email.selfPush": "Spécifiez manuellement la configuration du service de notifications Push",
"admin.email.smtpPasswordDescription": " Récupérez ces informations de la part de l'administrateur de votre serveur de mails.",
"admin.email.smtpPasswordExample": "Ex. : \"yourpassword\", \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
"admin.email.smtpPasswordTitle": "Mot de passe du serveur SMTP :",
@@ -308,10 +310,20 @@
"admin.general.localization.serverLocaleTitle": "Langue par défaut du serveur :",
"admin.general.log": "Journalisation",
"admin.general.policy": "Règles",
+ "admin.general.policy.allowEditPostAlways": "Any time",
+ "admin.general.policy.allowEditPostDescription": "Set policy on the length of time authors have to edit their messages after posting.",
+ "admin.general.policy.allowEditPostNever": "Jamais",
+ "admin.general.policy.allowEditPostTimeLimit": "seconds after posting",
+ "admin.general.policy.allowEditPostTitle": "Allow users to edit their messages:",
"admin.general.policy.permissionsAdmin": "Administrateurs d'équipe et administrateurs système",
"admin.general.policy.permissionsAll": "Tous les membres de l'équipe",
"admin.general.policy.permissionsAllChannel": "Tous les membres du canal",
+ "admin.general.policy.permissionsDeletePostAdmin": "Administrateurs d'équipe et administrateurs système",
+ "admin.general.policy.permissionsDeletePostAll": "Message authors can delete their own messages, and Administrators can delete any message",
+ "admin.general.policy.permissionsDeletePostSystemAdmin": "Administrateurs système",
"admin.general.policy.permissionsSystemAdmin": "Administrateurs système",
+ "admin.general.policy.restrictPostDeleteDescription": "Set policy on who has permission to delete messages.",
+ "admin.general.policy.restrictPostDeleteTitle": "Allow which users to delete messages:",
"admin.general.policy.restrictPrivateChannelCreationDescription": "Choisit qui peut créer des groupes privés.",
"admin.general.policy.restrictPrivateChannelCreationTitle": "Activer la création de groupes privés pour :",
"admin.general.policy.restrictPrivateChannelDeletionCommandLineToolLink": "outil en ligne de commande",
@@ -326,7 +338,7 @@
"admin.general.policy.restrictPublicChannelDeletionTitle": "Activer la suppression de canaux publics pour :",
"admin.general.policy.restrictPublicChannelManagementDescription": "Choisit qui peut renommer et définir l'en-tête ou la description des canaux publics.",
"admin.general.policy.restrictPublicChannelManagementTitle": "Activer le renommage de canaux publics pour :",
- "admin.general.policy.teamInviteDescription": "Choisir qui peut inviter d'autres personnes à une équipe en utilisant <b>Inviter un nouveau membre</b> pour inviter de nouveaux utilisateurs par courrier électronique, ou l'option <b>Obtenir un lien d'invitation d'équipe</b> du menu principal. Si l'option <b>Obtenir un lien d'invitation d'équipe</b> est utilisée pour partager un lien, vous pouvez faire expirer le code d'invitation depuis <b>Configuration de l'équipe</b> > <b>Code d'invitation</b> une fois que les utilisateurs désirés ont rejoint l'équipe.",
+ "admin.general.policy.teamInviteDescription": "Choisir qui peut inviter d'autres personnes à une équipe en utilisant <b>Inviter un nouveau membre</b> pour inviter des nouveaux utilisateurs par e-mail, ou l'option <b>Obtenir un lien d'invitation d'équipe</b> du menu principal. Si l'option <b>Obtenir un lien d'invitation d'équipe</b> est utilisée pour partager un lien, vous pouvez faire expirer le code d'invitation depuis <b>Configuration de l'équipe</b> > <b>Code d'invitation</b> une fois que les utilisateurs désirés ont rejoint l'équipe.",
"admin.general.policy.teamInviteTitle": "Autoriser l'envoi d'invitation depuis :",
"admin.general.privacy": "Confidentialité",
"admin.general.usersAndTeams": "Utilisateur et équipes",
@@ -363,6 +375,7 @@
"admin.image.amazonS3BucketExample": "Ex. : \"mattermost-media\"",
"admin.image.amazonS3BucketTitle": "Bucket S3 Amazon :",
"admin.image.amazonS3EndpointDescription": "Nom d'hôte de votre fournisseur de stockage compatible S3. Par défaut : 's3.amazonaws.com'.",
+ "admin.image.amazonS3EndpointExample": "E.g.: \"s3.amazonaws.com\"",
"admin.image.amazonS3EndpointTitle": "Endpoint Amazon S3 :",
"admin.image.amazonS3IdDescription": "Demandez cette information à votre administrateur AWS.",
"admin.image.amazonS3IdExample": "Ex. : \"AKIADTOVBGERKLCBV\"",
@@ -393,7 +406,7 @@
"admin.image.profileWidthDescription": "Largeur de la photo de profil.",
"admin.image.profileWidthExample": "Ex. : \"1024\"",
"admin.image.profileWidthTitle": "Largeur de photo de profil :",
- "admin.image.publicLinkDescription": "Clé de salage de 32 caractères ajoutée aux e-mails d'invitation. Générée aléatoirement lors de l'installation. Cliquez sur \"Regénérer\" pour créer une nouvelle clé de salage.",
+ "admin.image.publicLinkDescription": "Clé de salage de 32 caractères ajoutée aux e-mails d'invitation. Générée aléatoirement lors de l'installation. Veuillez cliquer sur \"Regénérer\" pour créer une nouvelle clé de salage.",
"admin.image.publicLinkExample": "Ex. : \"gxHVDcKUyP2y1eiyW8S8na1UYQAfq6J6\"",
"admin.image.publicLinkTitle": "Clé de salage publique :",
"admin.image.shareDescription": "Permettre aux utilisateurs de partager des liens et images publics.",
@@ -418,9 +431,9 @@
"admin.ldap.bindPwdTitle": "Mot de passe de l'utilisateur de recherche :",
"admin.ldap.bindUserDesc": "Nom de l'utilisateur LDAP utilisé pour rechercher les autres utilisateurs. Il peut s'agir d'un utilisateur créé spécifiquement pour Mattermost, et disposant de droits restreints à l'arborescence LDAP concernée par Mattermoste.",
"admin.ldap.bindUserTitle": "Nom d'utilisateur pour la recherche :",
- "admin.ldap.emailAttrDesc": "Attribut du serveur LDAP utilisé pour le champ \"adresse électronique\" dans Mattermost.",
+ "admin.ldap.emailAttrDesc": "Attribut du serveur LDAP utilisé pour le champ \"adresse e-mail\" dans Mattermost.",
"admin.ldap.emailAttrEx": "Ex. : \"mail\" ou \"userPrincipalName\"",
- "admin.ldap.emailAttrTitle": "Attribut \"adresse électronique\" :",
+ "admin.ldap.emailAttrTitle": "Attribut \"adresse e-mail\" :",
"admin.ldap.enableDesc": "Si activé, Mattermost permet de s'authentifier avec le serveur LDAP",
"admin.ldap.enableTitle": "Activer la connexion avec LDAP :",
"admin.ldap.firstnameAttrDesc": "(Optionnel) L'attribut dans le serveur AD/LDAP qui sera utilisé pour les prénoms des utilisateurs de Mattermost. Lorsque défini, les utilisateurs ne pourront pas éditer leur prénom étant donné qu'il est alors synchronisé avec le serveur LDAP. Lorsque laissé vide, les utilisateurs peuvent définir leur propre prénom dans les paramètres du compte.",
@@ -442,7 +455,7 @@
"admin.ldap.nicknameAttrDesc": "(Optionnel) L'attribut dans le serveur AD/LDAP qui sera utilisé pour les surnoms des utilisateurs de Mattermost. Lorsque défini, les utilisateurs ne pourront pas éditer leur surnoms étant donné qu'il est alors synchronisé avec le serveur LDAP. Lorsque laissé vide, les utilisateurs peuvent définir leur propre surnom dans les paramètres du compte.",
"admin.ldap.nicknameAttrEx": "Ex. : \"surnom\"",
"admin.ldap.nicknameAttrTitle": "Attribut \"nom d'utilisateur\" :",
- "admin.ldap.noLicense": "<h4 class=\"banner__heading\">Note :</h4><p>AD/LDAP est une fonctionnalité de l'édition d’Enterprise. Votre licence actuelle ne permet pas d'utiliser AD/LDAP. Cliquez <a href=\"http://mattermost.com\" target=\"_blank\">ici</a> pour plus d'informations et connaître les tarifs de l'édition d’Enterprise.</p>",
+ "admin.ldap.noLicense": "<h4 class=\"banner__heading\">Note :</h4><p>AD/LDAP est une fonctionnalité de l'Edition Entreprise. Votre licence actuelle ne permet pas d'utiliser AD/LDAP. Veuillez cliquer <a href=\"http://mattermost.com\" target=\"_blank\">ici</a> pour plus d'informations et connaître les tarifs de l'Edition Entreprise.</p>",
"admin.ldap.portDesc": "Le port utilisé par Mattermost pour vous connecter au serveur LDAP. Par défaut : 389.",
"admin.ldap.portEx": "Ex. : \"389\"",
"admin.ldap.portTitle": "Port du serveur LDAP :",
@@ -458,7 +471,7 @@
"admin.ldap.skipCertificateVerification": "Passer la vérification",
"admin.ldap.skipCertificateVerificationDesc": "Saute l'étape de vérification des certificats pour les connexions TLS ou STARTTLS . Non recommandé pour les environnements de production où TLS est nécessaire. Pour tester seulement.",
"admin.ldap.syncFailure": "Erreur de synchronisation : {error}",
- "admin.ldap.syncIntervalHelpText": "La synchronisation LDAP met à jour les informations utilisateurs de Mattermost pour refléter les changements du serveur LDAP. Par exemple, lorsque le nom d'un utilisateur change sur le serveur LDAP, le changement est appliqué sur Mattermost lors de la synchronisation. Les comptes supprimés ou désactivés sur le serveur LDAP auront leur compte Mattermost mis comme \"Inactif\" et auront leur session de révoquée. Mattermost réalise la synchronisation dans l'intervalle entré. Par exemple, si 60 est entré, Mattermost se synchronisera toutes les 60 minutes.",
+ "admin.ldap.syncIntervalHelpText": "La synchronisation LDAP met à jour les informations utilisateurs de Mattermost pour refléter les changements du serveur LDAP. Par exemple, lorsque le nom d'un utilisateur change sur le serveur LDAP, le changement est appliqué sur Mattermost lors de la synchronisation. Les comptes supprimés ou désactivés sur le serveur LDAP auront leur compte Mattermost mis comme \"Inactif\" et auront leur session de révoquée. Mattermost réalise la synchronisation dans l'intervalle entré. Par exemple, si 60 est spécifié, Mattermost se synchronisera toutes les 60 minutes.",
"admin.ldap.syncIntervalTitle": "Intervalle de synchronisation (en minutes)",
"admin.ldap.syncNowHelpText": "Provoque une synchronisation LDAP immédiate.",
"admin.ldap.sync_button": "Synchroniser maintenant l'annuaire LDAP",
@@ -475,13 +488,13 @@
"admin.license.chooseFile": "Parcourir",
"admin.license.edition": "Edition: ",
"admin.license.key": "Clé de licence: ",
- "admin.license.keyRemove": "Supprimer la Licence Enterprise et rétrograder le serveur",
+ "admin.license.keyRemove": "Supprimer la licence Entreprise et rétrograder le serveur",
"admin.license.noFile": "Aucun fichier chargé",
"admin.license.removing": "Suppression de la licence...",
"admin.license.title": "Édition et licence",
"admin.license.type": "Licence : ",
"admin.license.upload": "Télécharger",
- "admin.license.uploadDesc": "Téléchargez une licence de Mattermost pour mettre à niveau ce serveur. <a href=\"http://mattermost.com\" target=\"_blank\">Consultez Mattermost</a> pour en apprendre plus au sujet des avantages de la version Entreprise ou pour savoir comment acheter une clé.",
+ "admin.license.uploadDesc": "Téléchargez une licence de Mattermost pour mettre à niveau ce serveur vers l'Edition Entreprise. <a href=\"http://mattermost.com\" target=\"_blank\">Consultez Mattermost</a> pour en apprendre plus au sujet des avantages de l'Edition Entreprise ou pour savoir comment acheter une clé.",
"admin.license.uploading": "Téléchargement de la licence…",
"admin.log.consoleDescription": "En principe désactivée en production. Utilisé pour le développement, pour afficher les messages d'information sur la console (stdout).",
"admin.log.consoleTitle": "Affiche les journaux sur la console : ",
@@ -513,7 +526,7 @@
"admin.metrics.enableDescription": "Lorsqu'activé, Mattermost activera la collecte des rapports de performance et de profilage. Veuillez vous référer à la <a href=\"http://docs.mattermost.com/deployment/metrics.html\" target='_blank'>documentation</a> pour en savoir plus sur la configuration du suivi des performances de Mattermost.",
"admin.metrics.enableTitle": "Activer le suivi des performances :",
"admin.metrics.listenAddressDesc": "L'adresse d'écoute du serveur pour publier les mesures de performances.",
- "admin.metrics.listenAddressEx": "Ex. : \":8067\"",
+ "admin.metrics.listenAddressEx": "Ex. : \":8065\"",
"admin.metrics.listenAddressTitle": "Adresse IP du serveur :",
"admin.mfa.bannerDesc": "L'authentification multi-facteurs n'est disponible que pour les comptes avec authentification LDAP ou email. Si vous avez des utilisateurs avec d'autres méthodes d'authentification, il est recommandé de paramétrer l'authentification multi-facteurs directement avec le fournisseur SSO ou SAML.",
"admin.mfa.cluster": "Haute",
@@ -522,7 +535,7 @@
"admin.nav.logout": "Se déconnecter",
"admin.nav.report": "Signaler un problème",
"admin.nav.switch": "Sélection de l'équipe",
- "admin.notifications.email": "Adresse électronique",
+ "admin.notifications.email": "Adresse e-mail",
"admin.notifications.push": "notifications Push sur mobile",
"admin.notifications.title": "Paramètres de notification",
"admin.oauth.gitlab": "GitLab",
@@ -552,8 +565,8 @@
"admin.password.requirementsDescription": "Lettres obligatoires pour un mot de passe valide.",
"admin.password.symbol": "Au moins un symbole (ex : \"~!@#$%^&*()\")",
"admin.password.uppercase": "Au moins une majuscule",
- "admin.privacy.showEmailDescription": "Si désactivé, masque les adresses électronique des utilisateurs dans l'interface, y compris pour les responsables d'équipe. Cette option est pratique quand Mattermost est utilisé pour gérer des équipes dans lesquelles les utilisateurs préfèrent garder leurs informations privées.",
- "admin.privacy.showEmailTitle": "Afficher l'adresse électronique : ",
+ "admin.privacy.showEmailDescription": "Si désactivé, masque les adresses e-mail des utilisateurs dans l'interface, y compris pour les responsables d'équipe. Cette option est pratique quand Mattermost est utilisé pour gérer des équipes dans lesquelles les utilisateurs préfèrent garder leurs informations privées.",
+ "admin.privacy.showEmailTitle": "Afficher l'adresse e-mail : ",
"admin.privacy.showFullNameDescription": "Si désactivé, les utilisateurs ne peuvent voir le nom complet des autres utilisateurs (y compris le propriétaire de l'équipe et les adminsitrateurs). Le nom d'utilisateur est affiché à la place du nom de la personne.",
"admin.privacy.showFullNameTitle": "Afficher le nom complet : ",
"admin.purge.button": "Purger tous les caches",
@@ -591,16 +604,16 @@
"admin.reset_password.close": "Quitter",
"admin.reset_password.newPassword": "Nouveau mot de passe",
"admin.reset_password.select": "Sélectionner",
- "admin.reset_password.submit": "Veuillez saisir au moins {chars} caractères.",
+ "admin.reset_password.submit": "Veuillez spécifier au moins {chars} caractères.",
"admin.reset_password.titleReset": "Réinitialiser le mot de passe",
- "admin.reset_password.titleSwitch": "Basculez vers l’addresse électronique / mot de passe de votre compte",
+ "admin.reset_password.titleSwitch": "Basculez le type d'authentification sur le couple l’adresse e-mail / mot de passe",
"admin.saml.assertionConsumerServiceURLDesc": "Entrez https://<votre-url-mattermost>/login/sso/saml. Veillez à saisir HTTP ou HTTPS dans l'URL suivant votre configuration. Ce champ est aussi connu comme étant l'URL d'Assertion Consumer Service.",
"admin.saml.assertionConsumerServiceURLEx": "Ex. : \"https://<your-mattermost-url>/login/sso/saml\"",
"admin.saml.assertionConsumerServiceURLTitle": "URL du service d'identification :",
- "admin.saml.bannerDesc": "User attributes in SAML server, including user deactivation or removal, are updated in Mattermost during user login. Learn more at: <a href=\"https://docs.mattermost.com/deployment/sso-saml.html\">https://docs.mattermost.com/deployment/sso-saml.html</a>",
- "admin.saml.emailAttrDesc": "Attribut du SAML pour le champ \"adresse électronique\" dans Mattermost.",
+ "admin.saml.bannerDesc": "Les attributs de l'utilisateur spécifiés dans le serveur SAML, y compris la désactivation ou la suppression d'utilisateurs, sont mis à jour dans Mattermost lorsque l'utilisateur tente de se connecter. Pour en savoir plus, rendez-vous sur : <a href=\"https://docs.mattermost.com/deployment/sso-saml.html\">https://docs.mattermost.com/deployment/sso-saml.html</a>",
+ "admin.saml.emailAttrDesc": "Attribut du SAML pour le champ \"adresse e-mail\" dans Mattermost.",
"admin.saml.emailAttrEx": "Ex. : \"Email\" ou \"EmailPrincipal\"",
- "admin.saml.emailAttrTitle": "Attribut \"adresse électronique\" :",
+ "admin.saml.emailAttrTitle": "Attribut \"adresse e-mail\" :",
"admin.saml.enableDescription": "Si activé, Mattermost autorise la connexion en utilisant SAML. Consultez la <a href='http://docs.mattermost.com/deployment/sso-saml.html' target='_blank'>documentation</a> pour en apprendre davantage sur la configuration de SAML pour Mattermost.",
"admin.saml.enableTitle": "Activer la connexion avec SAML :",
"admin.saml.encryptDescription": "Si activé, Mattermost déchiffre les commandes SAML chiffrées avec le certificat public de votre serveur d'authentification.",
@@ -681,7 +694,7 @@
"admin.service.googleDescription": "Définissez cette clé pour activer l'affichage des titres pour les aperçus de vidéos YouTube. Sans la clé, les aperçus YouTube seront créés à partir des liens apparaissant des messages ou commentaires mais ils ne montreront pas le titre de la vidéo. Regardez un <a href=\"https://www.youtube.com/watch?v=Im69kzhpR3I\" target=\"_blank\">tutoriel Google Developers</a> pour des instructions sur comment obtenir une clé.",
"admin.service.googleExample": "Ex. : \"7rAh6iwQCkV4cA1Gsg3fgGOXJAQ43QV\"",
"admin.service.googleTitle": "Clé API Google :",
- "admin.service.iconDescription": "Lorsqu'activé, les webhooks, commandes slash et autres intégrations, telles que <a href=\"https://docs.mattermost.com/integrations/zapier.html\" target=\"_blank\">Zapier</a>, seront autorisées à changer la photo du profil à partir duquel elles émettent des messages. Note : Si les intégrations sont également autorisées à redéfinir les noms d'utilisateur, des utilisateurs pourraient effectuer des attaques de phishing en se faisant passer pour d'autres utilisateurs.",
+ "admin.service.iconDescription": "Lorsqu'activé, les webhooks, commandes slash et autres intégrations, telles que <a href=\"https://docs.mattermost.com/integrations/zapier.html\" target=\"_blank\">Zapier</a>, seront autorisées à changer la photo de profil à partir duquel elles émettent des messages. Note : Si les intégrations sont également autorisées à redéfinir les noms d'utilisateur, des utilisateurs pourraient effectuer des attaques de phishing en se faisant passer pour d'autres utilisateurs.",
"admin.service.iconTitle": "Activer les intégrations à redéfinir les images de profil utilisateur :",
"admin.service.insecureTlsDesc": "Si activé, toute requête sortante HTTPS acceptera les certificats non-vérifiés et auto-signés. Par exemple, les requêtes webhook sortantes avec un certificat TLS auto-signé, vers n'importe quel domaine, seront autorisées. Attention, cela rend votre serveur vulnérable aux attaques de type \"man in the middle\".",
"admin.service.insecureTlsTitle": "Autoriser les connexions sortantes non-sécurisées : ",
@@ -695,7 +708,7 @@
"admin.service.mfaDesc": "Si activé, Les utilisateurs auront la possibilité d'ajouter l'authentification multi- facteur à leur compte . Ils auront besoin d'un smartphone et une application d'authentification tels que Google Authenticator .",
"admin.service.mfaTitle": "Activité l’authentification multi-facteurs:",
"admin.service.mobileSessionDays": "Durée de la session sur les applis mobiles (en jours) :",
- "admin.service.mobileSessionDaysDesc": "Le nombre de jours entre la dernière fois qu'un utilisateur a entré ses identifiants et l'expiration de la session de l'utilisateur. Après le changement de ce paramètre, la nouvelle durée de session prendra effet la prochaine fois que les utilisateurs entreront leurs identifiants.",
+ "admin.service.mobileSessionDaysDesc": "Le nombre de jours entre la dernière fois qu'un utilisateur a spécifié ses identifiants et l'expiration de la session de l'utilisateur. Après le changement de ce paramètre, la nouvelle durée de session prendra effet la prochaine fois que les utilisateurs saisiront leurs identifiants.",
"admin.service.outWebhooksDesc": "Si activé, les webhooks sortants seront autorisés. Consultez la <a href='http://docs.mattermost.com/developer/webhooks-outgoing.html' target='_blank'>documentation</a> pour en apprendre davantage.",
"admin.service.outWebhooksTitle": "Activer les webhooks sortants : ",
"admin.service.overrideDescription": "Lorsqu'activé, les webhooks, commandes slash et autres intégrations, telles que <a href=\"https://docs.mattermost.com/integrations/zapier.html\" target=\"_blank\">Zapier</a>, seront autorisées à changer le nom d'utilisateur à partir duquel elles émettent des messages. Note : Si les intégrations sont également autorisées à redéfinir les photos de profil utilisateur, des utilisateurs pourraient effectuer des attaques de phishing en se faisant passer pour d'autres utilisateurs.",
@@ -714,7 +727,7 @@
"admin.service.siteURLDescription": "L'URL, incluant le numéro de port et le protocol, que les utilisateurs utilisent pour accéder à Mattermost. Ce champ peut être laissé vide à moins que vous ne configuriez l'envoi d'email par lots dans <b>Notifications > Email</b>. Lorsque le champ est vide, l'URL est configurée automatiquement sur base du trafic entrant.",
"admin.service.siteURLExample": "Ex. : \"https://mattermost.example.com:1234\"",
"admin.service.ssoSessionDays": "Durée de la session SSO (en jours) :",
- "admin.service.ssoSessionDaysDesc": "Le nombre de jours entre la dernière fois qu'un utilisateur a entré ses identifiants et l'expiration de la session de l'utilisateur. Si la méthode d'authentification est SAML ou GitLab, l'utilisateur pourra être automatiquement connecté à nouveau dans Mattermost s'ils sont déjà connectés dans SAML ou GitLab. Après le changement de ce paramètre, il prendra effet la prochaine fois que les utilisateurs entreront leurs identifiants.",
+ "admin.service.ssoSessionDaysDesc": "Le nombre de jours entre la dernière fois qu'un utilisateur a spécifié ses identifiants et l'expiration de la session de l'utilisateur. Si la méthode d'authentification est SAML ou GitLab, l'utilisateur pourra être automatiquement connecté à nouveau dans Mattermost s'ils sont déjà connectés dans SAML ou GitLab. Après le changement de ce paramètre, il prendra effet la prochaine fois que les utilisateurs saisiront leurs identifiants.",
"admin.service.testingDescription": "(Option de développement) Si activé, la commande \"/loadtest\" est active et charge des données de test. Changer ce paramètre nécessite de redémarrer le serveur.",
"admin.service.testingTitle": "Activer les commandes de test : ",
"admin.service.tlsCertFile": "Fichier du certificat TLS :",
@@ -743,7 +756,7 @@
"admin.sidebar.customization": "Personnalisation",
"admin.sidebar.database": "Base de données",
"admin.sidebar.developer": "Développeur",
- "admin.sidebar.email": "Adresse électronique",
+ "admin.sidebar.email": "Adresse e-mail",
"admin.sidebar.external": "Services externes",
"admin.sidebar.files": "Ficher",
"admin.sidebar.general": "Général",
@@ -804,8 +817,8 @@
"admin.sql.warning": "Attention : Ré-générer cette clé de salage peut provoquer des valeurs vides dans certaines colonnes de la base de données.",
"admin.support.aboutDesc": "Faites un lien vers la page À propos pour plus d'informations sur votre déploiement Mattermost, par exemple son utilisation et son public dans votre organisation. Par défaut, c'est un lien vers la page d'information de Mattermost.",
"admin.support.aboutTitle": "Lien \"À propos\" :",
- "admin.support.emailHelp": "Adresse électronique affichée sur les notifications de courrier électronique et au cours du tutoriel pour permettre aux utilisateurs de poser des questions.",
- "admin.support.emailTitle": "Adresse électronique de support :",
+ "admin.support.emailHelp": "Adresse e-mail qui apparaît sur les notifications envoyées par e-mail mais également au cours du tutoriel pour permettre aux utilisateurs de poser des questions.",
+ "admin.support.emailTitle": "Adresse e-mail de support :",
"admin.support.helpDesc": "Lien vers la documentation utilisateur de l'équipe. Laissez cette valeur par défaut sauf si votre organisation souhaite créer elle-même cette documentation.",
"admin.support.helpTitle": "Lien d'aide :",
"admin.support.noteDescription": "Si vous faites un lien vers un site externe, les URLs doivent commencer par http:// ou https://.",
@@ -834,7 +847,7 @@
"admin.team.maxChannelsExample": "Ex. : \"100\"",
"admin.team.maxChannelsTitle": "Nombre maximum de canaux par équipe :",
"admin.team.maxNotificationsPerChannelDescription": "Nombre utilisateurs maximum au delà duquel les messages comprenant des mentions @all, @here, et @channel n'engendreront plus l'envoi de notifications à cause des performances.",
- "admin.team.maxNotificationsPerChannelExample": "Ex. : \"1000\"",
+ "admin.team.maxNotificationsPerChannelExample": "Ex. : \"10000\"",
"admin.team.maxNotificationsPerChannelTitle": "Nombre maximum de notifications par canal :",
"admin.team.maxUsersDescription": "Nombre maximum d'utilisateurs par équipe, actifs et inactifs.",
"admin.team.maxUsersExample": "Ex. : \"25\"",
@@ -885,7 +898,7 @@
"admin.user_item.mfaYes": "<strong>MFA</strong> : Oui",
"admin.user_item.resetMfa": "supprimer l’identification à facteurs-multiples",
"admin.user_item.resetPwd": "Réinitialiser le mot de passe",
- "admin.user_item.switchToEmail": "Basculer vers votre adresse électronique / mot de passe",
+ "admin.user_item.switchToEmail": "Basculer le type de connexion sur le couple adresse e-mail / mot de passe",
"admin.user_item.sysAdmin": "Administrateur système",
"admin.user_item.teamAdmin": "Administrateur d'équipe",
"admin.webrtc.enableDescription": "Lorsqu'activé, Mattermost autorise les appels vidéo en <strong>tête-à-tête</strong>. Les appels par WebRTC sont disponibles sur Chrome, Firefox et les applications Mattermost de bureau.",
@@ -923,8 +936,10 @@
"analytics.chart.meaningful": "Pas assez de données pour afficher quelque chose de pertinent.",
"analytics.system.activeUsers": "Utilisateurs actifs avec messages",
"analytics.system.channelTypes": "Types de canaux",
- "analytics.system.expiredBanner": "La licence d'Enterprise a expiré le {date}. Vous avez 15 jours à compter de cette date pour renouveler la licence, veuillez contacter <a href='mailto:commercial@mattermost.com'>commercial@mattermost.com </a>.",
- "analytics.system.expiringBanner": "La licence d'Entreprise expire le {date}. Pour renouveler votre licence, veuillez contacter<a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>.",
+ "analytics.system.dailyActiveUsers": "Daily Active Users",
+ "analytics.system.expiredBanner": "La licence Entreprise a expiré le {date}. Vous avez 15 jours à compter de cette date pour renouveler la licence, veuillez contacter <a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>.",
+ "analytics.system.expiringBanner": "La licence Entreprise expire le {date}. Pour renouveler votre licence, veuillez contacter<a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>.",
+ "analytics.system.monthlyActiveUsers": "Monthly Active Users",
"analytics.system.postTypes": "Messages, fichiers et hashtags",
"analytics.system.privateGroups": "Groupes privés",
"analytics.system.publicChannels": "Canaux publics",
@@ -1006,7 +1021,7 @@
"audit_table.userAdded": "{username} ajouté au canal/groupe {channelName}",
"audit_table.userId": "Identifiant de l'utilisateur",
"audit_table.userRemoved": "{username} retiré du canal/groupe {channelName}",
- "audit_table.verified": "Votre adresse électronique est maintenant vérifiée",
+ "audit_table.verified": "Votre adresse e-mail est maintenant vérifiée",
"authorize.access": "Autoriser l'accès à <strong>{appName}</strong> ?",
"authorize.allow": "Autoriser",
"authorize.app": "L'application <strong>{appName}</strong> souhaite accéder et modifier vos informations de base.",
@@ -1021,8 +1036,7 @@
"backstage_sidebar.integrations.oauthApps": "Applications OAuth 2.0",
"backstage_sidebar.integrations.outgoing_webhooks": "Webhooks sortants",
"calling_screen": "Appel",
- "center_panel.recent": "Cliquez ici pour voir les messages les plus récents. ",
- "chanel_header.addMembers": "Ajouter des membres",
+ "center_panel.recent": "Veuillez cliquer ici pour voir les messages récents. ",
"change_url.close": "Fermer",
"change_url.endWithLetter": "Doit se terminer par une lettre ou un nombre",
"change_url.invalidUrl": "URL non valide",
@@ -1033,33 +1047,34 @@
"channelHeader.removeFromFavorites": "Retirer des favoris",
"channel_flow.alreadyExist": "Il existe déjà un canal avec cette URL",
"channel_flow.changeUrlDescription": "Certains caractères ne sont pas autorisés dans les URL et doivent être retirés.",
- "channel_flow.changeUrlTitle": "Changer l'URL de {term}",
+ "channel_flow.changeUrlTitle": "Changer l'URL {term}",
"channel_flow.channel": "Canal",
"channel_flow.create": "Créer {term}",
"channel_flow.group": "Groupe",
"channel_flow.handleTooShort": "L'URL du canal doit comporter au moins 2 caractères alphanumériques minuscules",
"channel_flow.invalidName": "Nom du canal incorrect",
"channel_flow.set_url_title": "Choisir l'URL de {term}",
+ "channel_header.addMembers": "Ajouter des membres",
"channel_header.addToFavorites": "Ajouter aux favoris",
"channel_header.channel": "Canal",
"channel_header.channelHeader": "Éditer l'entête du canal",
"channel_header.delete": "Supprimer {term}",
- "channel_header.flagged": "Messages avec indicateur",
+ "channel_header.flagged": "Messages marqués d'un indicateur",
"channel_header.group": "Groupe",
"channel_header.leave": "Quitter {term}",
"channel_header.manageMembers": "Gérer les membres",
"channel_header.notificationPreferences": "Préférences de notification",
"channel_header.recentMentions": "Mentions récentes",
"channel_header.removeFromFavorites": "Retirer des favoris",
- "channel_header.rename": "Renommer {term}",
- "channel_header.setHeader": "Éditer l'entête {term}",
- "channel_header.setPurpose": "Éditer le descriptif {term}",
+ "channel_header.rename": "Renommer le {term}",
+ "channel_header.setHeader": "Éditer l'entête du {term}",
+ "channel_header.setPurpose": "Éditer le descriptif du {term}",
"channel_header.viewInfo": "Informations",
"channel_header.viewMembers": "Voir les membres",
"channel_header.webrtc.call": "Démarrer un appel vidéo",
"channel_header.webrtc.offline": "L'utilisateur est hors ligne",
"channel_header.webrtc.unavailable": "Passer un nouvel appel n'est pas possible tant que l'appel en cours n'a pas été terminé",
- "channel_info.about": "À propos",
+ "channel_info.about": "À propos de",
"channel_info.close": "Fermer",
"channel_info.header": "En-tête :",
"channel_info.id": "ID : ",
@@ -1068,7 +1083,7 @@
"channel_info.purpose": "Description :",
"channel_info.url": "URL :",
"channel_invite.add": " Ajouter",
- "channel_invite.addNewMembers": "Ajouter de nouveaux membres à ",
+ "channel_invite.addNewMembers": "Ajouter des nouveaux membres à ",
"channel_invite.close": "Fermer",
"channel_loader.connection_error": "Oups... Il semble que votre connexion internet ait un problème...",
"channel_loader.posted": "Posté",
@@ -1079,32 +1094,38 @@
"channel_loader.uploadedFile": " téléchargé un fichier",
"channel_loader.uploadedImage": " téléchargé une image",
"channel_loader.wrote": " a écrit : ",
- "channel_members_modal.addNew": " Ajouter de nouveaux membres",
- "channel_members_modal.close": "Fermer",
- "channel_members_modal.remove": "Supprimer",
- "channel_memebers_modal.members": " Membres",
+ "channel_members_dropdown.channel_admin": "Channel Admin",
+ "channel_members_dropdown.channel_member": "Membres du canal",
+ "channel_members_dropdown.make_channel_admin": "Make Channel Admin",
+ "channel_members_dropdown.make_channel_member": "Make Channel Member",
+ "channel_members_dropdown.remove_from_channel": "Remove From Channel",
+ "channel_members_dropdown.remove_member": "Remove Member",
+ "channel_members_modal.addNew": " Ajouter des nouveaux membres",
+ "channel_members_modal.members": " Membres",
"channel_modal.cancel": "Annuler",
"channel_modal.channel": "Canal",
- "channel_modal.createNew": "Créer un(e) nouveau(elle) ",
- "channel_modal.descriptionHelp": "Décrivez comment ce {term} doit être utilisé.",
+ "channel_modal.createNew": "Créer un nouveau ",
+ "channel_modal.descriptionHelp": "Décrit comment ce {term} doit être utilisé.",
"channel_modal.displayNameError": "Ce champ est obligatoire",
"channel_modal.edit": "Modifier",
"channel_modal.group": "Groupe",
"channel_modal.header": "En-tête",
- "channel_modal.headerHelp": "Voir le text qui apparaitra en en-tête de {term} en regard du nom de {term}. Par exemple, saisissez des liens fréquemment utilisés en tapant [Link Title](http://example.com).",
+ "channel_modal.headerEx": "E.g.: \"[Link Title](http://example.com)\"",
+ "channel_modal.headerHelp": "Définit le texte qui apparaitra en en-tête de {term} en regard du nom du {term}. Par exemple, saisissez des liens fréquemment utilisés en tapant [Lien de titre](http://exemple.com).",
"channel_modal.modalTitle": "Nouveau ",
"channel_modal.name": "Nom",
"channel_modal.nameEx": "Exemple : \"Problèmes\", \"Marketing\", \"Éducation\"",
"channel_modal.optional": "(facultatif)",
- "channel_modal.privateGroup1": "Créer un nouveau groupe privé avec des membres restreints. ",
+ "channel_modal.privateGroup1": "Crée un nouveau groupe privé avec des membres restreints. ",
"channel_modal.privateGroup2": "Créer un groupe privé",
"channel_modal.publicChannel1": "Créer un canal public",
- "channel_modal.publicChannel2": "Créer un canal public que tout le monde peut rejoindre. ",
+ "channel_modal.publicChannel2": "Crée un canal public que tout le monde peut rejoindre. ",
"channel_modal.purpose": "Description",
+ "channel_modal.purposeEx": "E.g.: \"A channel to file bugs and improvements\"",
"channel_notifications.allActivity": "Pour toute l'activité",
"channel_notifications.allUnread": "Pour les messages non-lus",
"channel_notifications.globalDefault": "Par défaut ({notifyLevel})",
- "channel_notifications.markUnread": "Marquer la canal comme non-lu",
+ "channel_notifications.markUnread": "Marquer le canal comme non-lu",
"channel_notifications.never": "Jamais",
"channel_notifications.onlyMentions": "Seulement pour les mentions",
"channel_notifications.override": "Choisir une autre option que \"Par défaut\" remplacera le réglage par défaut. Les notifications sur le bureau sont disponibles sur Firefox, Safari et Chrome.",
@@ -1113,11 +1134,12 @@
"channel_notifications.unreadInfo": "Le nom du canal est en gras dans la barre latérale lorsqu'il y a des messages non-plus. Choisir \"Seulement pour les mentions\" mettra en gras le canal seulement si vous être mentionné.",
"channel_select.placeholder": "--- Sélectionnez un canal ---",
"channel_switch_modal.dm": "(Message privé)",
+ "channel_switch_modal.failed_to_open": "Failed to open channel.",
"channel_switch_modal.help": "Saisissez le nom du canal. Utilisez ↑ ↓ pour parcourir, TAB pour sélectionner, ↵ pour confirmer, ESC pour rejeter",
"channel_switch_modal.not_found": "Aucune correspondance trouvée.",
"channel_switch_modal.submit": "Basculer",
"channel_switch_modal.title": "Changer de canal",
- "claim.account.noEmail": "Aucune adresse électronique indiquée",
+ "claim.account.noEmail": "Aucune adresse e-mail spécifiée",
"claim.email_to_ldap.enterLdapPwd": "Saisissez l'identifiant et le mode de passe de votre compte LDAP",
"claim.email_to_ldap.enterPwd": "Saisissez le mot de passe pour votre compte {site}",
"claim.email_to_ldap.ldapId": "Identifiant LDAP",
@@ -1136,9 +1158,9 @@
"claim.email_to_oauth.ssoNote": "Vous devez déjà avoir un compte {type} valide",
"claim.email_to_oauth.ssoType": "Une fois votre compte associé, vous ne pourrez vous connectez que via le SSO {type}",
"claim.email_to_oauth.switchTo": "Changer de compte pour {uiType}",
- "claim.email_to_oauth.title": "Changer l'adresse électronique/mot de passe pour {uiType}",
+ "claim.email_to_oauth.title": "Changer l'adresse e-mail/mot de passe pour {uiType}",
"claim.ldap_to_email.confirm": "Confirmer le mot de passe",
- "claim.ldap_to_email.email": "Vous devrez utiliser l'adresse électronique {email} pour vous connecter.",
+ "claim.ldap_to_email.email": "Vous devrez utiliser l'adresse e-mail {email} pour vous connecter.",
"claim.ldap_to_email.enterLdapPwd": "Saisissez votre {ldapPassword} pour votre compte {site}",
"claim.ldap_to_email.enterPwd": "Saisissez un nouveau mot de passe pour votre compte",
"claim.ldap_to_email.ldapPasswordError": "Veuillez saisir votre mot de passe LDAP.",
@@ -1146,17 +1168,17 @@
"claim.ldap_to_email.pwd": "Mot de passe",
"claim.ldap_to_email.pwdError": "Veuillez saisir votre mot de passe.",
"claim.ldap_to_email.pwdNotMatch": "Les mots de passe ne correspondent pas.",
- "claim.ldap_to_email.ssoType": "Une fois votre compte modifié, vous ne pourrez plus vous connecter qu'avec votre adresse électronique et votre mot de passe.",
- "claim.ldap_to_email.switchTo": "Basculez votre compte vers adresse électronique / mot de passe",
- "claim.ldap_to_email.title": "Basculer votre compte de LDAP vers adresse électronique / mot de passe",
+ "claim.ldap_to_email.ssoType": "Une fois votre compte modifié, vous ne pourrez plus vous connecter qu'avec votre adresse e-mail et votre mot de passe.",
+ "claim.ldap_to_email.switchTo": "Basculez le type de connexion de votre compte sur le couple adresse e-mail / mot de passe",
+ "claim.ldap_to_email.title": "Basculer le type de connexion de votre compte de LDAP vers le couple adresse e-mail / mot de passe",
"claim.oauth_to_email.confirm": "Confirmez le mot de passe",
- "claim.oauth_to_email.description": "Une fois votre compte modifié, vous ne pourrez plus vous connecter qu'à l'aide de votre adresse électronique et votre mot de passe.",
+ "claim.oauth_to_email.description": "Une fois votre compte modifié, vous ne pourrez plus vous connecter qu'à l'aide de votre adresse e-mail et votre mot de passe.",
"claim.oauth_to_email.enterNewPwd": "Saisissez le mot de passe pour votre compte {site}",
"claim.oauth_to_email.enterPwd": "Veuillez saisir un mot de passe.",
"claim.oauth_to_email.newPwd": "Nouveau mot de passe",
"claim.oauth_to_email.pwdNotMatch": "Le mot de passe ne correspond pas.",
- "claim.oauth_to_email.switchTo": "Basculer de {type} vers adresse électronique et mot de passe",
- "claim.oauth_to_email.title": "Basculer du compte {type} vers l'adresse électronique",
+ "claim.oauth_to_email.switchTo": "Basculer le type de connexion de {type} vers le couple adresse e-mail et mot de passe",
+ "claim.oauth_to_email.title": "Basculer la connexion de type {type} vers l'utilisation d'une adresse e-mail",
"confirm_modal.cancel": "Annuler",
"connecting_screen": "Connexion en cours",
"create_comment.addComment": "Commenter...",
@@ -1169,7 +1191,7 @@
"create_post.error_message": "Votre message est trop long. Nombre de caractères : {length}/{limit}",
"create_post.post": "Article",
"create_post.shortcutsNotSupported": "Les raccourcis clavier ne sont pas pris en charge sur votre appareil.",
- "create_post.tutorialTip": "<h4>Envoyer des messages</h4><p>Entrez votre message ici et tapez <strong>Entrée</strong> pour l'envoyer.</p><p>Cliquez sur le bouton <strong>pièce-jointe</strong> pour télécharger une image ou un fichier.</p>",
+ "create_post.tutorialTip": "<h4>Envoyer des messages</h4><p>Veuillez saisir votre message ici et tapez <strong>Entrée</strong> pour l'envoyer.</p><p>Cliquez sur le bouton <strong>pièce-jointe</strong> pour télécharger une image ou un fichier.</p>",
"create_post.write": "Écrire un message...",
"create_team.agreement": "En créant votre compte et en utilisant {siteName}, vous acceptez nos <a href={TermsOfServiceLink}>Conditions Générales d'Utilisation</a> et notre <a href={PrivacyPolicyLink}>Politique des données personnelles</a>. Si vous le les acceptez pas, vous ne pouvez pas utiliser {siteName}.",
"create_team.display_name.back": "Retour à l’étape précedente",
@@ -1209,7 +1231,7 @@
"edit_channel_header_modal.description": "Modifiez le texte affiché à côté du nom du canal dans l'en-tête du canal.",
"edit_channel_header_modal.error": "L'en-tête du canal est trop long, saisissez un texte plus court",
"edit_channel_header_modal.save": "Enregistrer",
- "edit_channel_header_modal.title": "Modifier l'en-tête pour {channel}",
+ "edit_channel_header_modal.title": "Modifier l'en-tête de {channel}",
"edit_channel_header_modal.title_dm": "Modifier l'en-tête",
"edit_channel_purpose_modal.body": "Décrit de quelle manière ce {type} devrait être utilisé. Ce texte apparaît dans la liste des canaux dans le menu \"Suite...\" et guide les personnes pour décider si elles devraient le rejoindre ou non.",
"edit_channel_purpose_modal.cancel": "Annuler",
@@ -1224,7 +1246,7 @@
"edit_post.edit": "Modifier {title}",
"edit_post.editPost": "Modifier le message...",
"edit_post.save": "Enregistrer",
- "email_signup.address": "Adresse électronique",
+ "email_signup.address": "Adresse e-mail",
"email_signup.createTeam": "Créer une équipe",
"email_signup.emailError": "Veuillez spécifier une adresse e-mail valide.",
"email_signup.find": "Rechercher mes équipes",
@@ -1233,9 +1255,9 @@
"email_verify.notVerifiedBody": "Veuillez vérifier votre adresse e-mail dans l'attente de la réception d'un e-mail de vérification.",
"email_verify.resend": "Renvoyer l'e-mail",
"email_verify.sent": "L'e-mail de vérification a été envoyé.",
- "email_verify.verified": "L'adresse électronique de {siteName} a été vérifiée",
- "email_verify.verifiedBody": "<p>Votre adresse électronique a été vérifiée ! Cliquez <a href={url}>ici</a> pour vous connecter.</p>",
- "email_verify.verifyFailed": "Impossible de vérifier votre adresse électronique.",
+ "email_verify.verified": "L'adresse e-mail de {siteName} a été vérifiée",
+ "email_verify.verifiedBody": "<p>Votre adresse e-mail a été vérifiée ! Veuillez cliquer <a href={url}>ici</a> pour vous connecter.</p>",
+ "email_verify.verifyFailed": "Impossible de vérifier votre adresse e-mail.",
"emoji_list.actions": "Actions",
"emoji_list.add": "Ajouter un emoji personnalisé",
"emoji_list.creator": "Auteur",
@@ -1253,9 +1275,9 @@
"error.not_found.title": "Page non trouvée",
"error.not_supported.message": "La navigation privée n'est pas prise en charge.",
"error.not_supported.title": "Le navigateur n'est pas pris en charge",
- "error_bar.expired": "La licence d'Entreprise a expiré. Vous disposez de 15 jours à compter de l'expiration pour renouveler la licence, veuillez contacter commercial@mattermost.com pour plus d'informations",
- "error_bar.expiring": "La licence d'Entreprise expire le {date}. Pour renouveler votre licence, veuillez contacter commercial@mattermost.com",
- "error_bar.past_grace": "La licence d'Entreprise a expiré, veuillez contacter votre administrateur système pour plus d'informations",
+ "error_bar.expired": "La licence Entreprise a expiré. Vous disposez de 15 jours à compter de l'expiration pour renouveler la licence, veuillez contacter commercial@mattermost.com pour plus d'informations",
+ "error_bar.expiring": "La licence Entreprise expire le {date}. Pour renouveler votre licence, veuillez contacter commercial@mattermost.com",
+ "error_bar.past_grace": "La licence Entreprise a expiré, veuillez contacter votre administrateur système pour plus d'informations",
"error_bar.preview_mode": "Mode découverte : Les notifications par e-mail ne sont pas configurées",
"file_attachment.download": "Télécharger",
"file_info_preview.size": "Taille ",
@@ -1276,24 +1298,23 @@
"filtered_user_list.searchButton": "Rechercher",
"filtered_user_list.show": "Filtre :",
"filtered_user_list.team_only": "Membre de l’équipe",
- "find_team.email": "Adresse électronique",
+ "find_team.email": "Adresse e-mail",
"find_team.findDescription": "Un e-mail a été envoyé avec les liens vers les équipes dont vous êtes membre.",
"find_team.findTitle": "Trouvez votre équipe",
"find_team.getLinks": "Obtenez par e-mail des liens vers les équipes dont vous êtes membre.",
"find_team.placeholder": "vous@domaine.com",
"find_team.send": "Envoyer",
- "find_team.submitError": "Veuillez entrer une adresse électronique valide",
- "flag_post.flag": "Ajoutez un indicateur pour poursuivre",
+ "find_team.submitError": "Veuillez spécifier une adresse e-mail valide",
+ "flag_post.flag": "Marquez le message d'un indicateur pour le suivi",
"flag_post.unflag": "Supprimer l'indicateur",
+ "general_tab.chooseDescription": "Choisissez un nom pour votre équipe",
"general_tab.chooseName": "Choisissez un nom pour votre équipe",
- "general_tab.codeDesc": "Cliquez sur \"Modifier\" pour réinitialiser le code d'invitation",
+ "general_tab.codeDesc": "Veuillez cliquer sur 'Modifier' pour réinitialiser le code d'invitation",
"general_tab.codeLongDesc": "Le code d'invitation est utilisé dans l'URL contenant l'invitation à votre équipe créé depuis le menu principal grâce à <strong>Obtenir un lien d’invitation d’équipe</strong>. Regénérer cette clé rend toutes les invitations déjà envoyées invalide.",
"general_tab.codeTitle": "Code d'invitation",
- "general_tab.dirDisabled": "L'annuaire d'équipe a été désactivé. Veuillez demander à un administrateur système d'activer l'annuaire d'équipe dans la console système.",
- "general_tab.dirOff": "L'annuaire des équipes est désactivé sur ce site. ",
- "general_tab.emptyDescription": "Cliquez sur 'Modifier' pour ajouter une description d'équipe.",
+ "general_tab.emptyDescription": "Veuillez cliquer sur 'Modifier' pour ajouter une description d'équipe.",
+ "general_tab.getTeamInviteLink": "Créer une d'invitation",
"general_tab.includeDirDesc": "Inclure cette équipe affichera le nom de l'équipe dans l'annuaire sur la page d'accueil, ainsi qu'un lien pour rejoindre cette équipe.",
- "general_tab.includeDirTitle": "Afficher cette équipe dans l'annuaire",
"general_tab.no": "Non",
"general_tab.openInviteDesc": "Lorsque autorisé, un lien vers cette équipe sera inclus sur la page d'accueil permettant à quiconque avec un compte de rejoindre cette équipe.",
"general_tab.openInviteTitle": "Permettre à n'importe quel utilisateur disposant d'un compte de rejoindre cette équipe",
@@ -1315,7 +1336,7 @@
"get_app.mattermostInc": "Mattermost, Inc",
"get_app.openMattermost": "Ouvrir Mattermost",
"get_link.clipboard": " Lien copié",
- "get_link.close": "Quitter",
+ "get_link.close": "Fermer",
"get_link.copy": "Copier l'URL",
"get_post_link_modal.help": "Le lien ci-dessous permet aux utilisateurs de voir votre message.",
"get_post_link_modal.title": "Copier le lien permanent",
@@ -1479,12 +1500,12 @@
"integrations.outgoingWebhook.description": "Les webhooks sortants permettent des intégrations externes afin de recevoir et de répondre aux messages",
"integrations.outgoingWebhook.title": "Webhooks sortants",
"integrations.successful": "Installation réussie",
- "intro_messages.DM": "Vous êtes au début de votre historique de messages avec {teammate}.<br />Les messages privés et les fichiers partagés ici ne sont visibles pour personne d'autre.",
+ "intro_messages.DM": "Vous êtes au début de votre historique de messages avec {teammate}.<br />Les messages privés et les fichiers partagés ici ne sont pas visibles par les autres utilisateurs.",
"intro_messages.anyMember": " Tout membre peut rejoindre et lire ce canal.",
"intro_messages.beginning": "Début de {name}",
"intro_messages.channel": "canal",
"intro_messages.creator": "Ceci est le début de {type} {name}, créé par {creator} le {date}.",
- "intro_messages.default": "<h4 class='channel-intro__title'>Bienvenue sur {display_name}</h4><p class='channel-intro__content'><strong>Bienvenue sur {display_name}!</strong><br/><br/>Ceci est le premier canal que les membres de votre équipe verront lors de leur inscription. Utilisez-le pour poster des informations que tout le monde doit voir.</p>",
+ "intro_messages.default": "<h4 class='channel-intro__title'>Bienvenue sur {display_name}</h4><p class='channel-intro__content'><strong>Bienvenue sur {display_name}!</strong><br/><br/>Ceci est le premier canal que les membres de votre équipe verront lors de leur inscription. Utilisez-le pour poster des informations que tout le monde devrait voir.</p>",
"intro_messages.group": "Groupe privé",
"intro_messages.invite": "Inviter d'autres membres sur ce {type}",
"intro_messages.inviteOthers": "Inviter d'autres membres dans cette équipe",
@@ -1493,13 +1514,13 @@
"intro_messages.onlyInvited": " Seuls les membres invités peuvent voir ce groupe privé.",
"intro_messages.purpose": " L'objectif de {type} est : {purpose}.",
"intro_messages.setHeader": "Définir l'en-tête",
- "intro_messages.teammate": "Vous êtes au début de votre historique de messages avec cette personne. Les messages privés et les fichiers partagés ici ne sont visibles pour personne d'autre.",
+ "intro_messages.teammate": "Vous êtes au début de votre historique de messages avec cet utilisateur. Les messages privés et les fichiers partagés ici ne sont pas visibles par les autres utilisateurs.",
"invite_member.addAnother": "Ajouter un autre",
"invite_member.autoJoin": "Les membres automatiquement invités rejoignent le canal <strong>{channel}</strong>.",
"invite_member.cancel": "Annuler",
"invite_member.content": "Les e-mails sont désactivés pour votre équipe et ne peuvent pas être envoyés. Contactez votre administrateur système pour activer l'envoi d'e-mails et l'envoi d'invitations par e-mail.",
"invite_member.disabled": "La création d'utilisateurs est désactivée pour votre équipe. Veuillez contacter votre administrateur système.",
- "invite_member.emailError": "Veuillez saisir une adresse électronique valide",
+ "invite_member.emailError": "Veuillez saisir une adresse e-mail valide",
"invite_member.firstname": "Prénom",
"invite_member.inviteLink": "Lien d'invitation à l'équipe",
"invite_member.lastname": "Nom de famille",
@@ -1515,7 +1536,7 @@
"ldap_signup.ldap": "Créer une nouvelle équipe avec un compte LDAP",
"ldap_signup.length_error": "Le nom doit contenir de 3 à 15 caractères",
"ldap_signup.teamName": "Entrez le nom de votre nouvelle équipe",
- "ldap_signup.team_error": "Saisissez le nom de votre équipe",
+ "ldap_signup.team_error": "Veuillez saisir le nom de votre équipe",
"leave_team_modal.desc": "Vous serez retiré de toutes les chaînes publiques et privées. Si l'équipe est privé, vous ne serez pas en mesure de rejoindre l'équipe. Êtes-vous sûr?",
"leave_team_modal.no": "Non",
"leave_team_modal.title": "Quitter l'équipe ?",
@@ -1525,7 +1546,7 @@
"login.create": "Créer maintenant",
"login.createTeam": "Créer une nouvelle équipe",
"login.createTeamAdminOnly": "Cette option n'est disponible que pour les administrateurs système, et ne s'affiche pas pour les autres utilisateurs.",
- "login.email": "Adresse électronique",
+ "login.email": "Adresse e-mail",
"login.find": "Trouver vos autres équipes",
"login.forgot": "Mot de passe perdu",
"login.gitlab": "GitLab",
@@ -1534,15 +1555,15 @@
"login.ldapUsername": "Nom d’utilisateur LDAP",
"login.ldapUsernameLower": "Nom d’utilisateur LDAP",
"login.noAccount": "Pas de compte utilisateur ? ",
- "login.noEmail": "Veuillez saisir votre adresse électronique.",
- "login.noEmailLdapUsername": "Veuillez saisir votre adresse électronique ou {ldapUsername}",
- "login.noEmailUsername": "Veuillez saisir votre adresse électronique ou votre nom d'utilisateur",
- "login.noEmailUsernameLdapUsername": "Veuillez entrer votre adresse électronique, votre nom d'utilisateur ou {ldapUsername}",
+ "login.noEmail": "Veuillez saisir votre adresse e-mail.",
+ "login.noEmailLdapUsername": "Veuillez saisir votre adresse e-mail ou {ldapUsername}",
+ "login.noEmailUsername": "Veuillez saisir votre adresse e-mail ou votre nom d'utilisateur",
+ "login.noEmailUsernameLdapUsername": "Veuillez spécifier votre adresse e-mail, votre nom d'utilisateur ou {ldapUsername}",
"login.noLdapUsername": "Veuillez saisir votre {ldapUsername}",
"login.noMethods": "Aucune méthode configurée pour s'inscrive, veuillez contacter votre administrateur système.",
"login.noPassword": "Veuillez saisir votre mot de passe",
- "login.noUsername": "Veuillez entrer votre nom d'utilisateur",
- "login.noUsernameLdapUsername": "Saisissez votre nom d'utilisateur ou {ldapUsername}",
+ "login.noUsername": "Veuillez spécifier votre nom d'utilisateur",
+ "login.noUsernameLdapUsername": "Veuillez saisir votre nom d'utilisateur ou {ldapUsername}",
"login.office365": "Office 365",
"login.on": "activé {siteName}",
"login.or": "ou",
@@ -1550,25 +1571,28 @@
"login.passwordChanged": " Mot de passe mis a jour avec succès",
"login.session_expired": " Votre session a expiré. Merci de vous reconnecter.",
"login.signIn": "Connexion",
+ "login.signInLoading": "Signing in...",
"login.signInWith": "Se connecter avec:",
"login.userNotFound": "Nous ne trouvons aucun compte correspondants a vos identifiants de connexions.",
"login.username": "Nom d'utilisateur",
- "login.verified": " Adresse électronique vérifiée",
- "login_mfa.enterToken": "Pour compléter le processus de connexion , entrer un token de la authentificateur de votre smartphone",
+ "login.verified": " Adresse e-mail vérifiée",
+ "login_mfa.enterToken": "Pour terminer le processus de connexion, veuillez spécifier le jeton apparaissant dans l'application d'authentification de votre smartphone.",
"login_mfa.submit": "Envoyer",
"login_mfa.token": "MFA Token",
- "login_mfa.tokenReq": "Entrer un MFA Token",
+ "login_mfa.tokenReq": "Veuillez spécifier un jeton d'authentification multi-facteurs (MFA)",
"member_item.makeAdmin": "Passer Administrateur",
"member_item.member": "Membre",
"member_list.noUsersAdd": "Aucun utilisateur à ajouter.",
- "members_popover.msg": "Message",
+ "members_popover.manageMembers": "Gérer les membres",
+ "members_popover.msg": "Envoyer un message",
"members_popover.title": "Membres",
+ "members_popover.viewMembers": "Voir les membres",
"mfa.confirm.complete": "<strong>Installation terminée !</strong>",
"mfa.confirm.okay": "OK",
- "mfa.confirm.secure": "Votre compte est maintenant sécurisé. La prochaine fois que vous vous connectez, vous serez invité à entrer un code à partir de l'application Google Authenticator sur votre téléphone.",
+ "mfa.confirm.secure": "Votre compte est maintenant sécurisé. La prochaine fois que vous vous connectez, vous serez invité à spécifier un code à partir de l'application Google Authenticator sur votre téléphone.",
"mfa.setup.badCode": "Code non valide. Si ce problème persiste, contactez votre Administrateur Système.",
"mfa.setup.code": "Code MFA",
- "mfa.setup.codeError": "Veuillez entrer le code de Google Authenticator.",
+ "mfa.setup.codeError": "Veuillez saisir le code de Google Authenticator.",
"mfa.setup.required": "<strong>L'authentification multi-facteurs est requise sur {siteName}.</strong>",
"mfa.setup.save": "Enregistrer",
"mfa.setup.secret": "Clé secrète : {secret}",
@@ -1587,9 +1611,9 @@
"mobile.routes.selectTeam": "Choisir une équipe",
"more_channels.close": "Fermer",
"more_channels.create": "Créer un nouveau canal",
- "more_channels.createClick": "Cliquez sur \"Créer un nouveau canal\" pour en créer un",
+ "more_channels.createClick": "Veuillez cliquer sur \"Créer un nouveau canal\" pour en créer un nouveau",
"more_channels.join": "Rejoindre",
- "more_channels.noMore": "Plus de canal à rejoindre",
+ "more_channels.noMore": "Il n'y a plus d'autre canal que vous pouvez rejoindre",
"more_channels.title": "Plus de canaux",
"more_direct_channels.close": "Fermer",
"more_direct_channels.message": "Message",
@@ -1610,10 +1634,10 @@
"navbar.toggle1": "Basculer la barre latérale",
"navbar.toggle2": "Basculer la barre latérale",
"navbar.viewInfo": "Afficher Informations",
- "navbar_dropdown.about": "À propos de Mattermost",
+ "navbar_dropdown.about": "À propos",
"navbar_dropdown.accountSettings": "Paramètres du compte",
"navbar_dropdown.console": "Console système",
- "navbar_dropdown.create": "Créer une nouvelle équipe",
+ "navbar_dropdown.create": "Créer une équipe",
"navbar_dropdown.emoji": "Emoji personnalisés",
"navbar_dropdown.help": "Aide",
"navbar_dropdown.integrations": "Intégrations",
@@ -1626,21 +1650,21 @@
"navbar_dropdown.report": "Signaler un problème",
"navbar_dropdown.switchTeam": "Basculer sur {team}",
"navbar_dropdown.switchTo": "Basculer vers ",
- "navbar_dropdown.teamLink": "Obtenir un lien d'invitation d'équipe",
+ "navbar_dropdown.teamLink": "Créer une d'invitation",
"navbar_dropdown.teamSettings": "Configuration de l'équipe",
"navbar_dropdown.viewMembers": "Voir les membres",
"notification.dm": "Message privé",
"passwordRequirements": "Pré-requis du mot de passe :",
"password_form.change": "Modifier mon mot de passe",
- "password_form.click": "Cliquez <a href={url}>ici</a> pour vous connecter.",
+ "password_form.click": "Veuillez cliquer <a href={url}>ici</a> pour vous connecter.",
"password_form.enter": "Saisissez un nouveau mot de passe pour votre compte {siteName}.",
"password_form.error": "Veuillez saisir au moins {chars} caractères.",
"password_form.pwd": "Mot de passe",
"password_form.title": "Réinitialisation mot de passe",
"password_form.update": "Votre mot de passe a été correctement mis à jour.",
"password_send.checkInbox": "Veuillez contrôler votre boite de réception.",
- "password_send.description": "Pour réinitialiser votre mot de passe, saisissez l'adresse électronique que vous avez utilisé pour vous inscrire.",
- "password_send.email": "Adresse électronique",
+ "password_send.description": "Pour réinitialiser votre mot de passe, veuillez saisir l'adresse e-mail que vous avez utilisée pour vous inscrire",
+ "password_send.email": "Adresse e-mail",
"password_send.error": "Veuillez spécifier une adresse e-mail valide.",
"password_send.link": "Si le compte existe, une demande de réinitialisation du mot de passe sera envoyée à l'adresse e-mail : <br/><b>{email}</b><br/><br/>",
"password_send.reset": "Réinitialiser mon mot de passe",
@@ -1665,6 +1689,7 @@
"post_info.mobile.unflag": "Supprimer l'indicateur",
"post_info.permalink": "Lien permanent",
"post_info.reply": "Répondre",
+ "post_message_view.edited": "(edited)",
"posts_view.loadMore": "Charger plus de messages",
"posts_view.newMsg": "Nouveaux messages",
"posts_view.newMsgBelow": "Nouveau {count, plural, one {message} other {messages}} dessous",
@@ -1702,7 +1727,7 @@
"rhs_comment.mobile.unflag": "Supprimer l'indicateur",
"rhs_comment.permalink": "Lien permanent",
"rhs_header.backToCallTooltip": "Retour à l'appel",
- "rhs_header.backToFlaggedTooltip": "Retourner aux messages avec indicateur",
+ "rhs_header.backToFlaggedTooltip": "Retourner aux messages marqués d'un indicateur",
"rhs_header.backToResultsTooltip": "Retour aux résultats",
"rhs_header.closeSidebarTooltip": "Fermer la barre latérale",
"rhs_header.closeTooltip": "Fermer la barre latérale",
@@ -1718,19 +1743,19 @@
"rhs_root.permalink": "Lien permanent",
"search_bar.cancel": "Annuler",
"search_bar.search": "Rechercher",
- "search_bar.usage": "<h4>Options de recherche</h4><ul><li><span>Utilisez </span><b>\"des guillemets\"</b><span> pour rechercher des phrases</span></li><li><span>Utilisez </span><b>from:</b><span> pour rechercher les messages d'utilisateur spécifiques et </span><b>in:</b><span> pour rechercher les messages sur des canaux spécifiques</span></li></ul>",
+ "search_bar.usage": "<h4>Options de recherche</h4><ul><li><span>Utilisez </span><b>\"des guillemets\"</b><span> pour rechercher des phrases</span></li><li><span>Utilisez </span><b>from:</b><span> pour rechercher des messages d'utilisateur spécifiques et </span><b>in:</b><span> pour rechercher des messages sur des canaux spécifiques</span></li></ul>",
"search_header.results": "Résultats de la recherche",
"search_header.title2": "Mentions récentes",
- "search_header.title3": "Postes avec indicateur",
+ "search_header.title3": "Messages marqués d'un indicateur",
"search_item.direct": "Message privé (avec {username})",
"search_item.jump": "Lien (jump)",
"search_results.because": "<ul><li>Pour rechercher une partie d'un mot (ex. rechercher \"réa\" en souhaitant \"réagir\" ou \"réaction\"), ajoutez un * au terme recherché</li><li>En raison du nombre de résultats, les recherches sur deux lettres ou sur les mots communs tels que \"ce\", \"un\" et \"est\" n'apparaîtront pas dans les résultats de la recherche</li></ul>",
"search_results.noResults": "Aucun résultat. Recommencer ?",
- "search_results.usage": "<ul><li>Utilisez <b>\"des guillemets\"</b> pour rechercher des phrases</li><li>Utilisez <b>from:</b> pour rechercher les messages d'utilisateur spécifiques et <b>in:</b> pour rechercher les messages sur des canaux spécifiques</li></ul>",
- "search_results.usageFlag1": "Vous n'avez pas encore ajouté des indicateurs aux messages",
- "search_results.usageFlag2": "Vous pouvez ajouter un indicateur aux messages et aux commentaires en cliquant sur ",
+ "search_results.usage": "<ul><li>Utilisez <b>\"des guillemets\"</b> pour rechercher des phrases</li><li>Utilisez <b>from:</b> pour rechercher des messages d'utilisateurs spécifiques et <b>in:</b> pour rechercher des messages sur des canaux spécifiques</li></ul>",
+ "search_results.usageFlag1": "Vous n'avez pas encore de messages marqués d'un indicateur.",
+ "search_results.usageFlag2": "Vous pouvez marquer un message ou un commentaire en cliquant sur ",
"search_results.usageFlag3": " l'icône à côté de l'horodateur.",
- "search_results.usageFlag4": "Les indicateurs sont un moyen de marquer des messages de suivi. Vos indicateurs sont personnels et ne peuvent pas être vus par d'autres utilisateurs.",
+ "search_results.usageFlag4": "Marquer un message est un bon moyen d'assurer le suivi. Marquer un message est personnel et ne peut être vu par les autres utilisateurs.",
"setting_item_max.cancel": "Annuler",
"setting_item_max.save": "Enregistrer",
"setting_item_min.edit": "Modifier",
@@ -1751,26 +1776,26 @@
"sidebar.otherMembers": "En dehors de l’équipe",
"sidebar.pg": "Groupes privés",
"sidebar.removeList": "Retirer de la liste",
- "sidebar.tutorialScreen1": "<h4>Canaux</h4><p><strong>Les canaux</strong> organisent les conversations en sujets distincts. Ils sont ouverts à tout le monde dans votre équipe. Pour envoyer des messages privés, utilisez <strong>Messages Privés</strong> pour une personne ou <strong>Groupes Privés</strong> pour plusieurs personnes.</p>",
- "sidebar.tutorialScreen2": "<h4>Canaux \"{townsquare}\" et \"{offtopic}\"</h4><p>Voici deux canaux publics pour commencer :</p><p><strong>{townsquare}</strong> (\"centre-ville\") est l'endroit idéal pour communiquer avec toute l'équipe. Tous les membres de votre équipe sont membres de ce canal.</p><p><strong>{offtopic}</strong> (\"hors-sujet\") est l'endroit pour se détendre et parler d'autre chose que de travail. Vous et votre équipe décidez des autres canaux à créer.</p>",
+ "sidebar.tutorialScreen1": "<h4>Canaux</h4><p><strong>Les canaux</strong> organisent les conversations en sujets distincts. Ils sont ouverts à tous les utilisateurs de votre équipe. Pour envoyer des messages privés, utilisez <strong>Messages privés</strong> pour une personne ou <strong>Groupes privés</strong> pour plusieurs personnes.</p>",
+ "sidebar.tutorialScreen2": "<h4>Canaux \"{townsquare}\" et \"{offtopic}\"</h4><p>Voici deux canaux publics pour commencer :</p><p><strong>{townsquare}</strong> est l'endroit idéal pour communiquer avec toute l'équipe. Tous les membres de votre équipe sont membres de ce canal.</p><p><strong>{offtopic}</strong> (est l'endroit pour se détendre et parler d'autre chose que du travail. Vous et votre équipe décidez des autres canaux à créer.</p>",
"sidebar.tutorialScreen3": "<h4>Créer et rejoindre des canaux</h4><p>Cliquez sur <strong>\"Plus...\"</strong> pour créer un nouveau canal ou rejoindre un canal existant.</p><p>Vous pouvez aussi créer un nouveau canal ou un groupe privé en cliquant sur le symbole <strong>\"+\"</strong> à côté du nom du canal ou de l'en-tête du groupe privé.</p>",
"sidebar.unreadAbove": "Message(s) non-lu(s) ci-dessus",
"sidebar.unreadBelow": "Message(s) non-lu(s) ci-dessous",
- "sidebar_header.tutorial": "<h4>Menu principal</h4><p>Le <strong>Menu Principal</strong> est l'endroit où vous pouvez <strong>inviter de nouveaux membres</strong>, accéder aux <strong>paramètres de votre compte</strong> et configurer les <strong>couleurs de votre thème</strong>.</p><p>Les administrateurs d'équipe peuvent aussi accéder aux <strong>paramètres de l'équipe</strong>.</p><p>Les administrateurs système trouveront la <strong>console système</strong> pour administrer tout le site.</p>",
+ "sidebar_header.tutorial": "<h4>Menu principal</h4><p>Le <strong>Menu Principal</strong> est l'endroit où vous pouvez <strong>inviter des nouveaux membres</strong>, accéder aux <strong>paramètres de votre compte</strong> et configurer les <strong>couleurs de votre thème</strong>.</p><p>Les administrateurs d'équipe peuvent aussi accéder aux <strong>paramètres de l'équipe</strong>.</p><p>Les administrateurs système trouveront la <strong>console système</strong> pour administrer tout le site.</p>",
"sidebar_right_menu.accountSettings": "Paramètres du compte",
"sidebar_right_menu.console": "Console système",
- "sidebar_right_menu.flagged": "Postes avec indicateur",
+ "sidebar_right_menu.flagged": "Messages marqués d'un indicateur",
"sidebar_right_menu.help": "Aide",
- "sidebar_right_menu.inviteNew": "Inviter un nouveau membre",
+ "sidebar_right_menu.inviteNew": "Inviter un membre",
"sidebar_right_menu.logout": "Se déconnecter",
"sidebar_right_menu.manageMembers": "Gérer les membres",
- "sidebar_right_menu.nativeApps": "Télécharger les applications",
+ "sidebar_right_menu.nativeApps": "Télécharger les apps",
"sidebar_right_menu.recentMentions": "Mentions récentes",
"sidebar_right_menu.report": "Signaler un problème",
- "sidebar_right_menu.teamLink": "Obtenir un lien d'invitation d'équipe",
+ "sidebar_right_menu.teamLink": "Créer un lien d'invitation d'équipe",
"sidebar_right_menu.teamSettings": "Configuration de l'équipe",
"sidebar_right_menu.viewMembers": "Voir les membres",
- "signup.email": "Adresse électronique et mot de passe",
+ "signup.email": "Adresse e-mail et mot de passe",
"signup.gitlab": "Authentification unifiée avec GitLab",
"signup.google": "Compte Google",
"signup.ldap": "Informations d'authentification AD/LDAP",
@@ -1792,8 +1817,8 @@
"signup_user_completed.choosePwd": "Choisissez votre mot de passe",
"signup_user_completed.chooseUser": "Choisissez votre nom d'utilisateur",
"signup_user_completed.create": "Créer un compte",
- "signup_user_completed.emailHelp": "Une adresse électronique est obligatoire pour s'inscrire",
- "signup_user_completed.emailIs": "Votre adresse électronique est <strong>{email}</strong>. Vous utiliserez cette adresse pour vous connecter à {siteName}.",
+ "signup_user_completed.emailHelp": "Une adresse e-mail est obligatoire pour s'inscrire",
+ "signup_user_completed.emailIs": "Votre adresse e-mail est <strong>{email}</strong>. Vous utiliserez cette adresse pour vous connecter à {siteName}.",
"signup_user_completed.expired": "Vous avez déjà utilisé cette invitation pour vous inscrire, ou bien l'invitation a expiré.",
"signup_user_completed.gitlab": "avec GitLab",
"signup_user_completed.google": "avec Google",
@@ -1805,23 +1830,23 @@
"signup_user_completed.office365": "avec Office 365",
"signup_user_completed.onSite": "activé {siteName}",
"signup_user_completed.or": "ou",
- "signup_user_completed.passwordLength": "Veuillez entrer au moins {min} caractères",
+ "signup_user_completed.passwordLength": "Veuillez spécifier au moins {min} caractères",
"signup_user_completed.required": "Champ obligatoire",
"signup_user_completed.reserved": "Ce nom d'utilisateur est réservé, veuillez en choisir un autre.",
- "signup_user_completed.signIn": "Cliquez ici pour vous connecter.",
+ "signup_user_completed.signIn": "Veuillez cliquer ici pour vous connecter.",
"signup_user_completed.userHelp": "Les noms d'utilisateurs doivent commencer par une lettre et contenir entre {min} et {max} caractères composés de chiffres, lettres minuscules et des symboles '.', '-' et '_'",
"signup_user_completed.usernameLength": "Les noms d'utilisateurs doivent commencer par une lettre et contenir entre {min} et {max} caractères composés de chiffres, lettres minuscules et des symboles '.', '-' et '_'",
- "signup_user_completed.validEmail": "Veuillez entrer une adresse électronique valide",
+ "signup_user_completed.validEmail": "Veuillez spécifier une adresse e-mail valide",
"signup_user_completed.welcome": "Bienvenue sur :",
- "signup_user_completed.whatis": "Quelle est votre adresse électronique ?",
+ "signup_user_completed.whatis": "Quelle est votre adresse e-mail ?",
"signup_user_completed.withLdap": "Avec vos information d’identifications LDAP",
"sso_signup.find": "Trouver mes équipes",
"sso_signup.gitlab": "Créer une équipe avec un compte GitLab",
"sso_signup.google": "Créer une équipe avec un compte Google Apps",
"sso_signup.length_error": "Le nom doit contenir de 3 à 15 caractères",
"sso_signup.teamName": "Entrez le nom de votre nouvelle équipe",
- "sso_signup.team_error": "Saisissez le nom de votre équipe",
- "suggestion.mention.all": "Avertit tout le monde dans le canal, utilisez dans {townsquare} pour notifier toute l'équipe",
+ "sso_signup.team_error": "Veuillez spécifier le nom de votre équipe",
+ "suggestion.mention.all": "Avertit tout le monde dans le canal, utilisez-le dans {townsquare} pour notifier toute l'équipe",
"suggestion.mention.channel": "Notifier tout le monde dans le canal",
"suggestion.mention.channels": "Mes canaux",
"suggestion.mention.here": "Notifier toutes les personnes connectées dans ce canal",
@@ -1849,7 +1874,7 @@
"team_import_tab.importing": " Import...",
"team_import_tab.successful": " Import réussi : ",
"team_import_tab.summary": "Afficher le récapitulatif",
- "team_member_modal.close": "Quitter",
+ "team_member_modal.close": "Fermer",
"team_member_modal.members": "Membres de {team}",
"team_members_dropdown.confirmDemoteDescription": "Si vous vous retirez le rôle d'administrateur et qu'il n'y a aucun autre administrateur désigné, vous devrez en désigner un en utilisant les outils en ligne de commande depuis un terminal sur le serveur.",
"team_members_dropdown.confirmDemoteRoleTitle": "Confirmez le retrait de votre rôle d'administrateur",
@@ -1879,15 +1904,15 @@
"textbox.quote": ">citation",
"textbox.strike": "barré",
"tutorial_intro.allSet": "C'est parti !",
- "tutorial_intro.end": "Cliquez sur \"Suivant\" pour entrer dans {channel}. C'est le premier canal que les membres voient quand ils s'inscrivent. Utilisez-le pour poster des messages que tout le monde doit lire en premier.",
+ "tutorial_intro.end": "Veuillez cliquer sur \"Suivant\" pour entrer dans {channel}. Il s'agit du premier canal que les membres voient lorsqu'ils s'inscrivent. Utilisez-le pour poster des messages que tout le monde devrait lire en premier.",
"tutorial_intro.invite": "Inviter des membres",
- "tutorial_intro.mobileApps": "Installer les applications pour {link} pour un accès facile et des notifications en mobilité.",
+ "tutorial_intro.mobileApps": "Installez les applications pour {link} pour un accès facile et ainsi recevoir des notifications même lorsque vous êtes en déplacement.",
"tutorial_intro.mobileAppsLinkText": "PC, Mac, iOS et Android",
"tutorial_intro.next": "Suivant",
"tutorial_intro.screenOne": "<h3>Bienvenue dans :</h3><h1>Mattermost</h1><p>Toute la communication de votre équipe à un seul endroit, consultable instantanément et disponible partout.</p><p>Gardez votre équipe soudée et aider-la à accomplir les tâches qui importent vraiment.</p>",
"tutorial_intro.screenTwo": "<h3>Comment fonctionne Mattermost :</h3><p>Vous pouvez échanger dans des canaux publics, des groupes privés ou des messages privés.</p><p>Tout est archivé et peut être recherché depuis n'importe quel navigateur web de bureau, tablette ou mobile.</p>",
"tutorial_intro.skip": "Passer le tutoriel",
- "tutorial_intro.support": "Besoin de quoi que ce soit, envoyez-nous un e-mail à : ",
+ "tutorial_intro.support": "Vous avez besoin d'aide ? Envoyez-nous un e-mail à : ",
"tutorial_intro.teamInvite": "Inviter des collègues",
"tutorial_intro.whenReady": " quand vous serez prêt.",
"tutorial_tip.next": "Suivant",
@@ -1899,13 +1924,13 @@
"update_command.question": "Vos modifications peuvent casser la commande Slash existante. Voulez-vous vraiment la mettre à jour ?",
"update_command.update": "Mettre à jour",
"upload_overlay.info": "Faites glisser un fichier pour le télécharger.",
- "user.settings.advance.embed_preview": "Voir des aperçus des contenus des liens, si disponibles",
+ "user.settings.advance.embed_preview": "For the first web link in a message, display a preview of website content below the message, if available",
"user.settings.advance.embed_toggle": "Voir un aperçu pour tous les messages inclus",
"user.settings.advance.enabledFeatures": "{count, number} {count, plural, one {Feature} other {Features}} Activée",
"user.settings.advance.formattingDesc": "Si activé, les messages seront formatés pour créer des liens, montrer des emoji, le style du texte et ajouter des sauts de ligne. Par défaut, ce paramètre est activé. La modification de ce paramètre nécessite le rafraîchissement de la page.",
"user.settings.advance.formattingTitle": "Activé le formatage des messages",
"user.settings.advance.joinLeaveDesc": "Lorsqu'activé, les messages systèmes indiquant qu'un utilisateur s'est connecté ou a quitté un canal seront visibles. Lorsque désactivé, ces messages seront masqués. Un message sera toutefois toujours affiché lorsque vous êtes ajouté à un canal, de façon à ce que vous en soyez quand même informé.",
- "user.settings.advance.joinLeaveTitle": "Permettre de rejoindre/quitter les messages",
+ "user.settings.advance.joinLeaveTitle": "Active les messages indiquant qu'un utilisateur a rejoint/quitté le canal",
"user.settings.advance.markdown_preview": "Voir l'option d'aperçu du markdown dans la zone de saisie de message",
"user.settings.advance.off": "Désactivé",
"user.settings.advance.on": "Activé",
@@ -1913,15 +1938,15 @@
"user.settings.advance.preReleaseTitle": "Activer les fonctionnalités expérimentales",
"user.settings.advance.sendDesc": "Si activé, 'Entrée' insère une nouvelle ligne et 'Ctrl + Entrée' envoie le message.",
"user.settings.advance.sendTitle": "Envoyer vos messages avec Ctrl+Entrée",
- "user.settings.advance.slashCmd_autocmp": "Autoriser les applications externes à propose l'auto-complétion",
+ "user.settings.advance.slashCmd_autocmp": "Autoriser les applications externes à proposer l'auto-complétion des commandes slash",
"user.settings.advance.title": "Paramètres avancés",
"user.settings.advance.webrtc_preview": "Activer la possibilité de passer et de recevoir des appels WebRTC en tête-à-tête",
"user.settings.custom_theme.awayIndicator": "Indicateur \"absent\"",
"user.settings.custom_theme.buttonBg": "Fond de bouton",
"user.settings.custom_theme.buttonColor": "Texte de bouton",
- "user.settings.custom_theme.centerChannelBg": "Fond du canal central",
+ "user.settings.custom_theme.centerChannelBg": "Fond de l'espace central",
"user.settings.custom_theme.centerChannelColor": "Text du canal central",
- "user.settings.custom_theme.centerChannelTitle": "Styles du canal central",
+ "user.settings.custom_theme.centerChannelTitle": "Styles de l'espace central",
"user.settings.custom_theme.codeTheme": "Code du thème",
"user.settings.custom_theme.copyPaste": "Copiez/Collez pour partager les couleurs du thème :",
"user.settings.custom_theme.linkButtonTitle": "Styles des boutons et liens",
@@ -1942,13 +1967,13 @@
"user.settings.custom_theme.sidebarTitle": "Style des barres latérales",
"user.settings.custom_theme.sidebarUnreadText": "Texte non-lu de barre latérale",
"user.settings.display.channelDisplayTitle": "Mode d’affichage du canal",
- "user.settings.display.channeldisplaymode": "Sélectionner la largeur du canal central.",
+ "user.settings.display.channeldisplaymode": "Veuillez spécifier la largeur de l'espace central.",
"user.settings.display.clockDisplay": "Affichage de l'horloge",
- "user.settings.display.collapseDesc": "Développer les aperçus du contenu des liens, si disponibles.",
- "user.settings.display.collapseDisplay": "Aperçu des liens",
- "user.settings.display.collapseOff": "Désactivé",
- "user.settings.display.collapseOn": "Activé",
- "user.settings.display.fixedWidthCentered": "Largeur fixe, centrée",
+ "user.settings.display.collapseDesc": "Set whether previews of image links show as expanded or collapsed by default. This setting can also be controlled using the slash commands /expand and /collapse.",
+ "user.settings.display.collapseDisplay": "Default appearance of image link previews",
+ "user.settings.display.collapseOff": "Collapsed",
+ "user.settings.display.collapseOn": "Expanded",
+ "user.settings.display.fixedWidthCentered": "Largeur fixe, centré",
"user.settings.display.fontDesc": "Choisissez la police de caractères utilisée pour l'interface de Mattermost.",
"user.settings.display.fontTitle": "Police d'affichage",
"user.settings.display.fullScreen": "Largeur pleine",
@@ -1964,7 +1989,7 @@
"user.settings.display.normalClock": "Horloge 12 heures (exemple : 4:00 PM)",
"user.settings.display.preferTime": "Choisissez l'affichage des heures dans l'application.",
"user.settings.display.showFullname": "Afficher prénom et nom",
- "user.settings.display.showNickname": "Afficher le pseudo s'il existe, sinon afficher prénom et nom",
+ "user.settings.display.showNickname": "Afficher le pseudo s'il existe, sinon afficher le prénom d'abord puis le nom",
"user.settings.display.showUsername": "Afficher le nom d'utilisateur (défaut)",
"user.settings.display.teammateDisplay": "Affichage des membres de l'équipe",
"user.settings.display.theme.applyToAllTeams": "Appliquer le nouveau thème à toutes mes équipes",
@@ -1979,21 +2004,21 @@
"user.settings.general.checkEmailNoAddress": "Vérifiez votre boîte de réception pour valider votre nouvelle adresse e-mail.",
"user.settings.general.close": "Quitter",
"user.settings.general.confirmEmail": "E-mail de confirmation",
- "user.settings.general.email": "Adresse électronique",
+ "user.settings.general.email": "Adresse e-mail",
"user.settings.general.emailGitlabCantUpdate": "La connexion se produit par Gitlab. L'addresse Electronique ne peut pas être mis à jour . Adresse e-mail utilisée pour les notifications est {email} .",
- "user.settings.general.emailGoogleCantUpdate": "La connexion se produit par Gitlab. L'adresse Electronique ne peut pas être mise à jour. L'adresse électronique utilisée pour les notifications est {email} .",
- "user.settings.general.emailHelp1": "L'adresse électronique est utilisée pour la connexion, les notifications et la réinitialisation du mot de passe. Votre adresse électronique doit être validée si vous le changez.",
- "user.settings.general.emailHelp2": "L'envoi d'-emails a été désactivé par votre administrateur système. Aucune notification pare-mail ne peut être envoyée.",
- "user.settings.general.emailHelp3": "L'adresse électronique est utilisée pour la connexion, les notifications et la réinitialisation du mot de passe.",
+ "user.settings.general.emailGoogleCantUpdate": "La connexion s'effectue par Gitlab. L'adresse e-mail ne peut pas être mise à jour. L'adresse e-mail utilisée pour les notifications est {email} .",
+ "user.settings.general.emailHelp1": "L'adresse e-mail est utilisée pour la connexion, les notifications et la réinitialisation du mot de passe. Votre adresse e-mail doit être validée si vous la changez.",
+ "user.settings.general.emailHelp2": "L'envoi d'e-mails a été désactivé par votre administrateur système. Aucune notification par e-mail ne peut être envoyée.",
+ "user.settings.general.emailHelp3": "L'adresse e-mail est utilisée pour la connexion, les notifications et la réinitialisation du mot de passe.",
"user.settings.general.emailHelp4": "Un e-mail de vérification a été envoyé à {email}.",
- "user.settings.general.emailLdapCantUpdate": "La connexion s'effectue par le biais d'AD/LDAP. L'adresse e-mail ne peut pas être mise à jour. L'adresse e-mail utilisée pour les notifications est {email}.",
- "user.settings.general.emailMatch": "Les adresses électroniques que vous avez saisies ne correspondent pas.",
- "user.settings.general.emailOffice365CantUpdate": "La connexion se produit par Office 365. L'adresse électronique ne peut pas être mise à jour. L'adresse électronique utilisée pour les notifications est {email} .",
+ "user.settings.general.emailLdapCantUpdate": "La connexion s'effectue par AD/LDAP. L'adresse e-mail ne peut pas être mise à jour. L'adresse e-mail utilisée pour les notifications est {email}.",
+ "user.settings.general.emailMatch": "Les adresses e-mail que vous avez saisies ne correspondent pas.",
+ "user.settings.general.emailOffice365CantUpdate": "La connexion s'effectue par Office 365. L'adresse e-mail ne peut pas être mise à jour. L'adresse e-mail utilisée pour les notifications est {email} .",
"user.settings.general.emailSamlCantUpdate": "La connexion s'effectue par le biais de SAML. L'adresse e-mail ne peut pas être mise à jour. L'adresse e-mail utilisée pour les notifications est {email}.",
- "user.settings.general.emailUnchanged": "Votre nouvelle adresse électronique est la même que l'ancienne.",
- "user.settings.general.emptyName": "Cliquez sur ‘Modifier’ pour ajouter votre nom complet",
- "user.settings.general.emptyNickname": "Cliquez sur ‘Modifier’ pour ajouter un surnom",
- "user.settings.general.emptyPosition": "Cliquez sur 'Modifier' pour ajouter votre intitulé de poste / rôle",
+ "user.settings.general.emailUnchanged": "Votre nouvelle adresse e-mail est la même que l'ancienne.",
+ "user.settings.general.emptyName": "Veuillez cliquer sur ‘Modifier’ pour spécifier votre nom complet",
+ "user.settings.general.emptyNickname": "Veuillez cliquer sur ‘Modifier’ pour ajouter un surnom",
+ "user.settings.general.emptyPosition": "Veuillez cliquer sur 'Modifier' pour ajouter votre intitulé de poste / rôle",
"user.settings.general.field_handled_externally": "Ce champ est géré par le service d'authentification. Si vous souhaitez le modifier, vous devez le faire par le biais de votre service d'authentification.",
"user.settings.general.firstName": "Prénom",
"user.settings.general.fullName": "Nom complet",
@@ -2005,22 +2030,22 @@
"user.settings.general.loginLdap": "Connexion avec LDAP ({email})",
"user.settings.general.loginOffice365": "Dernière connexion avec Office 365 ({email})",
"user.settings.general.loginSaml": "Connexion avec SAML ({email})",
- "user.settings.general.newAddress": "Nouvelle adresse : {email}<br />Vérifiez votre messagerie pour valider votre adresse électronique.",
+ "user.settings.general.newAddress": "Nouvelle adresse : {email}<br />Vérifiez vos e-mails pour valider votre adresse e-mail.",
"user.settings.general.nickname": "Pseudo",
- "user.settings.general.nicknameExtra": "Vous pouvez utiliser un pseudo à la place de vos prénom, nom et nom d'utilisateur. Ceci est pratique lorsque deux personnes de votre équipe ont des noms proches.",
- "user.settings.general.notificationsExtra": "Par défaut, vous recevez une notification quand quelqu'un écrit votre prénom. Allez aux réglages {notify} pour modifier ce paramètre.",
+ "user.settings.general.nicknameExtra": "Vous pouvez utiliser un pseudo à la place de vos prénom, nom et nom d'utilisateur. Ceci est pratique lorsque deux personnes de votre équipe ont des noms similaires phonétiquement.",
+ "user.settings.general.notificationsExtra": "Par défaut, vous recevez une notification lorsqu'un utilisateur cite votre prénom. Rendez vous dans les réglages {notify} pour modifier ce paramètre.",
"user.settings.general.notificationsLink": "Notifications",
"user.settings.general.position": "Rôle",
- "user.settings.general.positionExtra": "Utilisez le champ poste pour votre rôle ou intitulé de poste. Il sera affiché dans votre infobulle de profil utilisateur.",
- "user.settings.general.primaryEmail": "Adresse de courrier électronique principale",
- "user.settings.general.profilePicture": "Photo du profil",
+ "user.settings.general.positionExtra": "Veuillez utiliser ce champ pour spécifier votre rôle ou intitulé de poste. Il sera affiché dans votre infobulle de profil utilisateur.",
+ "user.settings.general.primaryEmail": "Adresse e-mail principale",
+ "user.settings.general.profilePicture": "Photo de profil",
"user.settings.general.title": "Configuration générale",
- "user.settings.general.uploadImage": "Cliquez sur ‘Modifier’ pour télécharger une image",
+ "user.settings.general.uploadImage": "Veuillez cliquer sur ‘Modifier’ pour télécharger une image",
"user.settings.general.username": "Nom d'utilisateur",
- "user.settings.general.usernameInfo": "Choisissez quelque chose de facile à se souvenir pour vos collègues.",
+ "user.settings.general.usernameInfo": "Veuillez spécifier un nom d'utilisateur facile à reconnaître et à mémoriser pour vos collègues.",
"user.settings.general.usernameReserved": "Ce nom est réservé, veuillez en choisir un autre.",
"user.settings.general.usernameRestrictions": "Les noms d'utilisateurs doivent commencer par une lettre et contenir entre {min} et {max} caractères composés de chiffres, lettres minuscules et des symboles '.', '-' et '_'.",
- "user.settings.general.validEmail": "Veuillez entrer une adresse électronique valide",
+ "user.settings.general.validEmail": "Veuillez spécifier une adresse e-mail valide",
"user.settings.general.validImage": "Seules les images JPG ou PNG sont autorisées pour les photos de profil",
"user.settings.import_theme.cancel": "Annuler",
"user.settings.import_theme.importBody": "Pour importer un thème, rendez-vous sur une Slack team et cliquez sur \"Preferences -> Sidebar Theme\". Ouvrez la fenêtre de personnalisation, copiez les couleurs du thèmes et collez-les ici :",
@@ -2053,10 +2078,10 @@
"user.settings.notifications.channelWide": "Mentions globales \"@channel\", \"@all\"",
"user.settings.notifications.close": "Quitter",
"user.settings.notifications.comments": "Notifications de réponse",
- "user.settings.notifications.commentsAny": "Déclencher des notifications sur les messages de fils de réponse que je débute ou dans lesquels je participe",
- "user.settings.notifications.commentsInfo": "En plus des notifications qui apparaissent lorsque vous êtes mentionné, vous pouvez choisir de recevoir, en plus, des notifications pour les réponses aux fils que vous avez lancés.",
- "user.settings.notifications.commentsNever": "Ne pas déclencher de notifications sur les messages dans les fils de réponse à moins que je ne sois mentionné",
- "user.settings.notifications.commentsRoot": "Déclencher des notifications pour les messages dans les discussions que j'ai commencé",
+ "user.settings.notifications.commentsAny": "Recevoir des notifications sur les messages de fils de réponse que je débute ou dans lesquels je participe",
+ "user.settings.notifications.commentsInfo": "En plus des notifications que vous recevez lorsque vous êtes mentionné, vous pouvez choisir de recevoir des notifications pour chaque réponse apportée à un message que vous avez envoyé.",
+ "user.settings.notifications.commentsNever": "Ne pas recevoir de notifications pour toutes réponses apportées à un fil de réponses à moins que je ne sois mentionné",
+ "user.settings.notifications.commentsRoot": "Recevoir des notifications pour pour toutes réponses apportées à un fil de messages que j'ai débuté",
"user.settings.notifications.desktop": "Envoyer des notifications sur le bureau",
"user.settings.notifications.desktop.allFirefoxForever": "Pour toute activité, visible indéfiniment",
"user.settings.notifications.desktop.allFirefoxTimed": "Pour toute activité, visible {seconds} secondes",
@@ -2124,20 +2149,22 @@
"user.settings.security.currentPassword": "Mot de passe actuel",
"user.settings.security.currentPasswordError": "Veuillez saisir votre mot de passe actuel",
"user.settings.security.deauthorize": "Supprimer l'autorisation",
- "user.settings.security.emailPwd": "Adresse électronique et mot de passe",
+ "user.settings.security.emailPwd": "Adresse e-mail et mot de passe",
"user.settings.security.gitlab": "GitLab",
"user.settings.security.google": "Google",
"user.settings.security.lastUpdated": "Dernière mise à jour le {date} à {time}",
"user.settings.security.ldap": "AD/LDAP",
"user.settings.security.loginGitlab": "Connexion avec GitLab",
+ "user.settings.security.loginGoogle": "Login done through Google Apps",
"user.settings.security.loginLdap": "Connexion avec LDAP",
+ "user.settings.security.loginOffice365": "Login done through Office 365",
"user.settings.security.loginSaml": "Se connecter par SAML",
"user.settings.security.logoutActiveSessions": "Consulter et déconnecter les sessions actives",
"user.settings.security.method": "Méthode de connexion",
"user.settings.security.newPassword": "Nouveau mot de passe",
"user.settings.security.noApps": "Aucune application OAuth 2.0 autorisée.",
"user.settings.security.oauthApps": "Applications OAuth 2.0",
- "user.settings.security.oauthAppsDescription": "Cliquez sur \"Modifier\" pour gérer votre Application OAuth 2.0",
+ "user.settings.security.oauthAppsDescription": "Veuillez cliquer sur 'Modifier' pour gérer vos applications OAuth 2.0",
"user.settings.security.oauthAppsHelp": "Les applications agissent en votre nom pour accéder à vos données sur la base des autorisations que vous leur accordez.",
"user.settings.security.office365": "Office 365",
"user.settings.security.oneSignin": "Vous ne pouvez avoir qu'une seule méthode de connexion à la fois. Changer de méthode de connexion provoquera l'envoi d'un e-mail vous notifiant que le changement a réussi.",
@@ -2159,13 +2186,15 @@
"user.settings.security.passwordErrorUppercaseNumberSymbol": "Votre mot de passe doit contenir au moins {min} caractères et au moins une lettre majuscule, un chiffre et un symbole (parmi \"~!@#$%^&*()\").",
"user.settings.security.passwordErrorUppercaseSymbol": "Votre mot de passe doit contenir au moins {min} caractères et au moins une lettre majuscule et un symbole (parmi \"~!@#$%^&*()\").",
"user.settings.security.passwordGitlabCantUpdate": "La connexion se produit à travers GitLab. Le mot de passe ne peut pas être mis à jour.",
+ "user.settings.security.passwordGoogleCantUpdate": "La connexion se produit à travers GitLab. Le mot de passe ne peut pas être mis à jour.",
"user.settings.security.passwordLdapCantUpdate": "La connexion se produit via LDAP . Le mot de passe ne peut pas être mis à jour.",
"user.settings.security.passwordMatchError": "Les nouveaux mots de passe que vous avez saisis ne correspondent pas.",
"user.settings.security.passwordMinLength": "Longueur minimum invalide, impossible d'afficher l'aperçu.",
+ "user.settings.security.passwordOffice365CantUpdate": "La connexion se produit à travers GitLab. Le mot de passe ne peut pas être mis à jour.",
"user.settings.security.passwordSamlCantUpdate": "Ce champ est géré par le service d'authentification. Si vous souhaitez le modifier, vous devez le faire par le biais de votre service d'authentification.",
- "user.settings.security.retypePassword": "Saisissez le nouveau mot de passe",
+ "user.settings.security.retypePassword": "Répéter le nouveau mot de passe",
"user.settings.security.saml": "SAML",
- "user.settings.security.switchEmail": "Utilisation de l'adresse électronique et du mot de passe",
+ "user.settings.security.switchEmail": "Utilisation de l'adresse e-mail et du mot de passe",
"user.settings.security.switchGitlab": "Utiliser GitLab SSO",
"user.settings.security.switchGoogle": "Utiliser Google SSO",
"user.settings.security.switchLdap": "Changer pour utiliser LDAP",
diff --git a/webapp/i18n/ja.json b/webapp/i18n/ja.json
index fbf60c1cc..ddc8fc243 100644
--- a/webapp/i18n/ja.json
+++ b/webapp/i18n/ja.json
@@ -28,6 +28,7 @@
"activity_log.sessionsDescription": "セッションは新しいブラウザーまたはデバイスからログインからログインした時に作成されます。Mattermostはシステム管理者が指定した期間内であればログインし直すことなく使用できます。すぐにログアウトしたい場合には、「ログアウト」ボタンを使用することで、セッションを終了させることができます。",
"activity_log_modal.android": "Android",
"activity_log_modal.androidNativeApp": "Androidネイティブアプリ",
+ "activity_log_modal.desktop": "ネイティブデスクトップアプリ",
"activity_log_modal.iphoneNativeApp": "iPhoneネイティブアプリ",
"add_command.autocomplete": "自動補完",
"add_command.autocomplete.help": "(オプション) 自動補完リストにスラッシュコマンドを表示する。",
@@ -260,6 +261,7 @@
"admin.email.notificationEmailTitle": "通知電子メールでの電子メールアドレス:",
"admin.email.notificationOrganization": "通知のフッターの住所:",
"admin.email.notificationOrganizationDescription": "Mattermostからの通知電子メールに表示する組織名と住所をを設定します。例: \"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\"。この欄を空欄にすると、組織名と住所は表示されません。",
+ "admin.email.notificationOrganizationExample": "例: \"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\"",
"admin.email.notificationsDescription": "本番環境では有効に設定してください。有効な場合、Mattermostは電子メールによる通知を送信します。開発者は無効に設定することで、電子メールによる通知を省略し、開発をより早く進められるようにできます。<br/>また、有効に設定することで、プレビューモードのバナーが表示されなくなります(この設定を反映するには再度ログインし直してください)。",
"admin.email.notificationsTitle": "通知電子メールを有効にする: ",
"admin.email.passwordSaltDescription": "パスワード初期化の電子メールの署名に32文字のソルトを付与します。これはインストールするたびにランダムに生成されます。新しいソルトを生成するには「再生成する」をクリックしてください。",
@@ -308,10 +310,20 @@
"admin.general.localization.serverLocaleTitle": "デフォルトのサーバー言語:",
"admin.general.log": "ログ",
"admin.general.policy": "ポリシー",
+ "admin.general.policy.allowEditPostAlways": "いつでも",
+ "admin.general.policy.allowEditPostDescription": "投稿した後にメッセージを編集できる期間についてのポリシーを設定してください。",
+ "admin.general.policy.allowEditPostNever": "できない",
+ "admin.general.policy.allowEditPostTimeLimit": "投稿後数秒",
+ "admin.general.policy.allowEditPostTitle": "メッセージの編集ができるユーザー:",
"admin.general.policy.permissionsAdmin": "チーム管理者とシステム管理者",
"admin.general.policy.permissionsAll": "全てのチームメンバー",
"admin.general.policy.permissionsAllChannel": "全てのチャンネルのメンバー",
+ "admin.general.policy.permissionsDeletePostAdmin": "チーム管理者とシステム管理者",
+ "admin.general.policy.permissionsDeletePostAll": "メッセージの作成者は自身のメッセージを削除できます。また、管理者はどんなメッセージも削除することができます。",
+ "admin.general.policy.permissionsDeletePostSystemAdmin": "システム管理者",
"admin.general.policy.permissionsSystemAdmin": "システム管理者",
+ "admin.general.policy.restrictPostDeleteDescription": "メッセージを削除できる権限を持つユーザーについてのポリシーを設定してください。",
+ "admin.general.policy.restrictPostDeleteTitle": "メッセージの削除ができるユーザー:",
"admin.general.policy.restrictPrivateChannelCreationDescription": "非公開グループの作成ができるユーザーについてのポリシーを設定してください。",
"admin.general.policy.restrictPrivateChannelCreationTitle": "非公開グループの作成を許可する:",
"admin.general.policy.restrictPrivateChannelDeletionCommandLineToolLink": "コマンドラインツール",
@@ -363,6 +375,7 @@
"admin.image.amazonS3BucketExample": "例: \"mattermost-media\"",
"admin.image.amazonS3BucketTitle": "Amazon S3バケット:",
"admin.image.amazonS3EndpointDescription": "S3と互換性のあるストレージプロバイダーのホスト名です。デフォルトは`s3.amazonaws.com`です。",
+ "admin.image.amazonS3EndpointExample": "例: \"s3.amazonaws.com\"",
"admin.image.amazonS3EndpointTitle": "Amazon S3 エンドポイント:",
"admin.image.amazonS3IdDescription": "Amazon EC2の管理者から認証情報を入手してください。",
"admin.image.amazonS3IdExample": "例: \"AKIADTOVBGERKLCBV\"",
@@ -564,7 +577,7 @@
"admin.rate.enableLimiterTitle": "投稿頻度制限を有効にする: ",
"admin.rate.httpHeaderDescription": "入力した場合、指定されたHTTPヘッダーフィールド(例えば、NGINXで設定する場合には\"X-Real-IP\"、AmazonELBの場合には\"X-Forwarded-For\")で投稿頻度制限を変更できます。",
"admin.rate.httpHeaderExample": "例: \"X-Real-IP\", \"X-Forwarded-For\"",
- "admin.rate.httpHeaderTitle": "HTTPヘッダーで投稿頻度制限を変更",
+ "admin.rate.httpHeaderTitle": "HTTPヘッダーで投稿頻度制限を変更:",
"admin.rate.maxBurst": "最大バーストサイズ:",
"admin.rate.maxBurstDescription": "1秒間当たりのクエリーの上限数を指定してください。",
"admin.rate.maxBurstExample": "例: \"100\"",
@@ -597,7 +610,7 @@
"admin.saml.assertionConsumerServiceURLDesc": "https://<your-mattermost-url>/login/sso/saml を入力してください。HTTPとHTTPSのどちらを使うか注意してください。この入力欄は、アサーションコンシューマーサービスURLとも呼ばれます。",
"admin.saml.assertionConsumerServiceURLEx": "例: \"https://<your-mattermost-url>/login/sso/saml\"",
"admin.saml.assertionConsumerServiceURLTitle": "サービスプロバイダーログインURL:",
- "admin.saml.bannerDesc": "User attributes in SAML server, including user deactivation or removal, are updated in Mattermost during user login. Learn more at: <a href=\"https://docs.mattermost.com/deployment/sso-saml.html\">https://docs.mattermost.com/deployment/sso-saml.html</a>",
+ "admin.saml.bannerDesc": "非活性もしくは削除されたユーザーを含めSAMLサーバーのユーザー属性がログイン中に更新されました。詳細について: <a href=\"https://docs.mattermost.com/deployment/sso-saml.html\">https://docs.mattermost.com/deployment/sso-saml.html</a>",
"admin.saml.emailAttrDesc": "Mattermostのユーザーの電子メールアドレスを設定するために使用されるSAMLアサーションの属性値です。",
"admin.saml.emailAttrEx": "例: \"Email\"または\"PrimaryEmail\"",
"admin.saml.emailAttrTitle": "電子メール属性値:",
@@ -831,10 +844,10 @@
"admin.team.dirDesc": "有効な場合、チーム一覧に表示されるチームが、新しいチーム作成の代わりに、メインページに表示されます。",
"admin.team.dirTitle": "チーム一覧を有効にする: ",
"admin.team.maxChannelsDescription": "チーム毎のチャンネル数合計の最大値です。アクティブなチャンネルと削除済みのチャンネルの両方が数えられます。",
- "admin.team.maxChannelsExample": "例 \"100\"",
+ "admin.team.maxChannelsExample": "例: \"100\"",
"admin.team.maxChannelsTitle": "チーム毎の最大チャンネル数:",
"admin.team.maxNotificationsPerChannelDescription": "パフォーマンスの問題によりユーザーがメッセージ、@all、@here、@channelを入力してもが通知を送信されなくなるチャンネル内のユーザーの最大数です。",
- "admin.team.maxNotificationsPerChannelExample": "例 \"1000\"",
+ "admin.team.maxNotificationsPerChannelExample": "例: \"10000\"",
"admin.team.maxNotificationsPerChannelTitle": "チャンネルごとの最大通知数:",
"admin.team.maxUsersDescription": "チーム毎のユーザー数合計の最大値です。有効なユーザーと無効なユーザーの両方が数えられます。",
"admin.team.maxUsersExample": "例: \"25\"",
@@ -844,7 +857,7 @@
"admin.team.openServerTitle": "オープンサーバーを有効にする: ",
"admin.team.restrictDescription": "特定のドメインからだけチームとユーザーアカウントの作成を可能にします。単数(例: \"mattermost.org\")でもカンマ区切りで複数(例: \"corp.mattermost.com, mattermost.org\")でも指定できます。",
"admin.team.restrictDirectMessage": "ダイレクトメッセージの対象範囲:",
- "admin.team.restrictDirectMessageDesc": "'Mattermostの全てのユーザー'はチームに属していないユーザーへのダイレクトメッセージのチャンネルを利用することが出来ます。'このチームのメンバー'では同じチームに属しているユーザーに制限されます。",
+ "admin.team.restrictDirectMessageDesc": "'Mattermostの全てのユーザー'はチームに属していないユーザーへのダイレクトメッセージのチャンネルを利用することが出来ます。'チームのメンバーのみ'では同じチームに属しているユーザーに制限されます。",
"admin.team.restrictExample": "例: \"corp.mattermost.com, mattermost.org\"",
"admin.team.restrictNameDesc": "有効な場合、www、admin、support、test、channelなど予約された名前を持つチームは作成できません。",
"admin.team.restrictNameTitle": "予約されたチーム名: ",
@@ -901,13 +914,13 @@
"admin.webrtc.gatewayWebsocketUrlTitle": "ゲートウェイウェブソケットURL:",
"admin.webrtc.stunUriDescription": "stun:<your-stun-url>:<port> のようにSTUN URIを入力してください。STUNは、デバイスがNATの背後に置かれていた場合に、パブリックなIPアドレスへのアクセスをエンドホストがアシストすることを許可するために使われる標準的なネットワークプロトコルです。",
"admin.webrtc.stunUriExample": "例: \"stun:webrtc.mattermost.com:5349\"",
- "admin.webrtc.stunUriTitle": "STUN URI",
+ "admin.webrtc.stunUriTitle": "STUN URI:",
"admin.webrtc.turnSharedKeyDescription": "TURNサーバー共通鍵を入力してください。これは接続を確立するための動的なパスワードを生成するために使用されます。それぞれのパスワードが有効なのは短期間です。",
"admin.webrtc.turnSharedKeyExample": "例: \"bXdkOWQxc3d0Ynk3emY5ZmsxZ3NtazRjaWg=\"",
"admin.webrtc.turnSharedKeyTitle": "TURN共通鍵:",
"admin.webrtc.turnUriDescription": "turn:<your-turn-url>:<port> のようにTURN URIを入力してください。TURNは、デバイスが対称型NATの背後に置かれていた場合に、リレー用パブリックIPアドレスを使用することによって接続を確立することを、エンドホストがアシストするための標準的なネットワークプロトコルです。",
"admin.webrtc.turnUriExample": "例: \"turn:webrtc.mattermost.com:5349\"",
- "admin.webrtc.turnUriTitle": "TURN URI",
+ "admin.webrtc.turnUriTitle": "TURN URI:",
"admin.webrtc.turnUsernameDescription": "TURNサーバーユーザー名を入力してください。",
"admin.webrtc.turnUsernameExample": "例: \"myusername\"",
"admin.webrtc.turnUsernameTitle": "TURNユーザー名:",
@@ -923,12 +936,14 @@
"analytics.chart.meaningful": "意味のある表示のための十分なデータがありません。",
"analytics.system.activeUsers": "投稿実績のあるアクティブユーザー",
"analytics.system.channelTypes": "チャンネル形式",
+ "analytics.system.dailyActiveUsers": "日次アクティブユーザー",
"analytics.system.expiredBanner": "エンタープライズライセンスは{date}に期限が切れました。この日から15日以内にライセンスを更新してください。問い合わせは<a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>までお願いします。",
"analytics.system.expiringBanner": "エンタープライズライセンスは{date}に期限が切れます。ライセンスを更新するには、<a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>に問い合わせてください。",
+ "analytics.system.monthlyActiveUsers": "月次アクティブユーザー",
"analytics.system.postTypes": "投稿、ファイル、ハッシュタグ",
"analytics.system.privateGroups": "非公開グループ",
"analytics.system.publicChannels": "公開チャンネル",
- "analytics.system.skippedIntensiveQueries": "To maximize performance, some statistics are disabled. You can re-enable them in config.json. See: <a href='https://docs.mattermost.com/administration/statistics.html' target='_blank'>https://docs.mattermost.com/administration/statistics.html</a>",
+ "analytics.system.skippedIntensiveQueries": "パフォーマンスを最大にするために無効化された統計情報があります。config.jsonで再び有効化にすることができます。<a href='https://docs.mattermost.com/administration/statistics.html' target='_blank'>https://docs.mattermost.com/administration/statistics.html</a>を参照してください。",
"analytics.system.textPosts": "テキストのみの投稿数",
"analytics.system.title": "システムの使用統計",
"analytics.system.totalChannels": "総チャンネル数",
@@ -1022,7 +1037,6 @@
"backstage_sidebar.integrations.outgoing_webhooks": "外向きのウェブフック",
"calling_screen": "呼び出し中",
"center_panel.recent": "ここをクリックして最近のメッセージへ移動します。 ",
- "chanel_header.addMembers": "メンバーを追加する",
"change_url.close": "閉じる",
"change_url.endWithLetter": "英数字で終わらせてください",
"change_url.invalidUrl": "不正なURL",
@@ -1040,6 +1054,7 @@
"channel_flow.handleTooShort": "チャンネルURLは2文字以上の小文字の英数字にしてください",
"channel_flow.invalidName": "不正なチャンネル名です",
"channel_flow.set_url_title": "{term}のURLを設定する",
+ "channel_header.addMembers": "メンバーを追加する",
"channel_header.addToFavorites": "お気に入りに追加する",
"channel_header.channel": "チャンネル",
"channel_header.channelHeader": "チャンネルヘッダーを編集する",
@@ -1079,10 +1094,14 @@
"channel_loader.uploadedFile": " がファイルをアップロードしました",
"channel_loader.uploadedImage": " が画像をアップロードしました",
"channel_loader.wrote": " が書きました: ",
+ "channel_members_dropdown.channel_admin": "チャンネル管理者",
+ "channel_members_dropdown.channel_member": "チャンネルのメンバー",
+ "channel_members_dropdown.make_channel_admin": "チャンネル管理者を作成する",
+ "channel_members_dropdown.make_channel_member": "チャンネルメンバーを作成する",
+ "channel_members_dropdown.remove_from_channel": "チャンネルから削除する",
+ "channel_members_dropdown.remove_member": "メンバーを削除メンバーを削除する",
"channel_members_modal.addNew": " 新しいメンバーを追加する",
- "channel_members_modal.close": "閉じる",
- "channel_members_modal.remove": "削除する",
- "channel_memebers_modal.members": " メンバー",
+ "channel_members_modal.members": " メンバー",
"channel_modal.cancel": "キャンセル",
"channel_modal.channel": "チャンネル",
"channel_modal.createNew": "新規 ",
@@ -1091,6 +1110,7 @@
"channel_modal.edit": "編集する",
"channel_modal.group": "グループ",
"channel_modal.header": "ヘッダー",
+ "channel_modal.headerEx": "例: \"[Link Title](http://example.com)\"",
"channel_modal.headerHelp": "{term}名の近くの{term}のヘッダー部分に表示されるテキストを設定してください。例えば、よく入力されるリンク [リンクのタイトル](http://example.com) などを含めてください。",
"channel_modal.modalTitle": "新規 ",
"channel_modal.name": "名前",
@@ -1101,6 +1121,7 @@
"channel_modal.publicChannel1": "公開チャンネルを作成する",
"channel_modal.publicChannel2": "新しい誰でも参加できる公開チャンネルを作成します。 ",
"channel_modal.purpose": "目的",
+ "channel_modal.purposeEx": "例: \"バグや改善を取りまとめるチャンネル\"",
"channel_notifications.allActivity": "全てのアクティビティーについて",
"channel_notifications.allUnread": "全ての未読のメッセージについて",
"channel_notifications.globalDefault": "システム全体のデフォルト({notifyLevel})",
@@ -1113,6 +1134,7 @@
"channel_notifications.unreadInfo": "未読のメッセージがある場合、チャンネル名がサイドバーに太字で表示されます。「あなたについての投稿のみ」を選択することで、あなたについての投稿がある場合のみ太字で表示されます。",
"channel_select.placeholder": "--- チャンネルを選択してください ---",
"channel_switch_modal.dm": "(ダイレクトメッセージ)",
+ "channel_switch_modal.failed_to_open": "チャンネルを開けませんでした。",
"channel_switch_modal.help": "チャンネル名を入力してください。↑↓ で閲覧、TAB で選択、↵ で切り替え、ESC でキャンセル",
"channel_switch_modal.not_found": "一致するものは見つかりませんでした。",
"channel_switch_modal.submit": "切り替える",
@@ -1285,15 +1307,14 @@
"find_team.submitError": "有効な電子メールアドレスを入力してください",
"flag_post.flag": "追跡フラグ",
"flag_post.unflag": "フラグを消す",
+ "general_tab.chooseDescription": "あなたのチームの新しい説明を選択してください",
"general_tab.chooseName": "あなたのチームの新しい名称を選択してください",
"general_tab.codeDesc": "招待コードを再生成するには「編集」をクリックしてください。",
- "general_tab.codeLongDesc": "招待コードは、<strong>チーム招待リンクを入手</strong>で作成されたチーム招待リンクのURLの一部として使われます。再生成することで新しいチーム招待リンクが作成され、古いリンクは無効化されます。",
+ "general_tab.codeLongDesc": "招待コードは、メインメニューの {getTeamInviteLink} で作成されたチーム招待リンクのURLの一部として使われます。再生成することで新しいチーム招待リンクが作成され、古いリンクは無効化されます。",
"general_tab.codeTitle": "招待コード",
- "general_tab.dirDisabled": "チーム一覧が無効になっています。システム管理者にシステムコンソールのチームの設定でチーム一覧を有効化するように依頼してください。",
- "general_tab.dirOff": "このシステムでは、チーム一覧は無効になっています。",
"general_tab.emptyDescription": "チームの説明を追加するには「編集する」をクリックしてください。",
+ "general_tab.getTeamInviteLink": "チーム招待リンクを入手",
"general_tab.includeDirDesc": "ホームページのチーム一覧にチーム名が表示され、サインインページへのリンクが提供されます。",
- "general_tab.includeDirTitle": "チーム一覧に追加する",
"general_tab.no": "いいえ",
"general_tab.openInviteDesc": "許可された場合、このチームへのリンクは、このチームに誰もが参加できるようにランディングページに含まれます。",
"general_tab.openInviteTitle": "このサーバーにアカウントを持つ全てのユーザーが、このチームに参加できるようにする",
@@ -1550,6 +1571,7 @@
"login.passwordChanged": " パスワードは正常に更新されました",
"login.session_expired": " あなたのセッションは有効期限が切れました。再度ログインしてください。",
"login.signIn": "サインイン",
+ "login.signInLoading": "サインイン中です...",
"login.signInWith": "サインイン方法:",
"login.userNotFound": "あなたのログイン情報に合致するアカウントはありません。",
"login.username": "ユーザー名",
@@ -1561,8 +1583,10 @@
"member_item.makeAdmin": "管理者にする",
"member_item.member": "メンバー",
"member_list.noUsersAdd": "ユーザーは追加されません。",
+ "members_popover.manageMembers": "メンバーを管理する",
"members_popover.msg": "メッセージ",
"members_popover.title": "メンバー",
+ "members_popover.viewMembers": "メンバーを見る",
"mfa.confirm.complete": "<strong>セットアップが完了しました!</strong>",
"mfa.confirm.okay": "OK",
"mfa.confirm.secure": "あなたのアカウントは安全です。次にサインインするとき、あなたはGoogle Authenticatorからのコードの入力を求められるでしょう。",
@@ -1665,6 +1689,7 @@
"post_info.mobile.unflag": "フラグを消す",
"post_info.permalink": "パーマリンク",
"post_info.reply": "返信する",
+ "post_message_view.edited": "(編集済)",
"posts_view.loadMore": "もっとメッセージを読み込む",
"posts_view.newMsg": "新しいメッセージ",
"posts_view.newMsgBelow": "以下に新しい {count, plural, one {message} other {messages}}があります",
@@ -1899,7 +1924,7 @@
"update_command.question": "あなたの変更により既存のコマンドが動作しなくなる恐れがあります。本当に更新しますか?",
"update_command.update": "更新",
"upload_overlay.info": "ファイルをアップロードするためにドラッグアンドドロップします。",
- "user.settings.advance.embed_preview": "利用可能な場合、リンク先の内容を試験的にプレビューする",
+ "user.settings.advance.embed_preview": "メッセージ内の最初のWebのリンクについて、可能ならばそのメッセージの下にWebサイトの内容のプレビューを表示します",
"user.settings.advance.embed_toggle": "全ての埋め込まれたプレビューの表示非表示を切り替える",
"user.settings.advance.enabledFeatures": "{count, number} {count, plural, one {Feature} other {Features}}が有効化されました",
"user.settings.advance.formattingDesc": "オンにした場合、投稿は、リンクを作成したり、絵文字を表示したり、テキストに書式を設定したり、改行したりされます。デフォルトではオンに設定されています。この設定を変更した場合には、ページを再読み込みしてください。",
@@ -1944,10 +1969,10 @@
"user.settings.display.channelDisplayTitle": "チャンネル表示",
"user.settings.display.channeldisplaymode": "中央のチャンネルの幅を選択してください。",
"user.settings.display.clockDisplay": "時計表示",
- "user.settings.display.collapseDesc": "利用可能な場合、内容のプレビューを表示するようにリンクを拡張します。",
- "user.settings.display.collapseDisplay": "リンクのプレビュー",
- "user.settings.display.collapseOff": "オフ",
- "user.settings.display.collapseOn": "オン",
+ "user.settings.display.collapseDesc": "画像リンクのプレビューが展開して表示されるか折り畳んで表示されるかを設定してください。この設定はスラッシュコマンドの /expand と /collapse を使用して制御することもできます。",
+ "user.settings.display.collapseDisplay": "画像リンクプレビューのデフォルト表示",
+ "user.settings.display.collapseOff": "折り畳まれる",
+ "user.settings.display.collapseOn": "展開される",
"user.settings.display.fixedWidthCentered": "固定幅、中央寄せ",
"user.settings.display.fontDesc": "Mattermostユーザーインターフェイスで使うフォントを選択してください。",
"user.settings.display.fontTitle": "表示フォント",
@@ -2130,7 +2155,9 @@
"user.settings.security.lastUpdated": "最終更新: {date} {time}",
"user.settings.security.ldap": "AD/LDAP",
"user.settings.security.loginGitlab": "GitLabでログインしました",
+ "user.settings.security.loginGoogle": "Google Appsでログインしました",
"user.settings.security.loginLdap": "AD/LDAPでログインする",
+ "user.settings.security.loginOffice365": "Office 365でログインしました",
"user.settings.security.loginSaml": "SAMLでログインしました",
"user.settings.security.logoutActiveSessions": "アクティブなセッションを見てログアウトする",
"user.settings.security.method": "サインイン方法",
@@ -2159,9 +2186,11 @@
"user.settings.security.passwordErrorUppercaseNumberSymbol": "パスワードは少なくとも{min}文字以上にしてください。少なくとも1つの英大文字と数字、記号 (例: \"~!@#$%^&*()\") も必要です。",
"user.settings.security.passwordErrorUppercaseSymbol": "パスワードは少なくとも{min}文字以上にしてください。少なくとも1つの英大文字と記号 (例: \"~!@#$%^&*()\") も必要です。",
"user.settings.security.passwordGitlabCantUpdate": "GitLabでログインしています。パスワードは更新できません。",
+ "user.settings.security.passwordGoogleCantUpdate": "GitLabでログインしています。パスワードは更新できません。",
"user.settings.security.passwordLdapCantUpdate": "AD/LDAPでログインしています。パスワードは更新できません。",
"user.settings.security.passwordMatchError": "あなたの入力した新しいパスワードが一致しません。",
"user.settings.security.passwordMinLength": "最小の長さが不正です。プレビューを表示できません。",
+ "user.settings.security.passwordOffice365CantUpdate": "GitLabでログインしています。パスワードは更新できません。",
"user.settings.security.passwordSamlCantUpdate": "この欄はあなたのログインプロバイダーで使用されます。変更したい場合、ログインプロバイダーを通じて変更してください。",
"user.settings.security.retypePassword": "新しいパスワードをもう一度入力してください",
"user.settings.security.saml": "SAML",
diff --git a/webapp/i18n/ko.json b/webapp/i18n/ko.json
index 50b3c387f..bb73f0583 100644
--- a/webapp/i18n/ko.json
+++ b/webapp/i18n/ko.json
@@ -28,6 +28,7 @@
"activity_log.sessionsDescription": "세션은 기기의 새 브라우저로 로그인할때 생성됩니다. 세션을 사용하면 시스템에서 정한 시간동안은 다시 로그인할 필요가 없습니다. '로그아웃' 버튼을 사용해서 세션을 종료할 수 있습니다.",
"activity_log_modal.android": "안드로이드",
"activity_log_modal.androidNativeApp": "안드로이드 앱",
+ "activity_log_modal.desktop": "Native Desktop App",
"activity_log_modal.iphoneNativeApp": "아이폰 앱",
"add_command.autocomplete": "자동완성",
"add_command.autocomplete.help": "(선택사항) 명령어가 자동완성 목록에서 보이게 합니다.",
@@ -260,6 +261,7 @@
"admin.email.notificationEmailTitle": "알림 이메일 주소:",
"admin.email.notificationOrganization": "Notification Footer Mailing Address:",
"admin.email.notificationOrganizationDescription": "Organization name and address displayed on email notifications from Mattermost, such as \"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\". If the field is left empty, the organization name and address will not be displayed.",
+ "admin.email.notificationOrganizationExample": "예시 \"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\"",
"admin.email.notificationsDescription": "Typically set to true in production. When true, Mattermost attempts to send email notifications. Developers may set this field to false to skip email setup for faster development.<br />Setting this to true removes the Preview Mode banner (requires logging out and logging back in after setting is changed).",
"admin.email.notificationsTitle": "Enable Email Notifications: ",
"admin.email.passwordSaltDescription": "32-character salt added to signing of password reset emails. Randomly generated on install. Click \"Regenerate\" to create new salt.",
@@ -281,7 +283,7 @@
"admin.email.smtpPasswordExample": "예시 \"yourpassword\", \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
"admin.email.smtpPasswordTitle": "SMTP 서버 패스워드:",
"admin.email.smtpPortDescription": "SMTP 이메일 서버의 포트를 입력하세요.",
- "admin.email.smtpPortExample": "Ex: \"25\", \"465\", \"587\"",
+ "admin.email.smtpPortExample": "E.g.: \"25\", \"465\", \"587\"",
"admin.email.smtpPortTitle": "SMTP 서버 포트:",
"admin.email.smtpServerDescription": "Location of SMTP email server.",
"admin.email.smtpServerExample": "예시 \"smtp.yourcompany.com\", \"email-smtp.us-east-1.amazonaws.com\"",
@@ -308,10 +310,20 @@
"admin.general.localization.serverLocaleTitle": "서버 기본 언어:",
"admin.general.log": "로그",
"admin.general.policy": "정책",
+ "admin.general.policy.allowEditPostAlways": "Any time",
+ "admin.general.policy.allowEditPostDescription": "Set policy on the length of time authors have to edit their messages after posting.",
+ "admin.general.policy.allowEditPostNever": "알림 사용 안함",
+ "admin.general.policy.allowEditPostTimeLimit": "seconds after posting",
+ "admin.general.policy.allowEditPostTitle": "Allow users to edit their messages:",
"admin.general.policy.permissionsAdmin": "Team and System Admins",
"admin.general.policy.permissionsAll": "모든 팀 회원",
"admin.general.policy.permissionsAllChannel": "All channel members",
+ "admin.general.policy.permissionsDeletePostAdmin": "Team and System Admins",
+ "admin.general.policy.permissionsDeletePostAll": "Message authors can delete their own messages, and Administrators can delete any message",
+ "admin.general.policy.permissionsDeletePostSystemAdmin": "시스템 관리자",
"admin.general.policy.permissionsSystemAdmin": "시스템 관리자",
+ "admin.general.policy.restrictPostDeleteDescription": "Set policy on who has permission to delete messages.",
+ "admin.general.policy.restrictPostDeleteTitle": "Allow which users to delete messages:",
"admin.general.policy.restrictPrivateChannelCreationDescription": "Set policy on who can create private groups.",
"admin.general.policy.restrictPrivateChannelCreationTitle": "Enable private group creation for:",
"admin.general.policy.restrictPrivateChannelDeletionCommandLineToolLink": "command line tool",
@@ -363,6 +375,7 @@
"admin.image.amazonS3BucketExample": "예시 \"mattermost-media\"",
"admin.image.amazonS3BucketTitle": "Amazon S3 Bucket:",
"admin.image.amazonS3EndpointDescription": "Hostname of your S3 Compatible Storage provider. Defaults to `s3.amazonaws.com`.",
+ "admin.image.amazonS3EndpointExample": "E.g.: \"s3.amazonaws.com\"",
"admin.image.amazonS3EndpointTitle": "Amazon S3 Endpoint:",
"admin.image.amazonS3IdDescription": "Obtain this credential from your Amazon EC2 administrator.",
"admin.image.amazonS3IdExample": "예시 \"AKIADTOVBGERKLCBV\"",
@@ -513,7 +526,7 @@
"admin.metrics.enableDescription": "When true, Mattermost will enable performance monitoring collection and profiling. Please see <a href=\"http://docs.mattermost.com/deployment/metrics.html\" target='_blank'>documentation</a> to learn more about configuring performance monitoring for Mattermost.",
"admin.metrics.enableTitle": "Enable Performance Monitoring:",
"admin.metrics.listenAddressDesc": "The address the server will listen on to expose performance metrics.",
- "admin.metrics.listenAddressEx": "Ex \":8065\"",
+ "admin.metrics.listenAddressEx": "예시 \":8065\"",
"admin.metrics.listenAddressTitle": "Listen Address:",
"admin.mfa.bannerDesc": "Multi-factor authentication is only available for accounts with LDAP and email login methods. If there are users on your system with other login methods, it is recommended you set up multi-factor authentication directly with the SSO or SAML provider.",
"admin.mfa.cluster": "High",
@@ -834,7 +847,7 @@
"admin.team.maxChannelsExample": "예시 \"100\"",
"admin.team.maxChannelsTitle": "팀 당 최대 채널: ",
"admin.team.maxNotificationsPerChannelDescription": "Maximum total number of users in a channel before users typing messages, @all, @here, and @channel no longer send notifications because of performance.",
- "admin.team.maxNotificationsPerChannelExample": "Ex \"10000\"",
+ "admin.team.maxNotificationsPerChannelExample": "예시 \"10000\"",
"admin.team.maxNotificationsPerChannelTitle": "Max Notifications Per Channel:",
"admin.team.maxUsersDescription": "Maximum total number of users per team, including both active and inactive users.",
"admin.team.maxUsersExample": "예시 \"25\"",
@@ -923,8 +936,10 @@
"analytics.chart.meaningful": "Not enough data for a meaningful representation.",
"analytics.system.activeUsers": "활성 사용자 (글 작성 기준)",
"analytics.system.channelTypes": "Channel Types",
+ "analytics.system.dailyActiveUsers": "Daily Active Users",
"analytics.system.expiredBanner": "The Enterprise license expired on {date}. You have 15 days from this date to renew the license, please contact <a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>.",
"analytics.system.expiringBanner": "The Enterprise license is expiring on {date}. To renew your license, please contact <a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>.",
+ "analytics.system.monthlyActiveUsers": "Monthly Active Users",
"analytics.system.postTypes": "글, 파일, 해시태그",
"analytics.system.privateGroups": "비공개 그룹",
"analytics.system.publicChannels": "공개 채널",
@@ -1022,7 +1037,6 @@
"backstage_sidebar.integrations.outgoing_webhooks": "Outgoing Webhook",
"calling_screen": "Calling",
"center_panel.recent": "Click here to jump to recent messages. ",
- "chanel_header.addMembers": "회원 추가",
"change_url.close": "닫기",
"change_url.endWithLetter": "Must end with a letter or number",
"change_url.invalidUrl": "잘못된 URL",
@@ -1040,6 +1054,7 @@
"channel_flow.handleTooShort": "Channel URL must be 2 or more lowercase alphanumeric characters",
"channel_flow.invalidName": "잘못된 채널 이름",
"channel_flow.set_url_title": "{term} URL 설정",
+ "channel_header.addMembers": "회원 추가",
"channel_header.addToFavorites": "즐겨찾기에 추가",
"channel_header.channel": "채널",
"channel_header.channelHeader": "채널 헤더 설정",
@@ -1079,10 +1094,14 @@
"channel_loader.uploadedFile": " uploaded a file",
"channel_loader.uploadedImage": " uploaded an image",
"channel_loader.wrote": " wrote: ",
+ "channel_members_dropdown.channel_admin": "Channel Admin",
+ "channel_members_dropdown.channel_member": "채널 회원",
+ "channel_members_dropdown.make_channel_admin": "Make Channel Admin",
+ "channel_members_dropdown.make_channel_member": "Make Channel Member",
+ "channel_members_dropdown.remove_from_channel": "Remove From Channel",
+ "channel_members_dropdown.remove_member": "Remove Member",
"channel_members_modal.addNew": " 새로운 회원 추가",
- "channel_members_modal.close": "닫기",
- "channel_members_modal.remove": "제거하기",
- "channel_memebers_modal.members": " 회원",
+ "channel_members_modal.members": " 회원",
"channel_modal.cancel": "취소",
"channel_modal.channel": "채널",
"channel_modal.createNew": "새로운 ",
@@ -1091,6 +1110,7 @@
"channel_modal.edit": "편집",
"channel_modal.group": "그룹",
"channel_modal.header": "헤더",
+ "channel_modal.headerEx": "E.g.: \"[Link Title](http://example.com)\"",
"channel_modal.headerHelp": "{term} 상단 이름 옆에 표시될 텍스트를 입력하세요. 예를 들면, 다음과 같이 자주 사용되는 링크를 등록할 수 있습니다. [링크](http://example.com).",
"channel_modal.modalTitle": "새로운 ",
"channel_modal.name": "이름",
@@ -1101,6 +1121,7 @@
"channel_modal.publicChannel1": "공개 채널 만들기",
"channel_modal.publicChannel2": "누구나 참여할 수 있는 새 공개 채널을 만듭니다. ",
"channel_modal.purpose": "설명",
+ "channel_modal.purposeEx": "E.g.: \"A channel to file bugs and improvements\"",
"channel_notifications.allActivity": "모든 활동",
"channel_notifications.allUnread": "모든 읽지않은 메시지",
"channel_notifications.globalDefault": "전역 기본 설정 ({notifyLevel})",
@@ -1113,6 +1134,7 @@
"channel_notifications.unreadInfo": "읽지 않은 채널은 사이드바에서 굵은 글씨로 표시됩니다. \"멘션만\"을 선택하면 내가 멘션된 채널만 굵게 표시됩니다.",
"channel_select.placeholder": "--- 채널을 선택하세요 ---",
"channel_switch_modal.dm": "(개인 메시지)",
+ "channel_switch_modal.failed_to_open": "Failed to open channel.",
"channel_switch_modal.help": "채널 이름을 입력하세요. ↑↓ 방향키와 TAB키를 이용하여 선택하세요, ↵ 엔터키를 누르면 확인, ESC키를 누르면 취소됩니다.",
"channel_switch_modal.not_found": "일치하는 채널이 없습니다.",
"channel_switch_modal.submit": "변경",
@@ -1285,15 +1307,14 @@
"find_team.submitError": "유효한 이메일 주소를 입력하세요.",
"flag_post.flag": "중요 메시지로 지정",
"flag_post.unflag": "중요 메시지 해제",
+ "general_tab.chooseDescription": "사용자명을 선택하세요.",
"general_tab.chooseName": "사용자명을 선택하세요.",
"general_tab.codeDesc": "가입 링크를 변경하려면 '편집'을 클릭하세요.",
"general_tab.codeLongDesc": "The Invite Code is used as part of the URL in the team invitation link created by <strong>Get Team Invite Link</strong> in the main menu. Regenerating creates a new team invitation link and invalidates the previous link.",
"general_tab.codeTitle": "가입 링크",
- "general_tab.dirDisabled": "Team Directory has been disabled. Please ask a System Admin to enable the Team Directory in the System Console team settings.",
- "general_tab.dirOff": "Team directory is turned off for this system.",
"general_tab.emptyDescription": "Click 'Edit' to add a team description.",
+ "general_tab.getTeamInviteLink": "가입 링크",
"general_tab.includeDirDesc": "Including this team will display the team name from the Team Directory section of the Home Page, and provide a link to the sign-in page.",
- "general_tab.includeDirTitle": "Include this team in the Team Directory",
"general_tab.no": "아니요",
"general_tab.openInviteDesc": "When allowed, a link to this team will be included on the landing page allowing anyone with an account to join this team.",
"general_tab.openInviteTitle": "계정이 있는 사용자가 이 팀에 가입하는 것을 허용합니까?",
@@ -1550,6 +1571,7 @@
"login.passwordChanged": " 패스워드가 성공적으로 변경되었습니다.",
"login.session_expired": " 세션이 만료되었습니다. 다시 로그인 하세요.",
"login.signIn": "로그인",
+ "login.signInLoading": "Signing in...",
"login.signInWith": "다음으로 로그인하기:",
"login.userNotFound": "입력된 계정과 일치하는 사용자 정보를 찾을 수 없습니다.",
"login.username": "사용자명",
@@ -1561,8 +1583,10 @@
"member_item.makeAdmin": "관리자로 설정하기",
"member_item.member": "회원",
"member_list.noUsersAdd": "추가할 유저가 없습니다.",
+ "members_popover.manageMembers": "회원 관리",
"members_popover.msg": "메시지",
"members_popover.title": "회원",
+ "members_popover.viewMembers": "회원 보기",
"mfa.confirm.complete": "<strong>Set up complete!</strong>",
"mfa.confirm.okay": "확인",
"mfa.confirm.secure": "Your account is now secure. Next time you sign in, you will be asked to enter a code from the Google Authenticator app on your phone.",
@@ -1665,6 +1689,7 @@
"post_info.mobile.unflag": "중요 해제",
"post_info.permalink": "링크",
"post_info.reply": "답글",
+ "post_message_view.edited": "(edited)",
"posts_view.loadMore": "메시지 더 보기",
"posts_view.newMsg": "새로운 메시지",
"posts_view.newMsgBelow": "New {count, plural, one {message} other {messages}} below",
@@ -1899,7 +1924,7 @@
"update_command.question": "Your changes may break the existing slash command. Are you sure you would like to update it?",
"update_command.update": "Update",
"upload_overlay.info": "이 곳에 파일을 끌어 업로드하세요.",
- "user.settings.advance.embed_preview": "가능한 경우, 링크 내용 미리보기를 표시",
+ "user.settings.advance.embed_preview": "For the first web link in a message, display a preview of website content below the message, if available",
"user.settings.advance.embed_toggle": "미리보기 토글 버튼 보여주기",
"user.settings.advance.enabledFeatures": "{count, number}개 기능 활성화",
"user.settings.advance.formattingDesc": "활성화 하면 링크, 이모티콘, 글자 스타일 등을 사용할 수 있습니다. 기본적으로 활성화 되있습니다. 설정을 변경하려면 페이지 새로고침이 필요합니다.",
@@ -1944,10 +1969,10 @@
"user.settings.display.channelDisplayTitle": "채널 화면 모드",
"user.settings.display.channeldisplaymode": "채널 영역의 너비를 선택하세요.",
"user.settings.display.clockDisplay": "시간 표시",
- "user.settings.display.collapseDesc": "미리보기가 가능한 콘텐츠의 미리보기를 펼친 상태로 표시합니다.",
- "user.settings.display.collapseDisplay": "링크 미리보기",
- "user.settings.display.collapseOff": "끄기",
- "user.settings.display.collapseOn": "켜기",
+ "user.settings.display.collapseDesc": "Set whether previews of image links show as expanded or collapsed by default. This setting can also be controlled using the slash commands /expand and /collapse.",
+ "user.settings.display.collapseDisplay": "Default appearance of image link previews",
+ "user.settings.display.collapseOff": "Collapsed",
+ "user.settings.display.collapseOn": "Expanded",
"user.settings.display.fixedWidthCentered": "고정 너비, 가운데",
"user.settings.display.fontDesc": "Mattermost 화면에서 보여질 폰트를 선택하세요.",
"user.settings.display.fontTitle": "폰트",
@@ -2130,7 +2155,9 @@
"user.settings.security.lastUpdated": "{date} {time} 에 마지막으로 변경됨",
"user.settings.security.ldap": "AD/LDAP",
"user.settings.security.loginGitlab": "Gitlab을 통해 로그인 되었습니다.",
+ "user.settings.security.loginGoogle": "Login done through Google Apps",
"user.settings.security.loginLdap": "LDAP을 통해 로그인 되었습니다.",
+ "user.settings.security.loginOffice365": "Login done through Office 365",
"user.settings.security.loginSaml": "Gitlab을 통해 로그인 되었습니다.",
"user.settings.security.logoutActiveSessions": "활성화 된 세션 보기/로그아웃 하기",
"user.settings.security.method": "접속 방식",
@@ -2159,9 +2186,11 @@
"user.settings.security.passwordErrorUppercaseNumberSymbol": "Your password must contain at least {min} characters made up of at least one uppercase letter, at least one number, and at least one symbol (e.g. \"~!@#$%^&*()\").",
"user.settings.security.passwordErrorUppercaseSymbol": "Your password must contain at least {min} characters made up of at least one uppercase letter and at least one symbol (e.g. \"~!@#$%^&*()\").",
"user.settings.security.passwordGitlabCantUpdate": "Login occurs through GitLab. Password cannot be updated.",
+ "user.settings.security.passwordGoogleCantUpdate": "Login occurs through GitLab. Password cannot be updated.",
"user.settings.security.passwordLdapCantUpdate": "Login occurs through LDAP. Password cannot be updated.",
"user.settings.security.passwordMatchError": "다시 입력한 새로운 패스워드가 일치하지 않습니다.",
"user.settings.security.passwordMinLength": "Invalid minimum length, cannot show preview.",
+ "user.settings.security.passwordOffice365CantUpdate": "Login occurs through GitLab. Password cannot be updated.",
"user.settings.security.passwordSamlCantUpdate": "This field is handled through your login provider. If you want to change it, you need to do so through your login provider.",
"user.settings.security.retypePassword": "새로운 패스워드 확인",
"user.settings.security.saml": "SAML",
diff --git a/webapp/i18n/nl.json b/webapp/i18n/nl.json
index 0cdb10289..664ccb14f 100644
--- a/webapp/i18n/nl.json
+++ b/webapp/i18n/nl.json
@@ -28,6 +28,7 @@
"activity_log.sessionsDescription": "Sessies worden gemaakt wanneer je inlogt met een nieuwe webbrowser. Met een sessie kan Mattermost je gebruiken zonder dat je opnieuw hoeft in te loggen tijdens de ingestelde periode. Om eerder uit te loggen, gebruik je de 'Afmelden'-knop hieronder.",
"activity_log_modal.android": "Android",
"activity_log_modal.androidNativeApp": "Android-applicatie",
+ "activity_log_modal.desktop": "Native Desktop App",
"activity_log_modal.iphoneNativeApp": "iPhone-app",
"add_command.autocomplete": "Automatisch aanvullen",
"add_command.autocomplete.help": "(Optioneel) Toon slash-commando's bij het automatisch aanvullen.",
@@ -260,6 +261,7 @@
"admin.email.notificationEmailTitle": "Melding afzender E-mailadres",
"admin.email.notificationOrganization": "Email Adres in voettekst van melding",
"admin.email.notificationOrganizationDescription": "Organisatie naam en adres weergegeven op e-mailberichten van Mattermost, zoals \"© ABC Corporation, 565 Knight Way, Palo Alto, Californië, 94305, USA\". Als het veld leeg wordt gelaten, wordt de naam en het adres van de organisatie niet weergegeven.",
+ "admin.email.notificationOrganizationExample": "Bijvoorbeeld \"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\"",
"admin.email.notificationsDescription": "Meestal ingesteld op ingeschakeld in productie. Wanneer dit ingeschakeld is, zal Mattermost e-mailberichten verzenden. Ontwikkelaars kunnen dit veld op uitgeschakeld zetten om e-mail instellen over te slaan voor een snellere ontwikkeling.<br />Wanneer ingeschakeld wordt de preview mode banner niet meer getoond (dit vereist uitloggen en opnieuw inloggen nadat deze instelling wordt gewijzigd).",
"admin.email.notificationsTitle": "Inschakelen van E-mail Meldingen: ",
"admin.email.passwordSaltDescription": "32-karakter 'salt' wordt gebruikt om wachtwoord reset mails te beveiligen. Deze wordt tijdens installatie aangemaakt. Klik op \"Opnieuw Genereren\" om een nieuwe 'salt' te maken.",
@@ -281,7 +283,7 @@
"admin.email.smtpPasswordExample": "Bijv. \"uwwachtwoord\", \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
"admin.email.smtpPasswordTitle": "SMTP server wachtwoord:",
"admin.email.smtpPortDescription": "De poort van de SMTP (mailserver).",
- "admin.email.smtpPortExample": "Ex: \"25\", \"465\", \"587\"",
+ "admin.email.smtpPortExample": "E.g.: \"25\", \"465\", \"587\"",
"admin.email.smtpPortTitle": "SMTP server poort :",
"admin.email.smtpServerDescription": "Naam of ip adres van de SMTP (mail)server.",
"admin.email.smtpServerExample": "Bijv. \"smtp.uwbedrijf.nl\", \"email-smtp.us-east-1.amazonaws.com\"",
@@ -308,10 +310,20 @@
"admin.general.localization.serverLocaleTitle": "Standaard server taal:",
"admin.general.log": "Loggen",
"admin.general.policy": "Beleid",
+ "admin.general.policy.allowEditPostAlways": "Any time",
+ "admin.general.policy.allowEditPostDescription": "Set policy on the length of time authors have to edit their messages after posting.",
+ "admin.general.policy.allowEditPostNever": "Nooit",
+ "admin.general.policy.allowEditPostTimeLimit": "seconds after posting",
+ "admin.general.policy.allowEditPostTitle": "Allow users to edit their messages:",
"admin.general.policy.permissionsAdmin": "Team en Systeem Admins",
"admin.general.policy.permissionsAll": "Alle teamleden",
"admin.general.policy.permissionsAllChannel": "All channel members",
+ "admin.general.policy.permissionsDeletePostAdmin": "Team en Systeem Admins",
+ "admin.general.policy.permissionsDeletePostAll": "Message authors can delete their own messages, and Administrators can delete any message",
+ "admin.general.policy.permissionsDeletePostSystemAdmin": "Systeem beheerders",
"admin.general.policy.permissionsSystemAdmin": "Systeem beheerders",
+ "admin.general.policy.restrictPostDeleteDescription": "Set policy on who has permission to delete messages.",
+ "admin.general.policy.restrictPostDeleteTitle": "Allow which users to delete messages:",
"admin.general.policy.restrictPrivateChannelCreationDescription": "Set policy on who can create private groups.",
"admin.general.policy.restrictPrivateChannelCreationTitle": "Enable private group creation for:",
"admin.general.policy.restrictPrivateChannelDeletionCommandLineToolLink": "command line tool",
@@ -363,6 +375,7 @@
"admin.image.amazonS3BucketExample": "Bijv.: \"mattermost-media\"",
"admin.image.amazonS3BucketTitle": "Amazon S3 Bucket:",
"admin.image.amazonS3EndpointDescription": "Hostname of your S3 Compatible Storage provider. Defaults to `s3.amazonaws.com`.",
+ "admin.image.amazonS3EndpointExample": "E.g.: \"s3.amazonaws.com\"",
"admin.image.amazonS3EndpointTitle": "Amazon S3 Endpoint:",
"admin.image.amazonS3IdDescription": "Verkrijg de credentials van uw Amazon EC2 beheerder.",
"admin.image.amazonS3IdExample": "Bijv.: \"AKIADTOVBGERKLCBV\"",
@@ -513,7 +526,7 @@
"admin.metrics.enableDescription": "When true, Mattermost will enable performance monitoring collection and profiling. Please see <a href=\"http://docs.mattermost.com/deployment/metrics.html\" target='_blank'>documentation</a> to learn more about configuring performance monitoring for Mattermost.",
"admin.metrics.enableTitle": "Enable Performance Monitoring:",
"admin.metrics.listenAddressDesc": "The address the server will listen on to expose performance metrics.",
- "admin.metrics.listenAddressEx": "Bijv. \":8065\"",
+ "admin.metrics.listenAddressEx": "Bijv.: \":8065\"",
"admin.metrics.listenAddressTitle": "Luister Adres:",
"admin.mfa.bannerDesc": "Multi-factor authentication is only available for accounts with LDAP and email login methods. If there are users on your system with other login methods, it is recommended you set up multi-factor authentication directly with the SSO or SAML provider.",
"admin.mfa.cluster": "High",
@@ -831,10 +844,10 @@
"admin.team.dirDesc": "Wanneer dit aanstaat, teams die geconfigureerd zijn om zichtbaar te zijn in de team directory zullen zichtbaar zijn op de hoofdpagina in plaats van het maken van een nieuw team.",
"admin.team.dirTitle": "Inschakelen Team Directory: ",
"admin.team.maxChannelsDescription": "Maximum aantal gebruikers per team, inclusief actieve en niet-actieve gebruikers.",
- "admin.team.maxChannelsExample": "Bijv. \"100\"",
+ "admin.team.maxChannelsExample": "Bijv.: \"100\"",
"admin.team.maxChannelsTitle": "Max Channels Per Team:",
"admin.team.maxNotificationsPerChannelDescription": "Maximum total number of users in a channel before users typing messages, @all, @here, and @channel no longer send notifications because of performance.",
- "admin.team.maxNotificationsPerChannelExample": "Bijv. \"10000\"",
+ "admin.team.maxNotificationsPerChannelExample": "Bijv.: \"10000\"",
"admin.team.maxNotificationsPerChannelTitle": "Max Notifications Per Channel:",
"admin.team.maxUsersDescription": "Maximum aantal gebruikers per team, inclusief actieve en niet-actieve gebruikers.",
"admin.team.maxUsersExample": "Bijv.: \"25\"",
@@ -923,8 +936,10 @@
"analytics.chart.meaningful": "Niet genoeg gegevens voor een zinvolle weergave.",
"analytics.system.activeUsers": "Actieve gebruikers met berichten",
"analytics.system.channelTypes": "Kanaal types",
+ "analytics.system.dailyActiveUsers": "Daily Active Users",
"analytics.system.expiredBanner": "De Enterprise licentie is verlopen op {date}. U heeft 15 dagen vanaf deze datum om de licentie te vernieuwen. Neem contact op met <a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>.",
"analytics.system.expiringBanner": "De Enterprise licentie verloop op {date}. Om uw licentie te vernieuwen, neem contact op met <a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>.",
+ "analytics.system.monthlyActiveUsers": "Monthly Active Users",
"analytics.system.postTypes": "Berichten, bestanden en hashtags",
"analytics.system.privateGroups": "Privé groepen",
"analytics.system.publicChannels": "Publieke kanalen",
@@ -1022,7 +1037,6 @@
"backstage_sidebar.integrations.outgoing_webhooks": "Uitgaande webhooks",
"calling_screen": "Oproep",
"center_panel.recent": "Klik hier om naar uw recente berichten te gaan. ",
- "chanel_header.addMembers": "Leden toevoegen",
"change_url.close": "Afsluiten",
"change_url.endWithLetter": "Moet eindigen met een letter of een cijfer",
"change_url.invalidUrl": "Ongeldige URL",
@@ -1040,6 +1054,7 @@
"channel_flow.handleTooShort": "Kanaal URL moet 2 of meer kleine alfanumerieke karakters bevatten",
"channel_flow.invalidName": "Ongeldige kanaal naam",
"channel_flow.set_url_title": "Instelling {term} URL",
+ "channel_header.addMembers": "Leden toevoegen",
"channel_header.addToFavorites": "Add to Favorites",
"channel_header.channel": "Kanaal",
"channel_header.channelHeader": "Edit Channel Header",
@@ -1079,10 +1094,14 @@
"channel_loader.uploadedFile": " heeft een bestand geüpload",
"channel_loader.uploadedImage": " heeft een afbeelding geüpload",
"channel_loader.wrote": " schreef: ",
+ "channel_members_dropdown.channel_admin": "Channel Admin",
+ "channel_members_dropdown.channel_member": "Kanaal Leden",
+ "channel_members_dropdown.make_channel_admin": "Make Channel Admin",
+ "channel_members_dropdown.make_channel_member": "Make Channel Member",
+ "channel_members_dropdown.remove_from_channel": "Remove From Channel",
+ "channel_members_dropdown.remove_member": "Remove Member",
"channel_members_modal.addNew": "Leden toevoegen",
- "channel_members_modal.close": "Afsluiten",
- "channel_members_modal.remove": "Verwijderen",
- "channel_memebers_modal.members": " Leden",
+ "channel_members_modal.members": " Leden",
"channel_modal.cancel": "Annuleren",
"channel_modal.channel": "Kanaal",
"channel_modal.createNew": "Maak nieuw ",
@@ -1091,6 +1110,7 @@
"channel_modal.edit": "Bewerken",
"channel_modal.group": "Groep",
"channel_modal.header": "Kop",
+ "channel_modal.headerEx": "E.g.: \"[Link Title](http://example.com)\"",
"channel_modal.headerHelp": "Geef de tekst die zal verschijnen in het hoofd van de {term} naast de {term} naam. Bijvoorbeeld, veelgebruikte links door het opgeven van [Link Titel](http://example.com).",
"channel_modal.modalTitle": "Nieuw ",
"channel_modal.name": "Naam",
@@ -1101,6 +1121,7 @@
"channel_modal.publicChannel1": "Maak een publiek kanaal",
"channel_modal.publicChannel2": "Maak een publiek kanaal waar iedereen lid van kan worden. ",
"channel_modal.purpose": "Doel",
+ "channel_modal.purposeEx": "E.g.: \"A channel to file bugs and improvements\"",
"channel_notifications.allActivity": "Voor alle activiteiten",
"channel_notifications.allUnread": "Voor alle ongelezen berichten",
"channel_notifications.globalDefault": "Globale standaard ({notifyLevel})",
@@ -1113,6 +1134,7 @@
"channel_notifications.unreadInfo": "De kanaal-naam is vet in de navigatiekolom wanneer er ongelezen berichten zijn. Wanneer \"Enkel bij vermeldingen\" is geselecteerd, zal het kanaal enkel vet zijn wanneer uw naam vermeld is.",
"channel_select.placeholder": "--- Selecteer een kanaal ---",
"channel_switch_modal.dm": "(Privé bericht)",
+ "channel_switch_modal.failed_to_open": "Failed to open channel.",
"channel_switch_modal.help": "Tik kanaal naam. Gebruik ↑↓ voor bladeren, TAB om te selecteren, ↵ voor bevestiging, ESC voor annuleren",
"channel_switch_modal.not_found": "Geen overeenkomsten gevonden.",
"channel_switch_modal.submit": "Wisselen",
@@ -1285,15 +1307,14 @@
"find_team.submitError": "Vul een geldig e-mail adres in",
"flag_post.flag": "Markeren voor vervolgactie",
"flag_post.unflag": "Demarkeer",
+ "general_tab.chooseDescription": "Kies een nieuwe naam voor uw team",
"general_tab.chooseName": "Kies een nieuwe naam voor uw team",
"general_tab.codeDesc": "Klik op 'Bewerken' om de uitnodigings-code opnieuw te genereren.",
"general_tab.codeLongDesc": "De Uitnodigings Code is onderdeel van de URL in de team uitnodigings link gemaakt door de <strong>Krijg Team Uitnodigings Link\"</strong> in het hoofdmenu. Hergenereren maakt een nieuwe team uitnodiging en zal de vorige link onbruikbaar maken.",
"general_tab.codeTitle": "Uitnodigings-code",
- "general_tab.dirDisabled": "Team Directory is uitgeschakeld. Vraag je systeembeheerder om de Team Directory aan te zetten in de Systeem Console team instellingen.",
- "general_tab.dirOff": "Team-adresboek staat uit voor dit systeem",
"general_tab.emptyDescription": "Click 'Edit' to add a team description.",
+ "general_tab.getTeamInviteLink": "Verkrijg de team uitnodigings-link",
"general_tab.includeDirDesc": "Invoegen van dit team zal de team naam zichtbaar maken in de Team Directory sectie op de hoofdpaginam en zal een link naar de login pagina maken.",
- "general_tab.includeDirTitle": "Voeg dit team toe aan aan de Team Gids",
"general_tab.no": "Nee",
"general_tab.openInviteDesc": "Wanneer dit is toegestaan, zal een link naar dit team worden opgenomen op de landings-pagina, zodat iedereen met een account mee kan doen met dit team.",
"general_tab.openInviteTitle": "Sta iedere gebruiker met een account op deze server om lid te worden van dit team",
@@ -1550,6 +1571,7 @@
"login.passwordChanged": " Wachtwoord succesvol bijgewerkt",
"login.session_expired": " Uw sessie is verlopen. Log opnieuw in.",
"login.signIn": "Log in",
+ "login.signInLoading": "Signing in...",
"login.signInWith": "Log in met:",
"login.userNotFound": "We konden geen account vinden met jouw inlog gegevens.",
"login.username": "Gebruikersnaam",
@@ -1561,8 +1583,10 @@
"member_item.makeAdmin": "Maak Admin",
"member_item.member": "Lid",
"member_list.noUsersAdd": "Geen gebruikers om toe te voegen.",
+ "members_popover.manageMembers": "Leden beheren",
"members_popover.msg": "Bericht",
"members_popover.title": " Leden",
+ "members_popover.viewMembers": "Bekijk Leden",
"mfa.confirm.complete": "<strong>Set up complete!</strong>",
"mfa.confirm.okay": "OK",
"mfa.confirm.secure": "Your account is now secure. Next time you sign in, you will be asked to enter a code from the Google Authenticator app on your phone.",
@@ -1665,6 +1689,7 @@
"post_info.mobile.unflag": "Demarkeer ",
"post_info.permalink": "Permanente koppeling",
"post_info.reply": "Antwoord",
+ "post_message_view.edited": "(edited)",
"posts_view.loadMore": "Laad meer berichten",
"posts_view.newMsg": "Nieuwe berichten",
"posts_view.newMsgBelow": "New {count, plural, one {message} other {messages}} below",
@@ -1899,7 +1924,7 @@
"update_command.question": "Your changes may break the existing slash command. Are you sure you would like to update it?",
"update_command.update": "Update",
"upload_overlay.info": "Sleep hier een bestand om te uploaden.",
- "user.settings.advance.embed_preview": "Toon experimentele previews van de link inhoud wanneer deze beschikbaar is.",
+ "user.settings.advance.embed_preview": "For the first web link in a message, display a preview of website content below the message, if available",
"user.settings.advance.embed_toggle": "Toon schakel optie voor alle ingesloten voorbeelden",
"user.settings.advance.enabledFeatures": "{count, number} {count, plural, one {Feature} other {Features}} Ingeschakeld",
"user.settings.advance.formattingDesc": "Indien ingeschakeld, worden berichten opgemaakt met links, emoji, stijl van de tekst, en regeleinden toevoegen. Standaard is deze instelling ingeschakeld. Het wijzigen van deze instelling vereist dat de pagina vernieuwd wordt.",
@@ -1944,10 +1969,10 @@
"user.settings.display.channelDisplayTitle": "Kanaal Schermmodus",
"user.settings.display.channeldisplaymode": "Kies de breedte van het center kanaal.",
"user.settings.display.clockDisplay": "Klok weergave",
- "user.settings.display.collapseDesc": "Vouw links uit om een voorvertoning van de inhoud te zien wanneer deze beschikbaar is.",
- "user.settings.display.collapseDisplay": "Link voorvertoningen",
- "user.settings.display.collapseOff": "Uit",
- "user.settings.display.collapseOn": "Aan",
+ "user.settings.display.collapseDesc": "Set whether previews of image links show as expanded or collapsed by default. This setting can also be controlled using the slash commands /expand and /collapse.",
+ "user.settings.display.collapseDisplay": "Default appearance of image link previews",
+ "user.settings.display.collapseOff": "Collapsed",
+ "user.settings.display.collapseOn": "Expanded",
"user.settings.display.fixedWidthCentered": "Vaste breedte, gecentreerd",
"user.settings.display.fontDesc": "Selecteer het lettertype dat door de Mattermost user interface wordt gebruikt.",
"user.settings.display.fontTitle": "Lettertype",
@@ -2130,7 +2155,9 @@
"user.settings.security.lastUpdated": "Laatst bijgewerkt op {date} {time}",
"user.settings.security.ldap": "AD/LDAP",
"user.settings.security.loginGitlab": "Inloggen via Gitlab",
+ "user.settings.security.loginGoogle": "Login done through Google Apps",
"user.settings.security.loginLdap": "Aanmelden via AD/LDAP",
+ "user.settings.security.loginOffice365": "Login done through Office 365",
"user.settings.security.loginSaml": "Inloggen via Gitlab",
"user.settings.security.logoutActiveSessions": "Bekijk en uitloggen huidige sessies",
"user.settings.security.method": "Inlog methode",
@@ -2159,9 +2186,11 @@
"user.settings.security.passwordErrorUppercaseNumberSymbol": "Jouw wachtwoord moet minimaal {min} karakters bevatten met minimaal een hoofdetter, een cijfer en een karakter (bv. \"~!@#$%^&*()\").",
"user.settings.security.passwordErrorUppercaseSymbol": "Jouw wachtwoord moet minimaal {min} karakters bevatten met minimaal een hoofdletter en een karakter (bv. \"~!@#$%^&*()\").",
"user.settings.security.passwordGitlabCantUpdate": "Aanmelden gebeurt via Gitlab. Uw wachtwoord kan niet bijgewerkt worden.",
+ "user.settings.security.passwordGoogleCantUpdate": "Aanmelden gebeurt via Gitlab. Uw wachtwoord kan niet bijgewerkt worden.",
"user.settings.security.passwordLdapCantUpdate": "Aanmelden gebeurt via AD/LDAP. Uw wachtwoord kan niet bijgewerkt worden.",
"user.settings.security.passwordMatchError": "De wachtwoorden die U ingaf zijn niet identiek",
"user.settings.security.passwordMinLength": "Verkeerde minimale lengte, kan de voorvertoning niet tonen.",
+ "user.settings.security.passwordOffice365CantUpdate": "Aanmelden gebeurt via Gitlab. Uw wachtwoord kan niet bijgewerkt worden.",
"user.settings.security.passwordSamlCantUpdate": "Dit veld word gebruikt door jouw login provider. Als je het wilt veranderen, moet je dat via de login provider doen. ",
"user.settings.security.retypePassword": "Nieuw wachtwoord herhalen",
"user.settings.security.saml": "SAML",
diff --git a/webapp/i18n/pt-BR.json b/webapp/i18n/pt-BR.json
index bbd12f671..a194ff246 100644
--- a/webapp/i18n/pt-BR.json
+++ b/webapp/i18n/pt-BR.json
@@ -28,6 +28,7 @@
"activity_log.sessionsDescription": "Sessões são criadas quando você efetuar login em um novo navegador em um dispositivo. Sessões permitem que você use Mattermost sem ter que logar novamente por um período de tempo especificado pelo administrador do sistema. Se você deseja sair mais cedo, use o botão 'Logout' abaixo para terminar uma sessão.",
"activity_log_modal.android": "Android",
"activity_log_modal.androidNativeApp": "App Nativo para Android",
+ "activity_log_modal.desktop": "Aplicativo Nativo Desktop",
"activity_log_modal.iphoneNativeApp": "App Nativo para iPhone",
"add_command.autocomplete": "Autocompletar",
"add_command.autocomplete.help": "(Opcional) Exibir comandos slash na lista de auto preenchimento.",
@@ -219,10 +220,10 @@
"admin.customization.iosAppDownloadLinkDesc": "Adiciona um link para download do app iOS. Usuários que acessarem o site em um navegador móvel serão perguntados com uma página dando a opção para download do app. Deixe este campo em branco para evitar que a página apareça.",
"admin.customization.iosAppDownloadLinkTitle": "App iOS Link para Download:",
"admin.customization.nativeAppLinks": "Links Aplicativo Mattermost",
- "admin.customization.restrictCustomEmojiCreationAdmin": "Habilitar criação de emoji personalizados para Administradores de Sistemas e de Times ",
+ "admin.customization.restrictCustomEmojiCreationAdmin": "Habilitar criação de emoji personalizados para Administradores de Sistema e de Times ",
"admin.customization.restrictCustomEmojiCreationAll": "Permitir que todos possam criar emoji personalizados",
"admin.customization.restrictCustomEmojiCreationDesc": "Restringir a criação de emoji personalizado para determinados usuários.",
- "admin.customization.restrictCustomEmojiCreationSystemAdmin": "Permitir que apenas administradores de sistema possam criar emoji personalizados",
+ "admin.customization.restrictCustomEmojiCreationSystemAdmin": "Permitir que apenas administradores do sistema possam criar emoji personalizados",
"admin.customization.restrictCustomEmojiCreationTitle": "Restringir a Criação Emoji Personalizado:",
"admin.customization.support": "Legal e Suporte",
"admin.database.title": "Configurações do Banco de dados",
@@ -256,10 +257,11 @@
"admin.email.notificationDisplayExample": "Ex: \"Mattermost Notificação\", \"Sistema\", \"Não-Responda\"",
"admin.email.notificationDisplayTitle": "Notificação Nome de Exibição:",
"admin.email.notificationEmailDescription": "Endereço de email mostrado na conta de email quando envia notificações do Mattermost.",
- "admin.email.notificationEmailExample": "Ex: \"mattermost@yourcompany.com\", \"admin@yourcompany.com\"",
+ "admin.email.notificationEmailExample": "Ex: \"mattermost@suaempresa.com\", \"admin@suaempresa.com\"",
"admin.email.notificationEmailTitle": "Notificação a Partir do Endereço:",
"admin.email.notificationOrganization": "Notificação Endereço Rodapé:",
"admin.email.notificationOrganizationDescription": "Nome da empresa e endereço de email mostrado nas notificações do Mattermost, como \"® Empresa ABC, Av. Paulista, 1000, São Paulo, SP, 12345-150, BRA\". Se este campo for deixado em branco, o nome da organização e endereço não será mostrado.",
+ "admin.email.notificationOrganizationExample": "Ex. \"® Empresa ABC, Av. Paulista, 1000, São Paulo, SP, 12345-150, BRA\"",
"admin.email.notificationsDescription": "Normalmente definido como verdadeiro em produção. Quando verdadeiro, Mattermost tenta enviar notificações por e-mail. Os desenvolvedores podem definir este campo como falso para ignorar configuração de e-mail para o desenvolvimento mais rápido.<br /> A definição deste como verdadeiro remove a bandeira modo de visualização (requer sair e entrar novamente após a alteração).",
"admin.email.notificationsTitle": "Habilitar Notificações por E-mail: ",
"admin.email.passwordSaltDescription": "Salt de 32-caracteres adicionado para assinar o redefinição de senha por e-mail. Gerada aleatoriamente na instalação. Clique em \"Re-Gerar\" para criar um novo salt.",
@@ -287,7 +289,7 @@
"admin.email.smtpServerExample": "Ex: \"smtp.suaempresa.com\", \"email-smtp.us-east-1.amazonaws.com\"",
"admin.email.smtpServerTitle": "Servidor SMTP:",
"admin.email.smtpUsernameDescription": " Obter essa credencial do administrador das configurações do servidor de email.",
- "admin.email.smtpUsernameExample": "Ex: \"admin@yourcompany.com\", \"AKIADTOVBGERKLCBV\"",
+ "admin.email.smtpUsernameExample": "Ex: \"admin@suaempresa.com\", \"AKIADTOVBGERKLCBV\"",
"admin.email.smtpUsernameTitle": "Nome do Usuário do Servidor de SMTP:",
"admin.email.testing": "Testando...",
"admin.false": "falso",
@@ -308,10 +310,20 @@
"admin.general.localization.serverLocaleTitle": "Idioma Padrão do Servidor:",
"admin.general.log": "Carregando",
"admin.general.policy": "Política",
+ "admin.general.policy.allowEditPostAlways": "A qualquer momento",
+ "admin.general.policy.allowEditPostDescription": "Definir a política sobre o período de tempo que os autores têm de editar suas mensagens após a publicação.",
+ "admin.general.policy.allowEditPostNever": "Nunca",
+ "admin.general.policy.allowEditPostTimeLimit": "segundos após a postagem",
+ "admin.general.policy.allowEditPostTitle": "Permitir que os usuários editem suas mensagens:",
"admin.general.policy.permissionsAdmin": "Administradores de Time e Sistema",
"admin.general.policy.permissionsAll": "Todos os membros da equipe",
"admin.general.policy.permissionsAllChannel": "Todos os membros do canal",
+ "admin.general.policy.permissionsDeletePostAdmin": "Administradores de Equipe e Sistema",
+ "admin.general.policy.permissionsDeletePostAll": "Autor da mensagem pode deletar sua própria mensagem, e Administradores podem deletar qualquer mensagem",
+ "admin.general.policy.permissionsDeletePostSystemAdmin": "Administrador de Sistema",
"admin.general.policy.permissionsSystemAdmin": "Administrador de Sistema",
+ "admin.general.policy.restrictPostDeleteDescription": "Define a política de quem tem permissão para deletar mensagens.",
+ "admin.general.policy.restrictPostDeleteTitle": "Permitir quais usuários a deletar mensagens:",
"admin.general.policy.restrictPrivateChannelCreationDescription": "Define a política sobre quem pode criar grupos privados.",
"admin.general.policy.restrictPrivateChannelCreationTitle": "Ativar a criação de grupos privados para:",
"admin.general.policy.restrictPrivateChannelDeletionCommandLineToolLink": "ferramenta de linha de comando",
@@ -363,6 +375,7 @@
"admin.image.amazonS3BucketExample": "Ex.: \"mattermost-media\"",
"admin.image.amazonS3BucketTitle": "Amazon S3 Bucket:",
"admin.image.amazonS3EndpointDescription": "Nome do host provedor de armazenamento compatível com S3. O padrão é `s3.amazonaws.com`.",
+ "admin.image.amazonS3EndpointExample": "Ex.: \"s3.amazonaws.com\"",
"admin.image.amazonS3EndpointTitle": "Amazon S3 Endpoint:",
"admin.image.amazonS3IdDescription": "Obter essa credencial do seu administrador Amazon EC2.",
"admin.image.amazonS3IdExample": "Ex.: \"AKIADTOVBGERKLCBV\"",
@@ -513,7 +526,7 @@
"admin.metrics.enableDescription": "Quando verdadeiro, Mattermost irá habilitar a coleta do monitoramento de performance e profiling. Por favor verifique <a href=\"http://docs.mattermost.com/deployment/metrics.html\" target='_blank'>documentação</a> para ler mais sobre como configurar o monitoramento de performance para Mattermost.",
"admin.metrics.enableTitle": "Habilitar Monitoramento de Performance:",
"admin.metrics.listenAddressDesc": "O endereço que o servidor irá escutar para expor as métricas de performance.",
- "admin.metrics.listenAddressEx": "Ex \":8067\"",
+ "admin.metrics.listenAddressEx": "Ex.: \":8067\"",
"admin.metrics.listenAddressTitle": "Endereço à escutar:",
"admin.mfa.bannerDesc": "Autenticação por Multi-fator está apenas disponível para contas com LDAP e login por email. Se existir algum usuário no sistema com outro tipo de login, é recomendado que você configure a autenticação por multi-fator diretamente com o provedor de SSO ou SAML.",
"admin.mfa.cluster": "Alta",
@@ -564,7 +577,7 @@
"admin.rate.enableLimiterTitle": "Ativar Limitador de Velocidade: ",
"admin.rate.httpHeaderDescription": "Quando preenchido, a limitação de velocidade varia pelo campo do cabeçalho HTTP especificado (ex. quando configurado NGINX ajustado para \"X-Real-IP\", quando configurado AmazonELB ajustado para \"X-Forwarded-For\").",
"admin.rate.httpHeaderExample": "Ex.: \"X-Real-IP\", \"X-Forwarded-For\"",
- "admin.rate.httpHeaderTitle": "Variar o limite de velocidade pelo cabeçalho HTTP",
+ "admin.rate.httpHeaderTitle": "Variar o limite de velocidade pelo cabeçalho HTTP:",
"admin.rate.maxBurst": "Máximo Tamanho Burst:",
"admin.rate.maxBurstDescription": "Máximo número de pedidos permitidos além do limite de consultas por segundo.",
"admin.rate.maxBurstExample": "Ex.: \"100\"",
@@ -597,7 +610,7 @@
"admin.saml.assertionConsumerServiceURLDesc": "Digite https://<sua-url-mattermost>/login/sso/saml. Certifique-se de usar HTTP ou HTTPS em sua URL, dependendo da sua configuração do servidor. Este campo também é conhecido como Assertion Consumer Service URL.",
"admin.saml.assertionConsumerServiceURLEx": "Ex \"https://<sua-url-mattermost>/login/sso/saml\"",
"admin.saml.assertionConsumerServiceURLTitle": "URL Provedor de Serviço de Login:",
- "admin.saml.bannerDesc": "User attributes in SAML server, including user deactivation or removal, are updated in Mattermost during user login. Learn more at: <a href=\"https://docs.mattermost.com/deployment/sso-saml.html\">https://docs.mattermost.com/deployment/sso-saml.html</a>",
+ "admin.saml.bannerDesc": "Os atributos de usuário no servidor SAML, incluindo a desativação ou remoção do usuário, são atualizados no Mattermost durante o login do usuário. Leia mais em <a href=\"https://docs.mattermost.com/deployment/sso-saml.html\">https://docs.mattermost.com/deployment/sso-saml.html</a>",
"admin.saml.emailAttrDesc": "O atributo na SAML Assertion que será usado para preencher os endereços de email dos usuários em Mattermost.",
"admin.saml.emailAttrEx": "Ex.: \"Email\" ou \"PrimaryEmail\"",
"admin.saml.emailAttrTitle": "Atributo de E-mail:",
@@ -754,7 +767,7 @@
"admin.sidebar.legalAndSupport": "Legal e Suporte",
"admin.sidebar.license": "Edição e Licença",
"admin.sidebar.localization": "Regionalização",
- "admin.sidebar.logging": "Acesso",
+ "admin.sidebar.logging": "Logs",
"admin.sidebar.login": "Login",
"admin.sidebar.logs": "Logs",
"admin.sidebar.metrics": "Monitoramento de Performance (Beta)",
@@ -831,10 +844,10 @@
"admin.team.dirDesc": "Quando verdadeiro, as equipes que estão configuradas para mostrar o diretório de equipe irá mostrar na página principal, em lugar de criar uma nova equipe.",
"admin.team.dirTitle": "Ativar Diretório de Equipe: ",
"admin.team.maxChannelsDescription": "Número máximo total de canais por equipe, incluindo ambos canais ativos e inativos.",
- "admin.team.maxChannelsExample": "Ex \"100\"",
+ "admin.team.maxChannelsExample": "Ex.: \"100\"",
"admin.team.maxChannelsTitle": "Máximo Canais Por Equipe:",
"admin.team.maxNotificationsPerChannelDescription": "Número total máximo de usuários em um canal antes de os usuários digitarem as mensagens, @all, @here e @channel não será enviado notificações por motivos de performance.",
- "admin.team.maxNotificationsPerChannelExample": "Ex \"1000\"",
+ "admin.team.maxNotificationsPerChannelExample": "Ex.: \"1000\"",
"admin.team.maxNotificationsPerChannelTitle": "Máximo de Notificações por Canal",
"admin.team.maxUsersDescription": "Número máximo total de usuários por equipe, incluindo ambos usuários ativos e inativos.",
"admin.team.maxUsersExample": "Ex.: \"25\"",
@@ -901,13 +914,13 @@
"admin.webrtc.gatewayWebsocketUrlTitle": "Gateway Websocket URL:",
"admin.webrtc.stunUriDescription": "Digite sua STUN URI como stun:<your-stun-url>:<port>. STUN é um protocolo de rede padronizado que permite que um host de destino auxilie dispositivos para estabelecer uma conexão usando um endereço IP público se este está localizado atrás de um NAT.",
"admin.webrtc.stunUriExample": "Ex.: \"stun:webrtc.mattermost.com:5349\"",
- "admin.webrtc.stunUriTitle": "STUN URI",
+ "admin.webrtc.stunUriTitle": "STUN URI:",
"admin.webrtc.turnSharedKeyDescription": "Digite sua Chave Compartilhada TURN. Isto é usado para criar senhas dinâmicas para estabelecer a conexão. Cada senha é válida para um período de tempo curto.",
"admin.webrtc.turnSharedKeyExample": "Ex.: \"bXdkOWQxc3d0Ynk3emY5ZmsxZ3NtazRjaWg=\"",
"admin.webrtc.turnSharedKeyTitle": "Chave Compartilhada TURN:",
"admin.webrtc.turnUriDescription": "Digite sua TURN URI como turn:<your-turn-url>:<port>. TURN é um protocolo de rede padronizado que permite que um host de destino auxilie dispositivos para estabelecer uma conexão usando um endereço IP público se este está localizado atrás de um NAT simétrico.",
"admin.webrtc.turnUriExample": "Ex.: \"turn:webrtc.mattermost.com:5349\"",
- "admin.webrtc.turnUriTitle": "TURN URI",
+ "admin.webrtc.turnUriTitle": "TURN URI:",
"admin.webrtc.turnUsernameDescription": "Digite seu Usuário para o Servidor TURN.",
"admin.webrtc.turnUsernameExample": "Ex.: \"meuusuario\"",
"admin.webrtc.turnUsernameTitle": "Usuário TURN:",
@@ -923,8 +936,10 @@
"analytics.chart.meaningful": "Não há dados suficientes para uma representação significativa.",
"analytics.system.activeUsers": "Usuários Ativos Com Posts",
"analytics.system.channelTypes": "Tipos de Canal",
+ "analytics.system.dailyActiveUsers": "Daily Active Users",
"analytics.system.expiredBanner": "A licença Enterprise expirou em {date}. Você tem 15 dias a partir desta data para renovar a licença, por favor contate <a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>.",
"analytics.system.expiringBanner": "A licença Enterprise termina em {date}. Para renovar sua licença, por favor contate <a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>.",
+ "analytics.system.monthlyActiveUsers": "Monthly Active Users",
"analytics.system.postTypes": "Posts, Arquivos e Hashtags",
"analytics.system.privateGroups": "Grupos Privados",
"analytics.system.publicChannels": "Canais Públicos",
@@ -1022,7 +1037,6 @@
"backstage_sidebar.integrations.outgoing_webhooks": "Webhooks Saída",
"calling_screen": "Chamando",
"center_panel.recent": "Clique aqui para pular para mensagens recentes. ",
- "chanel_header.addMembers": "Adicionar Membros",
"change_url.close": "Fechar",
"change_url.endWithLetter": "Deve teminar com uma letra ou número",
"change_url.invalidUrl": "URL inválida",
@@ -1040,6 +1054,7 @@
"channel_flow.handleTooShort": "URL do canal precisa ter 2 ou mais caracteres minúsculos alfanuméricos",
"channel_flow.invalidName": "Nome do Canal Inválido",
"channel_flow.set_url_title": "Ajustar URL {term}",
+ "channel_header.addMembers": "Adicionar Membros",
"channel_header.addToFavorites": "Adicionar aos Favoritos",
"channel_header.channel": "Canal",
"channel_header.channelHeader": "Editar Cabeçalho do Canal",
@@ -1079,10 +1094,14 @@
"channel_loader.uploadedFile": " enviado um arquivo",
"channel_loader.uploadedImage": " enviado uma imagem",
"channel_loader.wrote": " escreveu: ",
+ "channel_members_dropdown.channel_admin": "Administrador de Canal",
+ "channel_members_dropdown.channel_member": "Membro do Canal",
+ "channel_members_dropdown.make_channel_admin": "Tornar Administrador de Canal",
+ "channel_members_dropdown.make_channel_member": "Tornar Membro de Canal",
+ "channel_members_dropdown.remove_from_channel": "Remover do Canal",
+ "channel_members_dropdown.remove_member": "Remover Membro",
"channel_members_modal.addNew": " Adicionar Novos Membros",
- "channel_members_modal.close": "Fechar",
- "channel_members_modal.remove": "Remover",
- "channel_memebers_modal.members": " Membros",
+ "channel_members_modal.members": " Membros",
"channel_modal.cancel": "Cancelar",
"channel_modal.channel": "Canal",
"channel_modal.createNew": "Criar Novo ",
@@ -1091,6 +1110,7 @@
"channel_modal.edit": "Editar",
"channel_modal.group": "Grupo",
"channel_modal.header": "Cabeçalho",
+ "channel_modal.headerEx": "Ex.: \"[Título do Link](http://example.com)\"",
"channel_modal.headerHelp": "Configure o texto que irá aparecer no topo do {term} ao lado do nome {term}. Por exemplo, inclua os links utilizados frequentemente digitando [Link Title](http://example.com).",
"channel_modal.modalTitle": "Novo ",
"channel_modal.name": "Nome",
@@ -1101,6 +1121,7 @@
"channel_modal.publicChannel1": "Criar um canal público",
"channel_modal.publicChannel2": "Criar um novo canal público para qualquer um participar. ",
"channel_modal.purpose": "Propósito",
+ "channel_modal.purposeEx": "Ex.: \"Um canal para arquivar bugs e melhorias\"",
"channel_notifications.allActivity": "Para todas as atividades",
"channel_notifications.allUnread": "Para todas as mensagens não lidas",
"channel_notifications.globalDefault": "Global padrão ({notifyLevel})",
@@ -1113,6 +1134,7 @@
"channel_notifications.unreadInfo": "O nome do canal fica em negrito na barra lateral quando houver mensagens não lidas. Selecionando \"Apenas menções\" o canal vai ficar em negrito apenas quando você for mencionado.",
"channel_select.placeholder": "--- Selecione um canal ---",
"channel_switch_modal.dm": "(Mensagem Direta)",
+ "channel_switch_modal.failed_to_open": "Falha ao abrir o canal.",
"channel_switch_modal.help": "Digite o nome do canal. Use ↑↓ para navegar, TAB para selecionar, ↵ para confirmar, ESC para cancelar",
"channel_switch_modal.not_found": "Nenhum resultado encontrado.",
"channel_switch_modal.submit": "Alternar",
@@ -1197,7 +1219,7 @@
"delete_channel.confirm": "Confirmar EXCLUSÃO do Canal",
"delete_channel.del": "Deletar",
"delete_channel.group": "grupo",
- "delete_channel.question": "Isto irá apagar o canal do equipe e todo o conteúdo não vai estar disponível para os usuários. Você está certo que deseja apagar {display_name} {term}?",
+ "delete_channel.question": "Isto irá apagar o canal da equipe e todo o conteúdo não vai estar mais disponível para os usuários. Você tem certeza de que deseja apagar o {term} {display_name}?",
"delete_post.cancel": "Cancelar",
"delete_post.comment": "Comentário",
"delete_post.confirm": "Confirmar Delete {term}",
@@ -1285,15 +1307,14 @@
"find_team.submitError": "Por favor entre um endereço de e-mail válido",
"flag_post.flag": "Marcar para seguir",
"flag_post.unflag": "Desmarcar",
+ "general_tab.chooseDescription": "Por favor escolha uma nova descrição para sua equipe",
"general_tab.chooseName": "Por favor escolha um novo nome para sua equipe",
"general_tab.codeDesc": "Clique 'Edit' para re-gerar o Código de Convite.",
- "general_tab.codeLongDesc": "O Código de convite é usado como parte da URL no link de convite da equipe criado por <strong>Obter Link de Convite de Equipe</strong> no menu principal. Re-gerar cria um novo link de convite de equipe e invalida os link anteriores.",
+ "general_tab.codeLongDesc": "O Código de convite é usado como parte da URL no link de convite da equipe criado por {getTeamInviteLink} no menu principal. Re-gerar cria um novo link de convite de equipe e invalida os link anteriores.",
"general_tab.codeTitle": "Código de Convite",
- "general_tab.dirDisabled": "Diretório de equipe foi desativado. Por favor peça a um Administrador de Sistema para ativar o Diretório de Equipe nas configurações do Console do Sistema.",
- "general_tab.dirOff": "Diretório de equipe está desativado para este sistema.",
"general_tab.emptyDescription": "Clique 'Editar' para adicionar uma descrição da equipe",
+ "general_tab.getTeamInviteLink": "Obter Link de Convite de Equipe",
"general_tab.includeDirDesc": "Incluindo esta equipe irá exibir o nome da equipe da seção Diretório Equipe da página inicial, e fornecer um link para a página de login.",
- "general_tab.includeDirTitle": "Incluir esta equipe no Diretório de Equipe",
"general_tab.no": "Não",
"general_tab.openInviteDesc": "Quando permitido, um link para esta equipe vai ser incluído na página de destino permitindo que qualquer pessoa com uma conta possa participar desta equipe.",
"general_tab.openInviteTitle": "Permitir que qualquer usuário com uma conta neste servidor possa se juntar a esta equipe",
@@ -1321,7 +1342,7 @@
"get_post_link_modal.title": "Copiar Permalink",
"get_public_link_modal.help": "O link abaixo permite que qualquer pessoa veja este arquivo sem ser registrado neste servidor.",
"get_public_link_modal.title": "Obter Link Público",
- "get_team_invite_link_modal.help": "Enviar o link abaixo para sua equipe de trabalho para que eles se inscrevam no site da sua equipe. O Link de Convite de Equipe como ele não muda pode ser compartilhado com várias pessoas ao menos que seja re-gerado em Configurações de Equipe pelo Administrador de Equipe.",
+ "get_team_invite_link_modal.help": "Envie o link abaixo para sua equipe de trabalho para que eles se inscrevam nessa equipe. O Link de Convite de Equipe pode ser compartilhado com vários colegas de equipe, pois ele não muda a menos que seja re-gerado nas Configurações de Equipe por um Administrador da mesma.",
"get_team_invite_link_modal.helpDisabled": "Criação de usuários está desabilitada para sua equipe. Por favor peça ao administrador de equipe por detalhes.",
"get_team_invite_link_modal.title": "Link para Convite de Equipe",
"help.attaching.downloading": "#### Download Arquivos\nFazer download de um arquivo anexado clicando no ícone de download ao lado da miniatura de arquivos ou abrindo o visualizador de arquivos e clicando em **Download**.",
@@ -1344,7 +1365,7 @@
"help.commands.intro": "Comandos slash executam operações no Mattermost digitando na caixa de entrada de texto. Digite `/` seguido por um comando e alguns argumentos para executar ações.\n\nComandos slash nativos vêm com todas as instalações Mattermost e comandos slash personalizado são configurados para interagir com aplicações externas. Saiba mais sobre a configuração de comandos slash personalizados em [developer slash command documentation page](http://docs.mattermost.com/developer/slash-commands.html).",
"help.commands.title": "# Executando Comandos\n___",
"help.composing.deleting": "## Deletando uma mensagem\nDelete uma mensagem clicando no ícone **[...]** ao lado do texto da mensagem que você escreveu, em seguida, clique em **Deletar**. Administrador de Sistema e de Equipe podem excluir qualquer mensagem em seu sistema ou equipe.",
- "help.composing.editing": "## Edição de Mensagem\nEditar uma mensagem clicando no ícone **[...]** ao lado o texto da mensagem que você compôs, em seguida, clique em **Editar**. Depois de fazer modificações no texto da mensagem, pressione **ENTER** para salvar as modificações. Edições mensagem não desencadeiam novas notificações de @menção, notificações de área de trabalho ou sons de notificação.",
+ "help.composing.editing": "## Edição de Mensagem\nEdite uma mensagem clicando no ícone **[...]** ao lado do texto de qualquer mensagem que você compôs, em seguida, clique em **Editar**. Após fazer modificações no texto da mensagem, pressione **ENTER** para salvá-las. Edições em mensagens não geram novas notificações de @menção, notificações da área de trabalho ou sons de notificação.",
"help.composing.linking": "## Link para uma mensagem\nO recurso **Permalink** cria um link para qualquer mensagem. Compartilhar este link com outros usuários no canal lhes permite visualizar a mensagem lincada no Arquivos de Mensagem. Os usuários que não são membros do canal onde a mensagem foi postada não podem ver o permalink. Obter o permalink de qualquer mensagem clicando no ícone **[...]** ao lado do texto da mensagem > **Permalink** > **Copiar Link**.",
"help.composing.posting": "## Postando uma Mensagem\nEscreva uma mensagem digitando na caixa de entrada de texto, em seguida, pressione **ENTER** para enviá-la. Use **Shift + ENTER** para criar uma nova linha sem enviar uma mensagem. Para enviar mensagens pressionando **CTRL + ENTER** vá para **Menu Principal > Configurações de Conta > Enviar mensagens com CTRL + ENTER**.",
"help.composing.posts": "#### Posts\nPosts podem ser consideradas as mensagens principais. Eles são as mensagens que, muitas vezes iniciam uma discussão com respostas. Posts são criados e enviados a partir da caixa de entrada de texto na parte inferior do painel central.",
@@ -1550,6 +1571,7 @@
"login.passwordChanged": " Senha atualizada com sucesso",
"login.session_expired": " Sua sessão expirou. Por favor faça login novamente.",
"login.signIn": "Login",
+ "login.signInLoading": "Iniciando a sessão...",
"login.signInWith": "Login com:",
"login.userNotFound": "Não foi possível encontrar uma conta que corresponda com as suas credenciais de login.",
"login.username": "Usuário",
@@ -1561,8 +1583,10 @@
"member_item.makeAdmin": "Tornar Admin",
"member_item.member": "Membro",
"member_list.noUsersAdd": "Nenhum usuário para adicionar.",
+ "members_popover.manageMembers": "Gerenciar Membros",
"members_popover.msg": "Mensagem",
"members_popover.title": "Membros",
+ "members_popover.viewMembers": "Ver Membros",
"mfa.confirm.complete": "<strong>Configuração completa!</strong>",
"mfa.confirm.okay": "Ok",
"mfa.confirm.secure": "Sua conta agora está segura. Na próxima vez que você fizer login, será pedido a você um código do aplicativo Google Authenticator",
@@ -1626,7 +1650,7 @@
"navbar_dropdown.report": "Relatar um Problema",
"navbar_dropdown.switchTeam": "Alternar para {team}",
"navbar_dropdown.switchTo": "Alternar para ",
- "navbar_dropdown.teamLink": "Obter Link Convite de Equipe",
+ "navbar_dropdown.teamLink": "Obter Link de Convite de Equipe",
"navbar_dropdown.teamSettings": "Configurações da Equipe",
"navbar_dropdown.viewMembers": "Ver Membros",
"notification.dm": "Mensagem Direta",
@@ -1651,7 +1675,7 @@
"permalink.error.access": "O permalink pertence a uma mensagem deletada ou a um canal o qual você não tem acesso.",
"post_attachment.collapse": "Mostrar menos...",
"post_attachment.more": "Mostrar mais...",
- "post_body.commentedOn": "Comentado da mensagem {name}{apostrophe}: ",
+ "post_body.commentedOn": "Comentado da mensagem de {name}: ",
"post_body.deleted": "(mensagem deletada)",
"post_body.plusMore": " mais {count} outros arquivos",
"post_body.plusOne": " mais 1 outro arquivo",
@@ -1665,6 +1689,7 @@
"post_info.mobile.unflag": "Desmarcar",
"post_info.permalink": "Permalink",
"post_info.reply": "Responder",
+ "post_message_view.edited": "(editado)",
"posts_view.loadMore": "Carregar mais mensagens",
"posts_view.newMsg": "Novas Mensagens",
"posts_view.newMsgBelow": "{count} {count, plural, one {nova mensagem} other {novas mensagens}} abaixo",
@@ -1767,7 +1792,7 @@
"sidebar_right_menu.nativeApps": "Download Aplicativos",
"sidebar_right_menu.recentMentions": "Menções Recentes",
"sidebar_right_menu.report": "Relatar um Problema",
- "sidebar_right_menu.teamLink": "Obter Link Convite de Equipe",
+ "sidebar_right_menu.teamLink": "Obter Link de Convite de Equipe",
"sidebar_right_menu.teamSettings": "Configurações da Equipe",
"sidebar_right_menu.viewMembers": "Ver Membros",
"signup.email": "Email e Senha",
@@ -1899,12 +1924,12 @@
"update_command.question": "Suas alterações podem fazer parar de funcionar o comando slash existente. Tem a certeza de que pretende atualizá-lo?",
"update_command.update": "Atualizar",
"upload_overlay.info": "Soltar um arquivo para enviá-lo.",
- "user.settings.advance.embed_preview": "Mostrar pré-visualização experimental de conteúdo de link, quando disponível",
+ "user.settings.advance.embed_preview": "For the first web link in a message, display a preview of website content below the message, if available",
"user.settings.advance.embed_toggle": "Exibir mostrar/esconder para todas as pre-visualizações",
"user.settings.advance.enabledFeatures": "{count, number} {count, plural, one {Recurso} other {Recursos}} Ativado",
"user.settings.advance.formattingDesc": "Se ativado, posts serão formatados para criar links, exibir emoji, estilo de texto e adicionar quebra de linhas. Por padrão é definido como ativado. Mudando está configuração será necessário recarregar a página.",
"user.settings.advance.formattingTitle": "Ativar Formatação de Post",
- "user.settings.advance.joinLeaveDesc": "Quando \"Ligado\", Mensagens do Sistema dizendo que um usuário entrou ou saiu de um canal será visível. Quando \"Off\", as Mensagens do Sistema sobre entrar ou sair de um canal serão ocultados. A mensagem ainda vai aparecer quando são adicionados a um canal, para que você possa receber uma notificação.",
+ "user.settings.advance.joinLeaveDesc": "Quando \"Ligado\", Mensagens do Sistema dizendo que um usuário entrou ou saiu de um canal serão visíveis. Quando \"Off\", as Mensagens do Sistema sobre entrada e saída de um canal serão ocultadas. Uma mensagem ainda vai aparecer quando você é adicionado a um canal para que você possa receber uma notificação.",
"user.settings.advance.joinLeaveTitle": "Ativar Mensagens Juntar/Deixar",
"user.settings.advance.markdown_preview": "Mostrar opção pré-visualização markdown na caixa de entrada de mensagens",
"user.settings.advance.off": "Desligado",
@@ -1944,10 +1969,10 @@
"user.settings.display.channelDisplayTitle": "Modo de Exibição do Canal",
"user.settings.display.channeldisplaymode": "Selecione a largura do centro do canal.",
"user.settings.display.clockDisplay": "Exibição do Relógio",
- "user.settings.display.collapseDesc": "Expandir links para mostrar pré-visualização de conteúdo, quando disponível.",
- "user.settings.display.collapseDisplay": "Visualização de link",
- "user.settings.display.collapseOff": "Desligado",
- "user.settings.display.collapseOn": "Ligado",
+ "user.settings.display.collapseDesc": "Set whether previews of image links show as expanded or collapsed by default. This setting can also be controlled using the slash commands /expand and /collapse.",
+ "user.settings.display.collapseDisplay": "Default appearance of image link previews",
+ "user.settings.display.collapseOff": "Collapsed",
+ "user.settings.display.collapseOn": "Expanded",
"user.settings.display.fixedWidthCentered": "Largura fixa, centralizada",
"user.settings.display.fontDesc": "Selecione a fonte mostrada na interface do usuário no Mattermost.",
"user.settings.display.fontTitle": "Fonte Exibição",
@@ -2023,7 +2048,7 @@
"user.settings.general.validEmail": "Por favor entre um endereço de e-mail válido",
"user.settings.general.validImage": "Somente imagens em JPG ou PNG podem ser usadas como imagem do perfil",
"user.settings.import_theme.cancel": "Cancelar",
- "user.settings.import_theme.importBody": "Para importar um tema, vá para uma equipe no Slack e olhe para “Preferences -> Sidebar Theme”. Abra a opção de tema personalizado, copie os valores das cores do tema e cole eles aqui:",
+ "user.settings.import_theme.importBody": "Para importar um tema, vá para uma equipe do Slack e procure por “Preferences -> Sidebar Theme”. Abra a opção de tema personalizado, copie os valores das cores do tema e cole-os aqui:",
"user.settings.import_theme.importHeader": "Importar Tema Slack",
"user.settings.import_theme.submit": "Enviar",
"user.settings.import_theme.submitError": "Formato inválido, por favor tente copiar e colar novamente.",
@@ -2130,7 +2155,9 @@
"user.settings.security.lastUpdated": "Última atualização {date} {time}",
"user.settings.security.ldap": "AD/LDAP",
"user.settings.security.loginGitlab": "Login feito através do GitLab",
+ "user.settings.security.loginGoogle": "Login feito através do Google Apps",
"user.settings.security.loginLdap": "Login feito através de AD/LDAP",
+ "user.settings.security.loginOffice365": "Login feito através do Office 365",
"user.settings.security.loginSaml": "Login feito através do SAML",
"user.settings.security.logoutActiveSessions": "Ver e fazer Logout das Sessões Ativas",
"user.settings.security.method": "Método de Login",
@@ -2159,9 +2186,11 @@
"user.settings.security.passwordErrorUppercaseNumberSymbol": "Sua senha deve conter pelo menos {min} caracteres constituídos por pelo menos uma letra maiúscula, pelo menos um número, e pelo menos um símbolo (ex. \"~!@#$%^&*()\").",
"user.settings.security.passwordErrorUppercaseSymbol": "Sua senha deve conter pelo menos {min} caracteres constituídos por pelo menos uma letra maiúscula e pelo menos um símbolo (ex. \"~!@#$%^&*()\").",
"user.settings.security.passwordGitlabCantUpdate": "Login ocorreu através do GitLab. Senha não pode ser atualizada.",
+ "user.settings.security.passwordGoogleCantUpdate": "Login ocorreu através do Google Apps. Senha não pode ser atualizada.",
"user.settings.security.passwordLdapCantUpdate": "Login ocorreu através de AD/LDAP. Senha não pode ser atualizada.",
"user.settings.security.passwordMatchError": "As novas senhas que você inseriu não correspondem.",
"user.settings.security.passwordMinLength": "Comprimento mínimo inválido, não é possível mostrar pré-visualização.",
+ "user.settings.security.passwordOffice365CantUpdate": "Login ocorreu através do Office 365. Senha não pode ser atualizada.",
"user.settings.security.passwordSamlCantUpdate": "Este campo é tratada pelo seu provedor de login. Se você quiser mudá-lo, você precisa fazê-lo através de seu provedor de login.",
"user.settings.security.retypePassword": "Digite Novamente a nova Senha",
"user.settings.security.saml": "SAML",
diff --git a/webapp/i18n/ru.json b/webapp/i18n/ru.json
index 0100cd97e..1036e365b 100644
--- a/webapp/i18n/ru.json
+++ b/webapp/i18n/ru.json
@@ -28,6 +28,7 @@
"activity_log.sessionsDescription": "Сессии создаются когда вы входите через новый браузер на устройстве. Они позволяют использовать Mattermost без необходимости повторного входа на протяжении времени установленого Системным Администратором. Если вы хотите выйти раньше, используйте кнопку 'Выход' ниже, чтобы завершить сессию.",
"activity_log_modal.android": "Android",
"activity_log_modal.androidNativeApp": "Приложение Android",
+ "activity_log_modal.desktop": "Native Desktop App",
"activity_log_modal.iphoneNativeApp": "Приложение для iPhone",
"add_command.autocomplete": "Автодополнение",
"add_command.autocomplete.help": "(Необязательно) Показывать слэш-команду в списке автодополнения.",
@@ -244,7 +245,7 @@
"admin.email.enableEmailBatchingTitle": "Включить почтовые объединения:",
"admin.email.fullPushNotification": "Послать полный фрагмент сообщения",
"admin.email.genericPushNotification": "Отправить общее описание с именами пользователей и каналов",
- "admin.email.inviteSaltDescription": "32-символа \"соли\" добавлено в приглашении по эл. почте. Случайно сгенерированных при установке.Кликните \"Перегенерировать\" чтобы создать новую \"соль\".",
+ "admin.email.inviteSaltDescription": "32-символа \"соли\" для подписи приглашений по эл. почте. При установке создается случайная \"соль\". Нажмите \"Перегенерировать\" чтобы создать новую.",
"admin.email.inviteSaltExample": "Например: \"bjlSR4QqkXFBr7TP4oDzlfZmcNuH9Yo\"",
"admin.email.inviteSaltTitle": "\"Соль\" для почтового приглашения:",
"admin.email.mhpns": "Используйте шифрованное, качественное HPNS соединение с iOS и Android приложениями",
@@ -260,6 +261,7 @@
"admin.email.notificationEmailTitle": "Адрес электронной почты для уведомлений:",
"admin.email.notificationOrganization": "Адрес эл. почты в нижнем колонтитуле:",
"admin.email.notificationOrganizationDescription": "Имя и адрес организации отображаемые в уведомлении по эл. почте от Mattermost, например \"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\". Если поле оставить пустым имя и адрес организации отображаться не будут.",
+ "admin.email.notificationOrganizationExample": "Например: \"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\"",
"admin.email.notificationsDescription": "Обычно включают в продуктивной системе. Если включено, Mattermost пытается отправить уведомление по эл. почте. Разработчики могут выключить чтобы уведомления не отправлялись.<br />Включение этой настройки удаляет баннер режима предпросмотра (требуется перезайти в систему для применения настроек).",
"admin.email.notificationsTitle": "Включить уведомления по электронной почте: ",
"admin.email.passwordSaltDescription": "32-символа \"соли\" добавлено в письмо сброса пароля. Они случайно сгенерированны при установке. Кликните \"Перегенерировать\" чтобы создать новую \"соль\".",
@@ -274,7 +276,7 @@
"admin.email.pushServerEx": "Например: \"http://push-test.mattermost.com\"",
"admin.email.pushServerTitle": "Сервер push уведомлений:",
"admin.email.pushTitle": "Включить push-уведомления: ",
- "admin.email.requireVerificationDescription": "Typically set to true in production. When true, Mattermost requires email verification after account creation prior to allowing login. Developers may set this field to false so skip sending verification emails for faster development.",
+ "admin.email.requireVerificationDescription": "Если истина, для разрешения входа Mattermost требует подтверждения адреса эл. почты после создания учетной записи. Обычно включается в production-системе. Разработчики могут отключить подтверждение адреса эл. почты для упрощения работы.",
"admin.email.requireVerificationTitle": "Требовать подтверждение адреса электронной почты: ",
"admin.email.selfPush": "Введите адрес сервиса отправки push-уведомлений вручную",
"admin.email.smtpPasswordDescription": " Получите эти данные от администратора, обслуживающего ваш сервер электронной почты.",
@@ -308,10 +310,20 @@
"admin.general.localization.serverLocaleTitle": "Язык сервера по умолчанию:",
"admin.general.log": "Ведение журнала",
"admin.general.policy": "Policy",
+ "admin.general.policy.allowEditPostAlways": "Any time",
+ "admin.general.policy.allowEditPostDescription": "Set policy on the length of time authors have to edit their messages after posting.",
+ "admin.general.policy.allowEditPostNever": "Никогда",
+ "admin.general.policy.allowEditPostTimeLimit": "seconds after posting",
+ "admin.general.policy.allowEditPostTitle": "Allow users to edit their messages:",
"admin.general.policy.permissionsAdmin": "Группа и Администраторы системы",
"admin.general.policy.permissionsAll": "Все участники команды",
"admin.general.policy.permissionsAllChannel": "Все участники канала",
+ "admin.general.policy.permissionsDeletePostAdmin": "Группа и Администраторы системы",
+ "admin.general.policy.permissionsDeletePostAll": "Message authors can delete their own messages, and Administrators can delete any message",
+ "admin.general.policy.permissionsDeletePostSystemAdmin": "Администраторы Системы",
"admin.general.policy.permissionsSystemAdmin": "Администраторы Системы",
+ "admin.general.policy.restrictPostDeleteDescription": "Set policy on who has permission to delete messages.",
+ "admin.general.policy.restrictPostDeleteTitle": "Allow which users to delete messages:",
"admin.general.policy.restrictPrivateChannelCreationDescription": "Установите политики того, кто может создавать приватные группы.",
"admin.general.policy.restrictPrivateChannelCreationTitle": "Включить возможность создания приватных групп для:",
"admin.general.policy.restrictPrivateChannelDeletionCommandLineToolLink": "командная строка",
@@ -363,6 +375,7 @@
"admin.image.amazonS3BucketExample": "Например: \"mattermost-media\"",
"admin.image.amazonS3BucketTitle": "Amazon S3 Корзина:",
"admin.image.amazonS3EndpointDescription": "Адрес вашего совместимого хранилища S3 . По умолчанию `s3.amazonaws.com`.",
+ "admin.image.amazonS3EndpointExample": "E.g.: \"s3.amazonaws.com\"",
"admin.image.amazonS3EndpointTitle": "Конечная точка Amazon S3:",
"admin.image.amazonS3IdDescription": "Получите эти учетные данные от своего администратора Amazon EC2.",
"admin.image.amazonS3IdExample": "Например: \"AKIADTOVBGERKLCBV\"",
@@ -510,12 +523,12 @@
"admin.log.logSettings": "Настройки журнала",
"admin.logs.reload": "Перезагрузить",
"admin.logs.title": "Серверные логи",
- "admin.metrics.enableDescription": "When true, Mattermost will enable performance monitoring collection and profiling. Please see <a href=\"http://docs.mattermost.com/deployment/metrics.html\" target='_blank'>documentation</a> to learn more about configuring performance monitoring for Mattermost.",
+ "admin.metrics.enableDescription": "Если включено, в Mattermost будет включен сбор данных о производительности и профилирование. Дополнительную информацию по конфигурации мониторинга производительности смотрите в <a href=\"http://docs.mattermost.com/deployment/metrics.html\" target='_blank'>документации</a>.",
"admin.metrics.enableTitle": "Включить мониторинг производительности",
- "admin.metrics.listenAddressDesc": "The address the server will listen on to expose performance metrics.",
- "admin.metrics.listenAddressEx": "Например: \":8067\"",
+ "admin.metrics.listenAddressDesc": "Адрес, прослушиваемый сервером для предоставления метрик производительности",
+ "admin.metrics.listenAddressEx": "Например: \":8065\"",
"admin.metrics.listenAddressTitle": "Прослушиваемый адрес:",
- "admin.mfa.bannerDesc": "Multi-factor authentication is only available for accounts with LDAP and email login methods. If there are users on your system with other login methods, it is recommended you set up multi-factor authentication directly with the SSO or SAML provider.",
+ "admin.mfa.bannerDesc": "Многофакторная проверка подлинности доступна только для учетных записей со входом через адрес эл. почты или LDAP. Если на вашей системе есть пользователи с другими методами входа, рекомендуем настроить многофакторную проверку подлинности напрямую через провайдер SSO или SAML.",
"admin.mfa.cluster": "Высокий",
"admin.mfa.title": "Включить многофакторную аутентификацию",
"admin.nav.help": "Помощь",
@@ -834,7 +847,7 @@
"admin.team.maxChannelsExample": "Например: \"100\"",
"admin.team.maxChannelsTitle": "Максимальное количество каналов на команду:",
"admin.team.maxNotificationsPerChannelDescription": "Maximum total number of users in a channel before users typing messages, @all, @here, and @channel no longer send notifications because of performance.",
- "admin.team.maxNotificationsPerChannelExample": "Например: \"1000\"",
+ "admin.team.maxNotificationsPerChannelExample": "Например: \"10000\"",
"admin.team.maxNotificationsPerChannelTitle": "Максимальное количество уведомлений на канал:",
"admin.team.maxUsersDescription": "Максимальное количество пользователей на команду.",
"admin.team.maxUsersExample": "Например: \"25\"",
@@ -866,7 +879,7 @@
"admin.team_analytics.totalPosts": "Всего сообщений",
"admin.true": "да",
"admin.userList.title": "Пользователи {team}",
- "admin.userList.title2": "Users for {team} ({count})",
+ "admin.userList.title2": "Пользователи для команды {team} ({count})",
"admin.user_item.authServiceEmail": ", <strong>Метод входа:</strong> Email",
"admin.user_item.authServiceNotEmail": ", <strong>Метод входа:</strong> {service}",
"admin.user_item.confirmDemoteDescription": "If you demote yourself from the System Admin role and there is not another user with System Admin privileges, you'll need to re-assign a System Admin by accessing the Mattermost server through a terminal and running the following command.",
@@ -923,8 +936,10 @@
"analytics.chart.meaningful": "Недостаточно данных для представления.",
"analytics.system.activeUsers": "Активные пользователи с сообщениями",
"analytics.system.channelTypes": "Типы канала",
+ "analytics.system.dailyActiveUsers": "Daily Active Users",
"analytics.system.expiredBanner": "Лицензия Enterprise истекает {date}. Свяжитесь с <a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a> и обновите лицензию в течение 15 дней.",
"analytics.system.expiringBanner": "Лицензия Enterprise истекает {date}. Для обновления лицензии свяжитесь с <a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>.",
+ "analytics.system.monthlyActiveUsers": "Monthly Active Users",
"analytics.system.postTypes": "Сообщения, файлы и хештэги",
"analytics.system.privateGroups": "Приватные Группы",
"analytics.system.publicChannels": "Публичные Каналы",
@@ -1022,7 +1037,6 @@
"backstage_sidebar.integrations.outgoing_webhooks": "Исходящие Webhook'и",
"calling_screen": "Вызов",
"center_panel.recent": "Нажмите здесь, чтобы перейти к последнему сообщению. ",
- "chanel_header.addMembers": "Добавить участников",
"change_url.close": "Закрыть",
"change_url.endWithLetter": "Должен заканчиваться буквой или цифрой",
"change_url.invalidUrl": "Некорректный URL",
@@ -1040,6 +1054,7 @@
"channel_flow.handleTooShort": "Ссылка на канал должна быть не короче 2-х буквенных символов",
"channel_flow.invalidName": "Недопустимое имя канала",
"channel_flow.set_url_title": "Установить адрес {term}",
+ "channel_header.addMembers": "Добавить участников",
"channel_header.addToFavorites": "Добавить в избранное",
"channel_header.channel": "Канал",
"channel_header.channelHeader": "Изменить заголовок канала",
@@ -1079,10 +1094,14 @@
"channel_loader.uploadedFile": " загрузил файл",
"channel_loader.uploadedImage": " загрузил изображение",
"channel_loader.wrote": " написал: ",
+ "channel_members_dropdown.channel_admin": "Channel Admin",
+ "channel_members_dropdown.channel_member": "Участники канала",
+ "channel_members_dropdown.make_channel_admin": "Make Channel Admin",
+ "channel_members_dropdown.make_channel_member": "Make Channel Member",
+ "channel_members_dropdown.remove_from_channel": "Remove From Channel",
+ "channel_members_dropdown.remove_member": "Remove Member",
"channel_members_modal.addNew": " Добавить участников",
- "channel_members_modal.close": "Закрыть",
- "channel_members_modal.remove": "Удалить",
- "channel_memebers_modal.members": " Участники",
+ "channel_members_modal.members": " Участники",
"channel_modal.cancel": "Отмена",
"channel_modal.channel": "Канал",
"channel_modal.createNew": "Создать ",
@@ -1091,6 +1110,7 @@
"channel_modal.edit": "Редактировать",
"channel_modal.group": "Группа",
"channel_modal.header": "Заголовок",
+ "channel_modal.headerEx": "E.g.: \"[Link Title](http://example.com)\"",
"channel_modal.headerHelp": "Задайте текст, который появится в заголовке {term} рядом с названием {term}. К примеру, вы можете включить часто используемые ссылки, введя [Текст ссылки](http://example.com).",
"channel_modal.modalTitle": "Новый ",
"channel_modal.name": "Имя",
@@ -1101,6 +1121,7 @@
"channel_modal.publicChannel1": "Создать общедоступный канал",
"channel_modal.publicChannel2": "Создать новый публичный канал. ",
"channel_modal.purpose": "Назначение",
+ "channel_modal.purposeEx": "E.g.: \"A channel to file bugs and improvements\"",
"channel_notifications.allActivity": "При любой активности",
"channel_notifications.allUnread": "При любых непрочитанных сообщениях",
"channel_notifications.globalDefault": "По умолчанию глобально ({notifyLevel})",
@@ -1113,6 +1134,7 @@
"channel_notifications.unreadInfo": "Название канала в боковом меню выделяется жирным, когда есть непрочитанные сообщения. Если выбрано \"Только при упоминаниях\" название канала будет выделяться только если Вас упомянут.",
"channel_select.placeholder": "--- Выбрать канал ---",
"channel_switch_modal.dm": "(Личное сообщение)",
+ "channel_switch_modal.failed_to_open": "Failed to open channel.",
"channel_switch_modal.help": "Напишите название канала. Используйте ↑↓ для перемещения, TAB для выбора, ↵ для подтверждения и ESC для отказа",
"channel_switch_modal.not_found": "Совпадений не найдено.",
"channel_switch_modal.submit": "Переключить",
@@ -1265,7 +1287,7 @@
"file_upload.limited": "Загрузка ограничена максимум {count} файлами(ом). Пожалуйста используйте дополнительные сообщения для отправки большего количества.more files.",
"file_upload.pasted": "Изображение вставлено в ",
"filtered_channels_list.count": "{count} {count, plural, =0 {0 channels} one {channel} other {channels}}",
- "filtered_channels_list.countTotal": "{count} {count, plural, =0 {0 channels} one {channel} other {channels}} of {total} Total",
+ "filtered_channels_list.countTotal": "{count} {count, plural, =0 {0 channels} one {channel} other {channels}} of {total} total",
"filtered_channels_list.search": "Поиск каналов",
"filtered_user_list.any_team": "Все пользователи",
"filtered_user_list.count": "{count} {count, plural, =0 {0 members} one {member} other {members}}",
@@ -1285,15 +1307,14 @@
"find_team.submitError": "Введите корректный email",
"flag_post.flag": "Отметить для отслеживания",
"flag_post.unflag": "Не помечено",
+ "general_tab.chooseDescription": "Введите новое имя для вашей команды",
"general_tab.chooseName": "Введите новое имя для вашей команды",
"general_tab.codeDesc": "Нажмите 'Редактировать' для перегенерации кода приглашения.",
- "general_tab.codeLongDesc": "Код приглашения используется как часть URL в ссылке прилашения в команду, созданная в разделе <strong>Получить ссылку для прилашения в команду</strong> в главном меню. Перегенерация создаст новую ссылку для приглашения и сделает предыдущую недействительной.",
+ "general_tab.codeLongDesc": "Код приглашения используется как часть URL в ссылке приглашения в команду, созданная в разделе {getTeamInviteLink} в главном меню. Пересоздание создаст новую ссылку для приглашения и сделает предыдущую недействительной.",
"general_tab.codeTitle": "Код приглашения",
- "general_tab.dirDisabled": "Team Directory has been disabled. Please ask a System Admin to enable the Team Directory in the System Console team settings.",
- "general_tab.dirOff": "Team Directory выключена для этой системы.",
"general_tab.emptyDescription": "Нажмите 'Редактировать' для добавления описания.",
+ "general_tab.getTeamInviteLink": "Ссылка для приглашения",
"general_tab.includeDirDesc": "Including this team will display the team name from the Team Directory section of the Home Page, and provide a link to the sign-in page.",
- "general_tab.includeDirTitle": "Добавить команду в список команд",
"general_tab.no": "Нет",
"general_tab.openInviteDesc": "Если разрешено, ссылка на эту команду будет отображаться на странице входа, позволяя любому пользователю войти в команду.",
"general_tab.openInviteTitle": "Разрешить вход любому пользователю с учетной записью на этом сервере",
@@ -1550,6 +1571,7 @@
"login.passwordChanged": " Пароль успешно обновлён",
"login.session_expired": " Сессия истекла. Пожалуйста, войдите заново",
"login.signIn": "Войти",
+ "login.signInLoading": "Signing in...",
"login.signInWith": "Войти с помощью:",
"login.userNotFound": "Мы не обнаружили аккаунт по вашим данным для входа.",
"login.username": "Имя пользователя",
@@ -1561,8 +1583,10 @@
"member_item.makeAdmin": "Сделать администратором",
"member_item.member": "Участник",
"member_list.noUsersAdd": "Нет пользователей для добавления.",
+ "members_popover.manageMembers": "Управление участниками",
"members_popover.msg": "Сообщение",
"members_popover.title": "Участники",
+ "members_popover.viewMembers": "Просмотреть список участников",
"mfa.confirm.complete": "<strong>Настройка завершена!</strong>",
"mfa.confirm.okay": "Понятно",
"mfa.confirm.secure": "Теперь ваш аккаунт защищён. В следующий раз будет запрошен ввод кода из Google Authentificator.",
@@ -1665,6 +1689,7 @@
"post_info.mobile.unflag": "Не помечено",
"post_info.permalink": "Постоянная ссылка",
"post_info.reply": "Ответить",
+ "post_message_view.edited": "(edited)",
"posts_view.loadMore": "Больше сообщений",
"posts_view.newMsg": "Новые сообщения",
"posts_view.newMsgBelow": "New {count, plural, one {message} other {messages}} below",
@@ -1677,7 +1702,7 @@
"reaction.reactionVerb.you": "reacted",
"reaction.reactionVerb.youAndUsers": "reacted",
"reaction.usersAndOthersReacted": "{users} and {otherUsers, number} other {otherUsers, plural, one {user} other {users}}",
- "reaction.usersReacted": "{users} and {lastUser}",
+ "reaction.usersReacted": "{users} и {lastUser}",
"reaction.you": "Вы",
"removed_channel.channelName": "канал",
"removed_channel.from": "Удалено из ",
@@ -1809,8 +1834,8 @@
"signup_user_completed.required": "Обязательное поле",
"signup_user_completed.reserved": "Имя зарезервировано, выберите другое.",
"signup_user_completed.signIn": "Щёлкните здесь для входа.",
- "signup_user_completed.userHelp": "Username must begin with a letter, and contain between {min} to {max} lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'",
- "signup_user_completed.usernameLength": "Username must begin with a letter, and contain between {min} to {max} lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'.",
+ "signup_user_completed.userHelp": "Имя пользователя должно начинаться с латинской буквы и содержать от {min} до {max} символов, включающих цифры, латинские буквы, а также символы '.', '-' и '_'.",
+ "signup_user_completed.usernameLength": "Имя пользователя должно начинаться с латинской буквы и содержать от {min} до {max} символов, включающих цифры, латинские буквы, а также символы '.', '-' и '_'.",
"signup_user_completed.validEmail": "Пожалуйста, введите корректный адрес электронной почты",
"signup_user_completed.welcome": "Добро пожаловать:",
"signup_user_completed.whatis": "Ваш адрес электронной почты?",
@@ -1868,7 +1893,7 @@
"team_settings_modal.generalTab": "Общие",
"team_settings_modal.importTab": "Импорт",
"team_settings_modal.title": "Настройки команды",
- "team_sidebar.join": "Other teams you can join.",
+ "team_sidebar.join": "Другие команды, к которым вы можете присоединиться.",
"textbox.bold": "**жирный**",
"textbox.edit": "Редактировать сообщение",
"textbox.help": "Помощь",
@@ -1877,7 +1902,7 @@
"textbox.preformatted": "```преформатированный```",
"textbox.preview": "Предпросмотр",
"textbox.quote": ">цитата",
- "textbox.strike": "strike",
+ "textbox.strike": "зачеркнутый",
"tutorial_intro.allSet": "Теперь всё готово!",
"tutorial_intro.end": "Нажмите “Далее” для входа в {channel}. Это первый канал, который пользователи команды видят после входа. Используйте его для общения всей команды.",
"tutorial_intro.invite": "Пригласить товарищей",
@@ -1899,7 +1924,7 @@
"update_command.question": "Your changes may break the existing slash command. Are you sure you would like to update it?",
"update_command.update": "Обновить",
"upload_overlay.info": "Бросьте сюда файл, чтобы загрузить его.",
- "user.settings.advance.embed_preview": "Показывать \"экспериментальный\" превью в ссылках, когда доступно",
+ "user.settings.advance.embed_preview": "For the first web link in a message, display a preview of website content below the message, if available",
"user.settings.advance.embed_toggle": "Показывать переключатель для всех встроенных превью",
"user.settings.advance.enabledFeatures": "{count, number} {count, plural, one {Feature} other {Features}} Enabled",
"user.settings.advance.formattingDesc": "If enabled, posts will be formatted to create links, show emoji, style the text, and add line breaks. By default, this setting is enabled. Changing this setting requires the page to be refreshed.",
@@ -1944,10 +1969,10 @@
"user.settings.display.channelDisplayTitle": "Режим отображения канала",
"user.settings.display.channeldisplaymode": "Выберите ширину центрального канала.",
"user.settings.display.clockDisplay": "Отображение времени",
- "user.settings.display.collapseDesc": "Раскрывать ссылки и показывать предпросмотр содержимого, если возможно.",
- "user.settings.display.collapseDisplay": "Предварительный просмотр ссылок",
- "user.settings.display.collapseOff": "Выкл",
- "user.settings.display.collapseOn": "Вкл",
+ "user.settings.display.collapseDesc": "Set whether previews of image links show as expanded or collapsed by default. This setting can also be controlled using the slash commands /expand and /collapse.",
+ "user.settings.display.collapseDisplay": "Default appearance of image link previews",
+ "user.settings.display.collapseOff": "Collapsed",
+ "user.settings.display.collapseOn": "Expanded",
"user.settings.display.fixedWidthCentered": "По центру, фиксировано по ширине",
"user.settings.display.fontDesc": "Выберите шрифт пользовательского интерфейса Mattermost.",
"user.settings.display.fontTitle": "Шрифт",
@@ -2130,7 +2155,9 @@
"user.settings.security.lastUpdated": "Последние изменения: {date} в {time}",
"user.settings.security.ldap": "AD/LDAP",
"user.settings.security.loginGitlab": "Вход выполнен с помощью GitLab",
+ "user.settings.security.loginGoogle": "Login done through Google Apps",
"user.settings.security.loginLdap": "Вход выполнен через AD/LDAP",
+ "user.settings.security.loginOffice365": "Login done through Office 365",
"user.settings.security.loginSaml": "Вход выполнен с помощью SAML",
"user.settings.security.logoutActiveSessions": "Просмотр и завершение активных сессий",
"user.settings.security.method": "Методы входа",
@@ -2159,9 +2186,11 @@
"user.settings.security.passwordErrorUppercaseNumberSymbol": "Пароль должен быть не короче {min} символов и содержать хотя бы один символ в верхнем регистре, хотя бы одну цифру и хотя бы один символ пунктуации (например, \"~!@#$%^&*()\").",
"user.settings.security.passwordErrorUppercaseSymbol": "Пароль должен быть не короче {min} символов и содержать хотя бы одну цифру и хотя бы один специальный символ (например, \"~!@#$%^&*()\").",
"user.settings.security.passwordGitlabCantUpdate": "Вход произошел через GitLab. Пароль не может быть изменен.",
+ "user.settings.security.passwordGoogleCantUpdate": "Вход произошел через GitLab. Пароль не может быть изменен.",
"user.settings.security.passwordLdapCantUpdate": "Вход произведен через AD/LDAP. Пароль не может быть обновлен.",
"user.settings.security.passwordMatchError": "Введенные пароли не совпадают.",
"user.settings.security.passwordMinLength": "Длина пароля меньше минимальной, предпросмотр невозможен.",
+ "user.settings.security.passwordOffice365CantUpdate": "Вход произошел через GitLab. Пароль не может быть изменен.",
"user.settings.security.passwordSamlCantUpdate": "This field is handled through your login provider. If you want to change it, you need to do so through your login provider.",
"user.settings.security.retypePassword": "Повторите новый пароль",
"user.settings.security.saml": "SAML",
diff --git a/webapp/i18n/zh_CN.json b/webapp/i18n/zh_CN.json
index 2599452f4..ed814e813 100644
--- a/webapp/i18n/zh_CN.json
+++ b/webapp/i18n/zh_CN.json
@@ -28,6 +28,7 @@
"activity_log.sessionsDescription": "当您在设备的新浏览器中登录时,将创建会话。会话让您使用Mattermost时无需在系统管理员限定的时间段内重新登录。如果您希望早些退出,点击下方的‘注销’按钮结束会话。",
"activity_log_modal.android": "安卓",
"activity_log_modal.androidNativeApp": "Android本地App",
+ "activity_log_modal.desktop": "电脑应用",
"activity_log_modal.iphoneNativeApp": "iPhone本地App",
"add_command.autocomplete": "自动完成",
"add_command.autocomplete.help": "(可选) 在自动完成列表显示斜杠命令。",
@@ -251,15 +252,16 @@
"admin.email.mhpnsHelp": "从 iTunes下载 <a href=\"https://itunes.apple.com/us/app/mattermost/id984966508?mt=8\" target='_blank'>Mattermost iOS app</a>。从 Google Play 下载 <a href=\"https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en\" target='_blank'>Mattermost Android app</a>。 了解更多 <a href=\"http://docs.mattermost.com/deployment/push.html#hosted-push-notifications-service-hpns\" target='_blank'>HPNS</a>。",
"admin.email.mtpns": "在iTunes和TPNS的谷歌Play使用iOS和Android应用程序",
"admin.email.mtpnsHelp": "从 iTunes下载 <a href=\"https://itunes.apple.com/us/app/mattermost/id984966508?mt=8\" target='_blank'>Mattermost iOS app</a>。从 Google Play 下载 <a href=\"https://play.google.com/store/apps/details?id=com.mattermost.mattermost&hl=en\" target='_blank'>Mattermost Android app</a>。 了解更多 <a href=\"http://docs.mattermost.com/deployment/push.html#test-push-notifications-service-tpns\" target='_blank'>TPNS</a>。",
- "admin.email.nofificationOrganizationExample": "例如 \"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\"",
+ "admin.email.nofificationOrganizationExample": "例如:\"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\"",
"admin.email.notificationDisplayDescription": "从 Mattermost 发送的电子邮件通知时显示的电子邮件帐号名。",
- "admin.email.notificationDisplayExample": "例如: \"Mattermost通知\", \"系统\", \"无答复\"",
+ "admin.email.notificationDisplayExample": "例如:\"Mattermost通知\", \"系统\", \"无答复\"",
"admin.email.notificationDisplayTitle": "通知显示名称:",
"admin.email.notificationEmailDescription": "从 Mattermost 发送的电子邮件通知时显示的电子邮件地址。",
- "admin.email.notificationEmailExample": "例如: \"mattermost@yourcompany.com\", \"admin@yourcompany.com\"",
+ "admin.email.notificationEmailExample": "例如:\"mattermost@yourcompany.com\", \"admin@yourcompany.com\"",
"admin.email.notificationEmailTitle": "通知邮件地址:",
"admin.email.notificationOrganization": "通知页脚地址:",
"admin.email.notificationOrganizationDescription": "从mattermost电子邮件通知显示组织机构名称和地址,如“©ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\"。如果字段为空,则将不显示该组织的名称和地址。",
+ "admin.email.notificationOrganizationExample": "例如:\"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\"",
"admin.email.notificationsDescription": "通常在正式环境中设置为是。当设为是时,Mattermost 将尝试发送电子邮件通知。开发人员可以设置为否以跳过电子邮件设置来加快开发速度。<br />置此为是时将删除预览模式横幅 (设置后需要注销后重新登录才生效)。",
"admin.email.notificationsTitle": "启用发送邮件通知:",
"admin.email.passwordSaltDescription": "32字盐值用来签署重置密码邮件。由安装时随机生成。点击 \"重新生成\" 生成新的盐。",
@@ -278,7 +280,7 @@
"admin.email.requireVerificationTitle": "要求电子邮件验证:",
"admin.email.selfPush": "手动输入推送通知服务位置",
"admin.email.smtpPasswordDescription": "从邮件服务器管理员获得此凭据。",
- "admin.email.smtpPasswordExample": "例如: \"yourpassword\", \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
+ "admin.email.smtpPasswordExample": "例如:\"yourpassword\", \"jcuS8PuvcpGhpgHhlcpT1Mx42pnqMxQY\"",
"admin.email.smtpPasswordTitle": "SMTP服务器密码:",
"admin.email.smtpPortDescription": "SMTP邮件服务器端口。",
"admin.email.smtpPortExample": "例如:\"25\", \"465\", \"587\"",
@@ -287,7 +289,7 @@
"admin.email.smtpServerExample": "例如:\"smtp.yourcompany.com\", \"email-smtp.us-east-1.amazonaws.com\"",
"admin.email.smtpServerTitle": "SMTP服务器:",
"admin.email.smtpUsernameDescription": "从邮件服务器管理员获得此凭据。",
- "admin.email.smtpUsernameExample": "例如: \"admin@yourcompany.com\", \"AKIADTOVBGERKLCBV\"",
+ "admin.email.smtpUsernameExample": "例如:\"admin@yourcompany.com\", \"AKIADTOVBGERKLCBV\"",
"admin.email.smtpUsernameTitle": "SMTP服务器用户名:",
"admin.email.testing": "测试中...",
"admin.false": "否",
@@ -308,10 +310,20 @@
"admin.general.localization.serverLocaleTitle": "默认服务器语言:",
"admin.general.log": "日志",
"admin.general.policy": "策略",
+ "admin.general.policy.allowEditPostAlways": "Any time",
+ "admin.general.policy.allowEditPostDescription": "Set policy on the length of time authors have to edit their messages after posting.",
+ "admin.general.policy.allowEditPostNever": "从不",
+ "admin.general.policy.allowEditPostTimeLimit": "seconds after posting",
+ "admin.general.policy.allowEditPostTitle": "Allow users to edit their messages:",
"admin.general.policy.permissionsAdmin": "团队和系统管理员",
"admin.general.policy.permissionsAll": "所有团队成员",
"admin.general.policy.permissionsAllChannel": "所有频道成员",
+ "admin.general.policy.permissionsDeletePostAdmin": "团队和系统管理员",
+ "admin.general.policy.permissionsDeletePostAll": "Message authors can delete their own messages, and Administrators can delete any message",
+ "admin.general.policy.permissionsDeletePostSystemAdmin": "系统管理员",
"admin.general.policy.permissionsSystemAdmin": "系统管理员",
+ "admin.general.policy.restrictPostDeleteDescription": "Set policy on who has permission to delete messages.",
+ "admin.general.policy.restrictPostDeleteTitle": "Allow which users to delete messages:",
"admin.general.policy.restrictPrivateChannelCreationDescription": "设置谁可以创建私有组的策略。",
"admin.general.policy.restrictPrivateChannelCreationTitle": "开启创建私有组:",
"admin.general.policy.restrictPrivateChannelDeletionCommandLineToolLink": "命令符工具",
@@ -363,6 +375,7 @@
"admin.image.amazonS3BucketExample": "例如 \"mattermost-media\"",
"admin.image.amazonS3BucketTitle": "Amazon S3 存储桶:",
"admin.image.amazonS3EndpointDescription": "您的 S3 兼容储存提供商的主机名称。默认为 `s3.amazonaws.com`。",
+ "admin.image.amazonS3EndpointExample": "例如:\"s3.amazonaws.com\"",
"admin.image.amazonS3EndpointTitle": "亚马逊 S3 连接点:",
"admin.image.amazonS3IdDescription": "从您的Amazon EC2管理员获得此证书。",
"admin.image.amazonS3IdExample": "例如 \"AKIADTOVBGERKLCBV\"",
@@ -467,7 +480,7 @@
"admin.ldap.testSuccess": "AD/LDAP 测试成功",
"admin.ldap.uernameAttrDesc": "AD/LDAP 服务器中属性用于填充 Mattermost 用户名属性。这可以和 ID 属性一致。",
"admin.ldap.userFilterDisc": "(可选) 输入个AD/LDAP筛选器用在搜索用户对象。只有被查询条件选中的用户才能访问 Mattermost。对于活动目录,过滤禁用用户的查询是(&(objectCategory=Person)(!(UserAccountControl:1.2.840.113556.1.4.803:=2)))。",
- "admin.ldap.userFilterEx": "例如 \"(objectClass=user)\"",
+ "admin.ldap.userFilterEx": "例如:\"(objectClass=user)\"",
"admin.ldap.userFilterTitle": "用户筛选器:",
"admin.ldap.usernameAttrEx": "例如 \"sAMAccountName\"",
"admin.ldap.usernameAttrTitle": "用户名属性:",
@@ -513,7 +526,7 @@
"admin.metrics.enableDescription": "当设置为是时,Mattermost 会启用性能监控收集和分析。请查看<a href=\"http://docs.mattermost.com/deployment/metrics.html\" target='_blank'>文档</a>了解更多Mattermost 性能监控配置信息。",
"admin.metrics.enableTitle": "开启性能监视:",
"admin.metrics.listenAddressDesc": "服务端监听的地址以公开性能指标数据。",
- "admin.metrics.listenAddressEx": "例如 \":8067\"",
+ "admin.metrics.listenAddressEx": "例如:\":8065\"",
"admin.metrics.listenAddressTitle": "监听地址:",
"admin.mfa.bannerDesc": "多重验证只能用在使用 LDAP 或邮箱地址登入方式的帐号。如果您的系统有用户使用其他登入方式,我们推荐您在 SSO 或 SAML 提供商直接设置多重验证。",
"admin.mfa.cluster": "高",
@@ -564,7 +577,7 @@
"admin.rate.enableLimiterTitle": "启用频率限制:",
"admin.rate.httpHeaderDescription": "填充时,变化率限制通过HTTP头字段指定(例如当配置NGINX\"X-Real-IP\",当配置AmazonELB为\"X-Forwarded-For\").",
"admin.rate.httpHeaderExample": "例如 \"X-Real-IP\", \"X-Forwarded-For\"",
- "admin.rate.httpHeaderTitle": "通过HTTP头变化频率限制",
+ "admin.rate.httpHeaderTitle": "通过HTTP头变化频率限制:",
"admin.rate.maxBurst": "最大过载大小:",
"admin.rate.maxBurstDescription": "超过每秒查询限制的最大请求数。",
"admin.rate.maxBurstExample": "例如 \"100\"",
@@ -831,10 +844,10 @@
"admin.team.dirDesc": "当设置为是时,设置为显示在团队目录里的团队会在主页显示并替代创建新的团队的位置。",
"admin.team.dirTitle": "启用团队目录:",
"admin.team.maxChannelsDescription": "每个团队最多频道数,包括活动的和已删除的频道。",
- "admin.team.maxChannelsExample": "例如 \"100\"",
+ "admin.team.maxChannelsExample": "例如:\"100\"",
"admin.team.maxChannelsTitle": "每团队最多频道数:",
"admin.team.maxNotificationsPerChannelDescription": "因性能限制输入消息、@all、@here 以及 @channel 发通知的最大频道总用户数 。",
- "admin.team.maxNotificationsPerChannelExample": "例如 \"1000\"",
+ "admin.team.maxNotificationsPerChannelExample": "例如:\"10000\"",
"admin.team.maxNotificationsPerChannelTitle": "每频道最大通知数:",
"admin.team.maxUsersDescription": "每个团队最多用户数,包括启用的和停用的用户。",
"admin.team.maxUsersExample": "例如 \"25\"",
@@ -901,13 +914,13 @@
"admin.webrtc.gatewayWebsocketUrlTitle": "网关 Websocket 网址:",
"admin.webrtc.stunUriDescription": "输入您的 STUN 网址 stun:<your-stun-url>:<port>。STUN 是一个网络协议标准用于让一个主机帮助双方都在 NAT 背后的用户连接到公网 IP。",
"admin.webrtc.stunUriExample": "例如 \"stun:webrtc.mattermost.com:5349\"",
- "admin.webrtc.stunUriTitle": "STUN URI",
+ "admin.webrtc.stunUriTitle": "STUN URI:",
"admin.webrtc.turnSharedKeyDescription": "输入您的 TURN 服务器共享密钥。此用来生成动态密码来创建连接。每个密码只有短暂有效期。",
"admin.webrtc.turnSharedKeyExample": "例如 \"bXdkOWQxc3d0Ynk3emY5ZmsxZ3NtazRjaWg=\"",
"admin.webrtc.turnSharedKeyTitle": "TURN 共享密钥:",
"admin.webrtc.turnUriDescription": "输入您的 TURN 网址以格式 turn:<your-turn-url>:<port>。TURN 是个标准的网络协议能协助双方都在NAT后时创建连接。",
"admin.webrtc.turnUriExample": "例如 \"turn:webrtc.mattermost.com:5349\"",
- "admin.webrtc.turnUriTitle": "TURN URI",
+ "admin.webrtc.turnUriTitle": "TURN URI:",
"admin.webrtc.turnUsernameDescription": "输入您的 TURN 服务器用户名。",
"admin.webrtc.turnUsernameExample": "例如 \"myusername\"",
"admin.webrtc.turnUsernameTitle": "TURN 用户名:",
@@ -923,8 +936,10 @@
"analytics.chart.meaningful": "没有足够的数据进行有意义的表示。",
"analytics.system.activeUsers": "有发信息的的正常用户",
"analytics.system.channelTypes": "频道类型",
+ "analytics.system.dailyActiveUsers": "Daily Active Users",
"analytics.system.expiredBanner": "企业授权已在 {date} 过期。您在即日起有15天时间更新授权,请联系 <a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>。",
"analytics.system.expiringBanner": "企业授权已在 {date} 过期。请更新授权,详情请联系 <a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>。",
+ "analytics.system.monthlyActiveUsers": "Monthly Active Users",
"analytics.system.postTypes": "发文,文件和标签",
"analytics.system.privateGroups": "私有组",
"analytics.system.publicChannels": "公共频道",
@@ -1022,7 +1037,6 @@
"backstage_sidebar.integrations.outgoing_webhooks": "对外Webhooks",
"calling_screen": "呼叫中",
"center_panel.recent": "点击这里跳转到最近的消息。",
- "chanel_header.addMembers": "添加成员",
"change_url.close": "关闭",
"change_url.endWithLetter": "必须以字母或数字结尾",
"change_url.invalidUrl": "无效的网址",
@@ -1040,6 +1054,7 @@
"channel_flow.handleTooShort": "频道网址必须为至少2个小写英文数字字符",
"channel_flow.invalidName": "无效的频道名称",
"channel_flow.set_url_title": "设置{term}URL",
+ "channel_header.addMembers": "添加成员",
"channel_header.addToFavorites": "添加到收藏",
"channel_header.channel": "频道",
"channel_header.channelHeader": "编辑频道标题",
@@ -1079,10 +1094,14 @@
"channel_loader.uploadedFile": "上传文件",
"channel_loader.uploadedImage": "上传图片",
"channel_loader.wrote": "写到:",
+ "channel_members_dropdown.channel_admin": "频道管理员",
+ "channel_members_dropdown.channel_member": "频道成员",
+ "channel_members_dropdown.make_channel_admin": "Make Channel Admin",
+ "channel_members_dropdown.make_channel_member": "Make Channel Member",
+ "channel_members_dropdown.remove_from_channel": "从频道移除",
+ "channel_members_dropdown.remove_member": "移除成员",
"channel_members_modal.addNew": "添加新成员",
- "channel_members_modal.close": "关闭",
- "channel_members_modal.remove": "移除",
- "channel_memebers_modal.members": "成员",
+ "channel_members_modal.members": " 位成员",
"channel_modal.cancel": "取消",
"channel_modal.channel": "频道",
"channel_modal.createNew": "创建新",
@@ -1091,6 +1110,7 @@
"channel_modal.edit": "编辑",
"channel_modal.group": "群组",
"channel_modal.header": "标题",
+ "channel_modal.headerEx": "例如:\"[链接标题](http://example.com)\"",
"channel_modal.headerHelp": "设定在 {term} 标题里在 {term} 旁边的文字。举例,输入常见链接 [链接标题](http://example.com)。",
"channel_modal.modalTitle": "新建",
"channel_modal.name": "名称",
@@ -1101,6 +1121,7 @@
"channel_modal.publicChannel1": "创建一个公共频道",
"channel_modal.publicChannel2": "创建一个任何人都能加入的新公共频道。",
"channel_modal.purpose": "用途",
+ "channel_modal.purposeEx": "例如:\"用于提交问题和建议的频道\"",
"channel_notifications.allActivity": "所有操作",
"channel_notifications.allUnread": "所有未读消息",
"channel_notifications.globalDefault": "默认全局({notifyLevel})",
@@ -1113,6 +1134,7 @@
"channel_notifications.unreadInfo": "有未读消息时,侧边栏的频道名称粗体显示。只有当您被提及时选择“仅对提及”会加粗频道名称。",
"channel_select.placeholder": "--- 选择一个频道 ---",
"channel_switch_modal.dm": "(私信)",
+ "channel_switch_modal.failed_to_open": "Failed to open channel.",
"channel_switch_modal.help": "输入频道名。↑↓浏览,TAB选择,↵确认,ESC取消",
"channel_switch_modal.not_found": "无匹配项。",
"channel_switch_modal.submit": "切换",
@@ -1285,15 +1307,14 @@
"find_team.submitError": "请输入一个有效的电子邮件地址",
"flag_post.flag": "标记以跟进",
"flag_post.unflag": "取消标记",
+ "general_tab.chooseDescription": "请为您的团队选个新的描述",
"general_tab.chooseName": "请选择一个新的名称为你的团队",
"general_tab.codeDesc": "点击 \"编辑\" 重新生成邀请码。",
- "general_tab.codeLongDesc": "邀请码用来在主菜单里使用<strong>获取团队邀请链接</strong>所产生的团队邀请链接中的一部分。重新生成一个新的团队邀请链接将使之前的链接无效。",
+ "general_tab.codeLongDesc": "作为团队邀请链接中URL的一部分,邀请码在主菜单中由 {getTeamInviteLink} 创建。重新生成创建一个新的团队邀请链接将使之前的链接无效。",
"general_tab.codeTitle": "邀请码",
- "general_tab.dirDisabled": "团队目录已被禁用。请联系系统管理员到系统控制台中启用团队目录。",
- "general_tab.dirOff": "此系统的团队目录已关闭。",
"general_tab.emptyDescription": "点击 '修改' 添加团队描述。",
+ "general_tab.getTeamInviteLink": "获取团队邀请链接",
"general_tab.includeDirDesc": "包含此团队将在首页的团队目录里显示该团队名,并提供一个链接到登陆页面。",
- "general_tab.includeDirTitle": "团队目录中已包括这个团队",
"general_tab.no": "否",
"general_tab.openInviteDesc": "允许时,此团队的链接将会显示在首页让任何有帐号的用户可以加入此团队。",
"general_tab.openInviteTitle": "允许任何在本服务器上的用户加入此团队",
@@ -1550,6 +1571,7 @@
"login.passwordChanged": "成功更新密码",
"login.session_expired": "您的会话已过期,请重新登录。",
"login.signIn": "登录",
+ "login.signInLoading": "登入中...",
"login.signInWith": "登录使用:",
"login.userNotFound": "我们找不到现有的帐户匹配您的凭证。",
"login.username": "用户名",
@@ -1561,8 +1583,10 @@
"member_item.makeAdmin": "Admin",
"member_item.member": "成员",
"member_list.noUsersAdd": "没有用户可添加。",
+ "members_popover.manageMembers": "成员管理",
"members_popover.msg": "消息",
"members_popover.title": "成员",
+ "members_popover.viewMembers": "查看成员",
"mfa.confirm.complete": "<strong>设置完成!</strong>",
"mfa.confirm.okay": "确定",
"mfa.confirm.secure": "您的帐号现在安全了。下次登入时,您将要求输入 Google Authenticator 应用提供的令牌。",
@@ -1665,6 +1689,7 @@
"post_info.mobile.unflag": "取消标记",
"post_info.permalink": "永久链接",
"post_info.reply": "回复",
+ "post_message_view.edited": "(已编辑)",
"posts_view.loadMore": "载入更多消息",
"posts_view.newMsg": "新消息",
"posts_view.newMsgBelow": "以下有 {count} 个新消息",
@@ -1899,7 +1924,7 @@
"update_command.question": "您的修改可能破坏现有的斜杠命令。您确定要更新吗?",
"update_command.update": "更新",
"upload_overlay.info": "拖动文件上传。",
- "user.settings.advance.embed_preview": "可用时,显示实验性链接内容预览",
+ "user.settings.advance.embed_preview": "For the first web link in a message, display a preview of website content below the message, if available",
"user.settings.advance.embed_toggle": "显示切换所有嵌入预览",
"user.settings.advance.enabledFeatures": "已启用 {count, number} 项功能",
"user.settings.advance.formattingDesc": "开启时,文章会显示链接,表情符,格式,以及添加断行。默认下,此选项时开启的。修改此设定需要刷新页面。",
@@ -1944,10 +1969,10 @@
"user.settings.display.channelDisplayTitle": "频道显示模式",
"user.settings.display.channeldisplaymode": "选择中间栏的宽度。",
"user.settings.display.clockDisplay": "时钟显示",
- "user.settings.display.collapseDesc": "可用时,展开链接显示内容预览。",
- "user.settings.display.collapseDisplay": "链接预览",
- "user.settings.display.collapseOff": "关闭",
- "user.settings.display.collapseOn": "开启",
+ "user.settings.display.collapseDesc": "Set whether previews of image links show as expanded or collapsed by default. This setting can also be controlled using the slash commands /expand and /collapse.",
+ "user.settings.display.collapseDisplay": "Default appearance of image link previews",
+ "user.settings.display.collapseOff": "Collapsed",
+ "user.settings.display.collapseOn": "Expanded",
"user.settings.display.fixedWidthCentered": "固定宽度,居中",
"user.settings.display.fontDesc": "选择在Mattermost用户界面显示的字体。",
"user.settings.display.fontTitle": "显示字体",
@@ -2130,7 +2155,9 @@
"user.settings.security.lastUpdated": "上次更新时间{date}{time}",
"user.settings.security.ldap": "AD/LDAP",
"user.settings.security.loginGitlab": "用 GitLab 登录",
+ "user.settings.security.loginGoogle": "Login done through Google Apps",
"user.settings.security.loginLdap": "用 AD/LDAP 登录",
+ "user.settings.security.loginOffice365": "Login done through Office 365",
"user.settings.security.loginSaml": "用 SAML 登录",
"user.settings.security.logoutActiveSessions": "查看并退出正在执行的会话",
"user.settings.security.method": "登录方式",
@@ -2159,9 +2186,11 @@
"user.settings.security.passwordErrorUppercaseNumberSymbol": "您的密码必须包含至少 {min} 个字符且至少有一个大写字母,一个数字,以及一个符号(如\"~!@#$%^&*()\")。",
"user.settings.security.passwordErrorUppercaseSymbol": "您的密码必须包含至少 {min} 个字符且至少有一个大写字母以及一个符号(如\"~!@#$%^&*()\")。",
"user.settings.security.passwordGitlabCantUpdate": "通过GitLab登录。电子邮件不能被更新。",
+ "user.settings.security.passwordGoogleCantUpdate": "通过 Google Apps 登录。密码不能被更新。",
"user.settings.security.passwordLdapCantUpdate": "通过 AD/LDAP 登录。密码不能被更新。",
"user.settings.security.passwordMatchError": "您输入的新密码不一致。",
"user.settings.security.passwordMinLength": "无效最小长度,无法显示预览。",
+ "user.settings.security.passwordOffice365CantUpdate": "通过 Office 365 登录。密码不能被更新。",
"user.settings.security.passwordSamlCantUpdate": "此栏由您的登入提供商决定。如果您想更改,您需要到您的登入提供者改动。",
"user.settings.security.retypePassword": "再次输入新密码",
"user.settings.security.saml": "SAML",
diff --git a/webapp/i18n/zh_TW.json b/webapp/i18n/zh_TW.json
index 63078772f..d7b8d6f86 100644
--- a/webapp/i18n/zh_TW.json
+++ b/webapp/i18n/zh_TW.json
@@ -28,6 +28,7 @@
"activity_log.sessionsDescription": "當使用新的瀏覽器登入時工作階段會被建立。工作階段讓您可以在系統管理員設定的時間內不用重複登入。如果想先行登出,請使用下方的 '登出' 按鈕以結束工作階段。",
"activity_log_modal.android": "Android",
"activity_log_modal.androidNativeApp": "Android 原生應用程式",
+ "activity_log_modal.desktop": "Native Desktop App",
"activity_log_modal.iphoneNativeApp": "iPhone 原生應用程式",
"add_command.autocomplete": "自動完成",
"add_command.autocomplete.help": "在自動完成列表上顯示斜線命令(非必須)。",
@@ -260,6 +261,7 @@
"admin.email.notificationEmailTitle": "通知信寄件人地址:",
"admin.email.notificationOrganization": "通知信信尾地址:",
"admin.email.notificationOrganizationDescription": "顯示於來自 Mattermost 通知信的組織名稱跟地址。如:\"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\"。如果這欄為空,信將不會顯示組織名稱跟地址。",
+ "admin.email.notificationOrganizationExample": "如:\"© ABC Corporation, 565 Knight Way, Palo Alto, California, 94305, USA\"",
"admin.email.notificationsDescription": "正式環境通常設為啟用。啟用時 Mattermost 會傳送電子郵件通知。開發者可以設為停用以跳過設定電子郵件加速開發。<br/>啟用時移除預覽模式橫幅(變更設定後須重新登入以生效)。",
"admin.email.notificationsTitle": "啟用電子郵件通知:",
"admin.email.passwordSaltDescription": "32字元的 Salt 用來簽署重設密碼之電子郵件。於安裝時隨機產生。按\"重新產生\"建立新的 salt。",
@@ -308,10 +310,20 @@
"admin.general.localization.serverLocaleTitle": "預設的伺服器語言:",
"admin.general.log": "記錄",
"admin.general.policy": "政策",
+ "admin.general.policy.allowEditPostAlways": "Any time",
+ "admin.general.policy.allowEditPostDescription": "Set policy on the length of time authors have to edit their messages after posting.",
+ "admin.general.policy.allowEditPostNever": "永不",
+ "admin.general.policy.allowEditPostTimeLimit": "seconds after posting",
+ "admin.general.policy.allowEditPostTitle": "Allow users to edit their messages:",
"admin.general.policy.permissionsAdmin": "團隊跟系統管理員",
"admin.general.policy.permissionsAll": "團隊全員",
"admin.general.policy.permissionsAllChannel": "所有頻道成員",
+ "admin.general.policy.permissionsDeletePostAdmin": "團隊跟系統管理員",
+ "admin.general.policy.permissionsDeletePostAll": "Message authors can delete their own messages, and Administrators can delete any message",
+ "admin.general.policy.permissionsDeletePostSystemAdmin": "系統管理員",
"admin.general.policy.permissionsSystemAdmin": "系統管理員",
+ "admin.general.policy.restrictPostDeleteDescription": "Set policy on who has permission to delete messages.",
+ "admin.general.policy.restrictPostDeleteTitle": "Allow which users to delete messages:",
"admin.general.policy.restrictPrivateChannelCreationDescription": "設定誰能建立私人群組的政策。",
"admin.general.policy.restrictPrivateChannelCreationTitle": "允許建立私人群組:",
"admin.general.policy.restrictPrivateChannelDeletionCommandLineToolLink": "命令列工具",
@@ -363,6 +375,7 @@
"admin.image.amazonS3BucketExample": "如:\"mattermost-media\"",
"admin.image.amazonS3BucketTitle": "Amazon S3 儲存貯體:",
"admin.image.amazonS3EndpointDescription": "相容於 S3 的儲存提供者的主機名稱。預設為`s3.amazonaws.com`。",
+ "admin.image.amazonS3EndpointExample": "E.g.: \"s3.amazonaws.com\"",
"admin.image.amazonS3EndpointTitle": "Amazon S3 端點:",
"admin.image.amazonS3IdDescription": "從 Amazon EC2 管理員取得認證。",
"admin.image.amazonS3IdExample": "如:\"AKIADTOVBGERKLCBV\"",
@@ -513,7 +526,7 @@
"admin.metrics.enableDescription": "啟用時,Mattermost 會啟用效能監視的收集與分析。詳細如何設定 Mattermost 的效能監視,請參閱<a href=\"http://docs.mattermost.com/deployment/metrics.html\" target='_blank'>文件</a>。",
"admin.metrics.enableTitle": "啟用效能監視:",
"admin.metrics.listenAddressDesc": "伺服器將監聽以公開效能計量值的位址。",
- "admin.metrics.listenAddressEx": "例如:\":8067\"",
+ "admin.metrics.listenAddressEx": "如:\":8065\"",
"admin.metrics.listenAddressTitle": "監聽位址:",
"admin.mfa.bannerDesc": "多重要素驗證僅供以 LDAP 或是電子郵件登入的帳號使用。如果系統中有使用者使用其他的登入方式,我們建議直接對 SSO 或 SAML 提供者設定多重要素驗證。",
"admin.mfa.cluster": "高",
@@ -834,7 +847,7 @@
"admin.team.maxChannelsExample": "如:\"100\"",
"admin.team.maxChannelsTitle": "團隊最大頻道數:",
"admin.team.maxNotificationsPerChannelDescription": "頻道使用者人數在超過此數量後,由於效能考量,使用者輸入訊息、@all、@here 以及 @channel 將不再發送通知。",
- "admin.team.maxNotificationsPerChannelExample": "例如 \"1000\"",
+ "admin.team.maxNotificationsPerChannelExample": "如:\"10000\"",
"admin.team.maxNotificationsPerChannelTitle": "單一頻道最大通知數:",
"admin.team.maxUsersDescription": "每個團隊最大人數,包含活躍與不活躍的使用者。",
"admin.team.maxUsersExample": "如:\"25\"",
@@ -923,8 +936,10 @@
"analytics.chart.meaningful": "沒有足夠有意義的資料可顯示。",
"analytics.system.activeUsers": "有發文的活躍使用者",
"analytics.system.channelTypes": "頻道類型",
+ "analytics.system.dailyActiveUsers": "Daily Active Users",
"analytics.system.expiredBanner": "企業版授權將在{date}過期。請在該日起15天內更新授權。詳情請洽<a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>。",
"analytics.system.expiringBanner": "企業版授權將在{date}過期。請洽<a href='mailto:commercial@mattermost.com'>commercial@mattermost.com</a>以更新授權。",
+ "analytics.system.monthlyActiveUsers": "Monthly Active Users",
"analytics.system.postTypes": "發文,檔案與#標籤",
"analytics.system.privateGroups": "私人群組",
"analytics.system.publicChannels": "公開頻道",
@@ -1022,7 +1037,6 @@
"backstage_sidebar.integrations.outgoing_webhooks": "傳出的 Webhook",
"calling_screen": "撥打中",
"center_panel.recent": "按這裡跳到最新的訊息。",
- "chanel_header.addMembers": "新增成員",
"change_url.close": "關閉",
"change_url.endWithLetter": "必須以字元或數字做結尾",
"change_url.invalidUrl": "錯誤的網址",
@@ -1040,6 +1054,7 @@
"channel_flow.handleTooShort": "頻道網址必須為小寫英數字、至少兩個字元",
"channel_flow.invalidName": "無效的頻道名稱",
"channel_flow.set_url_title": "設定 {term} 網址",
+ "channel_header.addMembers": "新增成員",
"channel_header.addToFavorites": "新增至我的最愛",
"channel_header.channel": "頻道",
"channel_header.channelHeader": "編輯頻道標題",
@@ -1079,10 +1094,14 @@
"channel_loader.uploadedFile": " 已上傳一個檔案",
"channel_loader.uploadedImage": " 已上傳一張圖片",
"channel_loader.wrote": " 寫下: ",
+ "channel_members_dropdown.channel_admin": "Channel Admin",
+ "channel_members_dropdown.channel_member": "頻道成員",
+ "channel_members_dropdown.make_channel_admin": "Make Channel Admin",
+ "channel_members_dropdown.make_channel_member": "Make Channel Member",
+ "channel_members_dropdown.remove_from_channel": "Remove From Channel",
+ "channel_members_dropdown.remove_member": "Remove Member",
"channel_members_modal.addNew": " 增加新成員",
- "channel_members_modal.close": "關閉",
- "channel_members_modal.remove": "移除",
- "channel_memebers_modal.members": " 成員",
+ "channel_members_modal.members": " 成員",
"channel_modal.cancel": "取消",
"channel_modal.channel": "頻道",
"channel_modal.createNew": "建立新的",
@@ -1091,6 +1110,7 @@
"channel_modal.edit": "編輯",
"channel_modal.group": "群組",
"channel_modal.header": "標題",
+ "channel_modal.headerEx": "E.g.: \"[Link Title](http://example.com)\"",
"channel_modal.headerHelp": "設定除了{term}名字以外還會顯示在{term}標題的文字。舉例來說,可以輸入 [Link Title](http://example.com) 以顯示常用連結。",
"channel_modal.modalTitle": "新增",
"channel_modal.name": "名字",
@@ -1101,6 +1121,7 @@
"channel_modal.publicChannel1": "建立公開頻道",
"channel_modal.publicChannel2": "建立一個人人可加入的頻道。 ",
"channel_modal.purpose": "用途",
+ "channel_modal.purposeEx": "E.g.: \"A channel to file bugs and improvements\"",
"channel_notifications.allActivity": "所有的活動所有的活動",
"channel_notifications.allUnread": "全部的未讀訊息",
"channel_notifications.globalDefault": "系統預設({notifyLevel})",
@@ -1113,6 +1134,7 @@
"channel_notifications.unreadInfo": "當有未讀訊息時,側邊欄的頻道名字會用粗體表示。選擇\"僅限於提及您的\"將只有您被提及的頻道會用粗體。",
"channel_select.placeholder": "--- 選擇頻道 ---",
"channel_switch_modal.dm": "(直接傳訊)",
+ "channel_switch_modal.failed_to_open": "Failed to open channel.",
"channel_switch_modal.help": "請輸入頻道名。↑↓可以瀏覽、TAB 選擇、↵ 確認、ESC 取消",
"channel_switch_modal.not_found": "找不到符合的。",
"channel_switch_modal.submit": "切換",
@@ -1285,15 +1307,14 @@
"find_team.submitError": "請輸入一個有效的電子郵件位址",
"flag_post.flag": "標記以追蹤",
"flag_post.unflag": "取消標記",
+ "general_tab.chooseDescription": "為團隊取名",
"general_tab.chooseName": "為團隊取名",
"general_tab.codeDesc": "按下'修改'來重新產生邀請碼。",
"general_tab.codeLongDesc": "邀請碼用來當作主選單中<strong>取得團隊邀請連結</strong>所產生的團隊邀請連結的一部分。重新產生會建立一個新的招待連結並且讓舊的連結失效。",
"general_tab.codeTitle": "邀請碼",
- "general_tab.dirDisabled": "團隊列表已被關閉。請向系統管理員要求開啟系統控制台中的團隊列表。",
- "general_tab.dirOff": "此系統的團隊列表已被關閉。",
"general_tab.emptyDescription": "按下'編輯'以增加團隊敘述。",
+ "general_tab.getTeamInviteLink": "取得團隊邀請連結",
"general_tab.includeDirDesc": "在首頁的團隊列表顯示此團隊名字、提供通往登入頁面的連結。",
- "general_tab.includeDirTitle": "加入團隊列表",
"general_tab.no": "否",
"general_tab.openInviteDesc": "允許時,往此團隊的連結將會出現在首頁,有帳號的人皆可以此連結加入此團隊。",
"general_tab.openInviteTitle": "允許任何在此服務器有帳號的使用者加入此團隊",
@@ -1550,6 +1571,7 @@
"login.passwordChanged": " 已成功更新密碼",
"login.session_expired": " 工作階段已逾期,請重新登入。",
"login.signIn": "登入",
+ "login.signInLoading": "Signing in...",
"login.signInWith": "登入方法:",
"login.userNotFound": "無法找到與您輸入的認證相符的帳號。",
"login.username": "使用者名稱",
@@ -1561,8 +1583,10 @@
"member_item.makeAdmin": "設為管理員",
"member_item.member": "成員",
"member_list.noUsersAdd": "沒有可增加的使用者。",
+ "members_popover.manageMembers": "成員管理",
"members_popover.msg": "訊息",
"members_popover.title": "成員",
+ "members_popover.viewMembers": "檢視成員",
"mfa.confirm.complete": "<strong>完成設定!</strong>",
"mfa.confirm.okay": "確定",
"mfa.confirm.secure": "您的帳號現在安全了。下一次登入時,將會被要求輸入手機上 Google Authenticator 所提供的代碼。",
@@ -1665,6 +1689,7 @@
"post_info.mobile.unflag": "取消標記",
"post_info.permalink": "永久網址",
"post_info.reply": "回覆",
+ "post_message_view.edited": "(edited)",
"posts_view.loadMore": "載入更多訊息",
"posts_view.newMsg": "新訊息",
"posts_view.newMsgBelow": "下面還有 {count} 個新訊息",
@@ -1899,7 +1924,7 @@
"update_command.question": "變更可能會導致現有的斜線命令無法使用。您確定要更新嘛?",
"update_command.update": "更新",
"upload_overlay.info": "將檔案拖曳到這裡上傳。",
- "user.settings.advance.embed_preview": "可以使用時,顯示實驗性的連結內容預覽",
+ "user.settings.advance.embed_preview": "For the first web link in a message, display a preview of website content below the message, if available",
"user.settings.advance.embed_toggle": "內嵌預覽顯示開關",
"user.settings.advance.enabledFeatures": "已啟用 {count, number} 項功能",
"user.settings.advance.formattingDesc": "啟用時,文章會顯示連結、顯示繪文字、套用樣式到文字上並自動斷行。此設定預設為開啟。修改此設定後需要重新讀取頁面以生效。",
@@ -1944,10 +1969,10 @@
"user.settings.display.channelDisplayTitle": "頻道顯示模式",
"user.settings.display.channeldisplaymode": "選擇中央頻道的寬度。",
"user.settings.display.clockDisplay": "顯示時間",
- "user.settings.display.collapseDesc": "可用時,展開連結以顯示內容預覽。",
- "user.settings.display.collapseDisplay": "連結預覽",
- "user.settings.display.collapseOff": "關閉",
- "user.settings.display.collapseOn": "啟用",
+ "user.settings.display.collapseDesc": "Set whether previews of image links show as expanded or collapsed by default. This setting can also be controlled using the slash commands /expand and /collapse.",
+ "user.settings.display.collapseDisplay": "Default appearance of image link previews",
+ "user.settings.display.collapseOff": "Collapsed",
+ "user.settings.display.collapseOn": "Expanded",
"user.settings.display.fixedWidthCentered": "固定寬度,置中對齊",
"user.settings.display.fontDesc": "選擇 Mattermost 使用者介面的字型。",
"user.settings.display.fontTitle": "顯示字型",
@@ -2130,7 +2155,9 @@
"user.settings.security.lastUpdated": "最後一次更新於 {date} {time}",
"user.settings.security.ldap": "AD/LDAP",
"user.settings.security.loginGitlab": "已經由 GitLab 登入",
+ "user.settings.security.loginGoogle": "Login done through Google Apps",
"user.settings.security.loginLdap": "已經由 AD/LDAP 登入",
+ "user.settings.security.loginOffice365": "Login done through Office 365",
"user.settings.security.loginSaml": "已經由 SAML 登入",
"user.settings.security.logoutActiveSessions": "觀看並登出使用中的工作階段",
"user.settings.security.method": "登入方式",
@@ -2159,9 +2186,11 @@
"user.settings.security.passwordErrorUppercaseNumberSymbol": "密碼最短必須有{min}個字元且至少有一個大寫英文字母、一個數字和一個符號(\"~!@#$%^&*()\")。",
"user.settings.security.passwordErrorUppercaseSymbol": "密碼最短必須有{min}個字元且至少有一個大寫英文字母和一個符號(\"~!@#$%^&*()\")。",
"user.settings.security.passwordGitlabCantUpdate": "經由 GitLab 登入。無法變更密碼。",
+ "user.settings.security.passwordGoogleCantUpdate": "經由 GitLab 登入。無法變更密碼。",
"user.settings.security.passwordLdapCantUpdate": "經由 AD/LDAP 登入。無法變更密碼。",
"user.settings.security.passwordMatchError": "輸入的新密碼不一致。",
"user.settings.security.passwordMinLength": "無效的最短長度,無法顯示預覽。",
+ "user.settings.security.passwordOffice365CantUpdate": "經由 GitLab 登入。無法變更密碼。",
"user.settings.security.passwordSamlCantUpdate": "此欄位由您的登入提供者決定。如果想變更它,得經由登入提供者變更。",
"user.settings.security.retypePassword": "再次輸入新密碼",
"user.settings.security.saml": "SAML",
diff --git a/webapp/package.json b/webapp/package.json
index 1086fea30..e9a174798 100644
--- a/webapp/package.json
+++ b/webapp/package.json
@@ -16,7 +16,7 @@
"intl": "1.2.5",
"jasny-bootstrap": "3.1.3",
"jquery": "3.1.1",
- "marked": "mattermost/marked#69736482dbad685c398a5eec33a59b5ab06057ac",
+ "marked": "mattermost/marked#1adb66d8df3d582f4434e8e6cd401715463643d9",
"match-at": "0.1.0",
"object-assign": "4.1.0",
"pdfjs-dist": "1.6.319",
diff --git a/webapp/root.html b/webapp/root.html
index 3fc9dfa59..70cf47cc3 100644
--- a/webapp/root.html
+++ b/webapp/root.html
@@ -34,10 +34,52 @@
<!-- CSS Should always go first -->
<link rel='stylesheet' class='code_theme'>
+ <style>
+ .error-screen {
+ font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
+ padding-top: 50px;
+ max-width: 750px;
+ font-size: 14px;
+ color: #333333;
+ margin: auto;
+ display: none;
+ line-height: 1.5;
+ }
+ .error-screen h2 {
+ font-size: 30px;
+ font-weight: normal;
+ line-height: 1.2;
+ }
+
+ .error-screen ul {
+ padding-left: 15px;
+ line-height: 1.7;
+ margin-top: 0;
+ margin-bottom: 10px;
+ }
+
+ .error-screen hr {
+ color: #ddd;
+ margin-top: 20px;
+ margin-bottom: 20px;
+ border: 0;
+ border-top: 1px solid #eee;
+ }
+
+ .error-screen-visible {
+ display: block;
+ }
+ </style>
</head>
<body>
<div id='root'>
+ <div class='error-screen'>
+ <h2>Cannot connect to Mattermost</h2>
+ <hr/>
+ <p>We’re having trouble connecting to Mattermost. If refreshing this page (Ctrl+R or Command+R) does not work, please verify that your computer is connected to the internet.</p>
+ <br/>
+ </div>
<div
class='loading-screen'
style='position: relative'
@@ -50,6 +92,9 @@
</div>
</div>
<script>
+ if (typeof window.setup_root !== 'function') {
+ document.querySelector('.error-screen').classList.add('error-screen-visible');
+ }
window.setup_root();
</script>
<noscript>
diff --git a/webapp/sass/base/_typography.scss b/webapp/sass/base/_typography.scss
index f595e0ed9..1d3f1d052 100644
--- a/webapp/sass/base/_typography.scss
+++ b/webapp/sass/base/_typography.scss
@@ -26,6 +26,11 @@ body {
word-break: break-all;
}
+.overflow--ellipsis {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
.fa {
&.fa-margin--left {
margin-left: 2px;
diff --git a/webapp/sass/layout/_post.scss b/webapp/sass/layout/_post.scss
index cd32ba55e..9448ad767 100644
--- a/webapp/sass/layout/_post.scss
+++ b/webapp/sass/layout/_post.scss
@@ -437,6 +437,15 @@
@include opacity(.9);
box-shadow: none;
}
+
+ &.btn-file__disabled {
+ @include opacity(.1);
+
+ &:hover,
+ &:active {
+ @include opacity(.1);
+ }
+ }
}
textarea {
@@ -560,9 +569,9 @@
}
blockquote {
- display: inline-block;
font-size: 1em;
margin-left: 0;
+ margin-top: 1.3em;
padding: 3px 0 0 25px;
vertical-align: top;
@@ -572,6 +581,11 @@
top: 2px;
}
}
+ .search-item-snippet {
+ blockquote {
+ margin-top: 0;
+ }
+ }
.markdown__heading {
clear: both;
@@ -598,7 +612,15 @@
}
p + p {
- margin-top: 1em;
+ margin: 1em 0;
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+ }
+ span {
+ > p:first-child {
+ margin-bottom: 1em;
+ }
}
ol,
@@ -978,12 +1000,24 @@
width: 100%;
word-wrap: break-word;
- p {
+ div {
margin: 0 0 .4em;
}
p + p {
- margin-top: 1.4em;
+ margin: 1.4em 0;
+ &:last-of-type {
+ margin-bottom: 0;
+ }
+ }
+
+ span {
+ > p:last-child {
+ display: inline;
+ }
+ > p:first-child {
+ margin-bottom: 1.4em;
+ }
}
li {
@@ -1063,6 +1097,12 @@
color: $white;
}
}
+
+ span.edited {
+ color: #A3A3A3;
+ font-size: 0.87em;
+ opacity: 0.6;
+ }
}
.post__link {
diff --git a/webapp/sass/layout/_webhooks.scss b/webapp/sass/layout/_webhooks.scss
index 99a82f00e..904c50ccc 100644
--- a/webapp/sass/layout/_webhooks.scss
+++ b/webapp/sass/layout/_webhooks.scss
@@ -68,6 +68,9 @@
&.attachment__container--danger {
border-left-color: #e40303;
}
+ .sitename {
+ color: #A3A3A3;
+ }
}
.attachment__body {
@@ -80,6 +83,14 @@
&.attachment__body--no_thumb {
width: 100%;
}
+ .attachment__image {
+ margin-bottom: 0;
+ max-height: 150px;
+ max-width: 150px;
+ &.loading {
+ height: 150px;
+ }
+ }
}
.attachment__text p:last-of-type {
@@ -103,6 +114,13 @@
line-height: 18px;
margin: 5px 0;
padding: 0;
+
+ &.has-link {
+ color: #2f81b7;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ }
}
.attachment-link-more {
@@ -144,4 +162,4 @@
}
}
}
-} \ No newline at end of file
+}
diff --git a/webapp/sass/responsive/_mobile.scss b/webapp/sass/responsive/_mobile.scss
index 11b6312c9..5ade6046e 100644
--- a/webapp/sass/responsive/_mobile.scss
+++ b/webapp/sass/responsive/_mobile.scss
@@ -219,6 +219,9 @@
}
}
}
+ blockquote {
+ margin-top: 0;
+ }
}
&.same--root {
@@ -1077,7 +1080,7 @@
}
.post-create-footer {
- padding: 1em 0;
+ padding: 0 45px 0 45px;
.control-label {
margin: .5em 0;
diff --git a/webapp/sass/responsive/_tablet.scss b/webapp/sass/responsive/_tablet.scss
index c9bc1b06b..ac8b50961 100644
--- a/webapp/sass/responsive/_tablet.scss
+++ b/webapp/sass/responsive/_tablet.scss
@@ -26,7 +26,7 @@
}
.post-create-footer {
- padding: 0 1em;
+ padding: 0 45px 0 45px;
.msg-typing {
display: none;
diff --git a/webapp/stores/channel_store.jsx b/webapp/stores/channel_store.jsx
index c93edf7f4..30f395cc3 100644
--- a/webapp/stores/channel_store.jsx
+++ b/webapp/stores/channel_store.jsx
@@ -5,6 +5,7 @@ import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
import EventEmitter from 'events';
import TeamStore from 'stores/team_store.jsx';
+import UserStore from 'stores/user_store.jsx';
var Utils;
import {ActionTypes, Constants} from 'utils/constants.jsx';
@@ -25,6 +26,7 @@ class ChannelStoreClass extends EventEmitter {
this.currentId = null;
this.postMode = this.POST_MODE_CHANNEL;
this.channels = [];
+ this.members_in_channel = {};
this.myChannelMembers = {};
this.moreChannels = {};
this.stats = {};
@@ -241,6 +243,29 @@ class ChannelStoreClass extends EventEmitter {
return this.myChannelMembers;
}
+ saveMembersInChannel(channelId = this.getCurrentId(), members) {
+ const oldMembers = this.members_in_channel[channelId] || {};
+ this.members_in_channel[channelId] = Object.assign({}, oldMembers, members);
+ }
+
+ removeMemberInChannel(channelId = this.getCurrentId(), userId) {
+ if (this.members_in_channel[channelId]) {
+ Reflect.deleteProperty(this.members_in_channel[channelId], userId);
+ }
+ }
+
+ getMembersInChannel(channelId = this.getCurrentId()) {
+ return Object.assign({}, this.members_in_channel[channelId]) || {};
+ }
+
+ hasActiveMemberInChannel(channelId = this.getCurrentId(), userId) {
+ if (this.members_in_channel[channelId] && this.members_in_channel[channelId][userId]) {
+ return true;
+ }
+
+ return false;
+ }
+
storeMoreChannels(channels, teamId = TeamStore.getCurrentId()) {
const newChannels = {};
for (let i = 0; i < channels.length; i++) {
@@ -259,7 +284,12 @@ class ChannelStoreClass extends EventEmitter {
getMoreChannelsList(teamId = TeamStore.getCurrentId()) {
const teamChannels = this.moreChannels[teamId] || {};
- return Object.keys(teamChannels).map((cid) => teamChannels[cid]);
+
+ if (!Utils) {
+ Utils = require('utils/utils.jsx'); //eslint-disable-line global-require
+ }
+
+ return Object.keys(teamChannels).map((cid) => teamChannels[cid]).sort(Utils.sortByDisplayName);
}
storeStats(stats) {
@@ -343,6 +373,25 @@ class ChannelStoreClass extends EventEmitter {
return channelNamesMap;
}
+
+ isChannelAdminForCurrentChannel() {
+ return this.isChannelAdmin(UserStore.getCurrentId(), this.getCurrentId());
+ }
+
+ isChannelAdmin(userId, channelId) {
+ if (!Utils) {
+ Utils = require('utils/utils.jsx'); //eslint-disable-line global-require
+ }
+
+ const channelMembers = this.getMembersInChannel(channelId);
+ const channelMember = channelMembers[userId];
+
+ if (channelMember) {
+ return Utils.isChannelAdmin(channelMember.roles);
+ }
+
+ return false;
+ }
}
var ChannelStore = new ChannelStoreClass();
@@ -409,7 +458,10 @@ ChannelStore.dispatchToken = AppDispatcher.register((payload) => {
ChannelStore.storeMoreChannels(action.channels);
ChannelStore.emitChange();
break;
-
+ case ActionTypes.RECEIVED_MEMBERS_IN_CHANNEL:
+ ChannelStore.saveMembersInChannel(action.channel_id, action.channel_members);
+ ChannelStore.emitChange();
+ break;
case ActionTypes.RECEIVED_CHANNEL_STATS:
var stats = Object.assign({}, ChannelStore.getStats());
stats[action.stats.channel_id] = action.stats;
diff --git a/webapp/stores/notification_store.jsx b/webapp/stores/notification_store.jsx
index 878ac3c9d..4c89fe480 100644
--- a/webapp/stores/notification_store.jsx
+++ b/webapp/stores/notification_store.jsx
@@ -111,7 +111,7 @@ class NotificationStoreClass extends EventEmitter {
// the window itself is not active
const activeChannel = ChannelStore.getCurrent();
const channelId = channel ? channel.id : null;
- const notify = activeChannel.id !== channelId || !this.inFocus;
+ const notify = (activeChannel && activeChannel.id !== channelId) || !this.inFocus;
if (notify) {
Utils.notifyMe(title, body, channel, teamId, duration, !sound);
diff --git a/webapp/stores/opengraph_store.jsx b/webapp/stores/opengraph_store.jsx
new file mode 100644
index 000000000..4ad156df0
--- /dev/null
+++ b/webapp/stores/opengraph_store.jsx
@@ -0,0 +1,68 @@
+import EventEmitter from 'events';
+
+import AppDispatcher from 'dispatcher/app_dispatcher.jsx';
+import Constants from 'utils/constants.jsx';
+
+const ActionTypes = Constants.ActionTypes;
+
+const CHANGE_EVENT = 'change';
+const URL_DATA_CHANGE_EVENT = 'url_data_change';
+
+class OpenGraphStoreClass extends EventEmitter {
+ constructor() {
+ super();
+ this.ogDataObject = {}; // Format: {<url>: <data-object>}
+ }
+
+ emitChange() {
+ this.emit(CHANGE_EVENT);
+ }
+
+ addChangeListener(callback) {
+ this.on(CHANGE_EVENT, callback);
+ }
+
+ removeChangeListener(callback) {
+ this.removeListener(CHANGE_EVENT, callback);
+ }
+
+ emitUrlDataChange(url) {
+ this.emit(URL_DATA_CHANGE_EVENT, url);
+ }
+
+ addUrlDataChangeListener(callback) {
+ this.on(URL_DATA_CHANGE_EVENT, callback);
+ }
+
+ removeUrlDataChangeListener(callback) {
+ this.removeListener(URL_DATA_CHANGE_EVENT, callback);
+ }
+
+ storeOgInfo(url, ogInfo) {
+ this.ogDataObject[url] = ogInfo;
+ }
+
+ getOgInfo(url) {
+ return this.ogDataObject[url];
+ }
+}
+
+var OpenGraphStore = new OpenGraphStoreClass();
+
+// Not expecting more that `Constants.POST_CHUNK_SIZE` post previews rendered at a time
+OpenGraphStore.setMaxListeners(Constants.POST_CHUNK_SIZE);
+
+OpenGraphStore.dispatchToken = AppDispatcher.register((payload) => {
+ var action = payload.action;
+
+ switch (action.type) {
+ case ActionTypes.RECIVED_OPEN_GRAPH_METADATA:
+ OpenGraphStore.storeOgInfo(action.url, action.data);
+ OpenGraphStore.emitUrlDataChange(action.url);
+ OpenGraphStore.emitChange();
+ break;
+ default:
+ }
+});
+
+export default OpenGraphStore;
diff --git a/webapp/stores/post_store.jsx b/webapp/stores/post_store.jsx
index 023e74e35..411eaf724 100644
--- a/webapp/stores/post_store.jsx
+++ b/webapp/stores/post_store.jsx
@@ -23,6 +23,7 @@ class PostStoreClass extends EventEmitter {
this.selectedPostId = null;
this.postsInfo = {};
this.latestPageTime = {};
+ this.earliestPostFromPage = {};
this.currentFocusedPostId = null;
}
emitChange() {
@@ -116,20 +117,8 @@ class PostStoreClass extends EventEmitter {
return null;
}
- getEarliestPost(id) {
- if (this.postsInfo.hasOwnProperty(id)) {
- const postList = this.postsInfo[id].postList;
-
- for (let i = postList.order.length - 1; i >= 0; i--) {
- const postId = postList.order[i];
-
- if (postList.posts[postId].state !== Constants.POST_DELETED) {
- return postList.posts[postId];
- }
- }
- }
-
- return null;
+ getEarliestPostFromPage(id) {
+ return this.earliestPostFromPage[id];
}
getLatestPost(id) {
@@ -207,7 +196,7 @@ class PostStoreClass extends EventEmitter {
return this.currentFocusedPostId;
}
- storePosts(id, newPosts, checkLatest) {
+ storePosts(id, newPosts, checkLatest, checkEarliest) {
if (isPostListNull(newPosts)) {
return;
}
@@ -225,6 +214,17 @@ class PostStoreClass extends EventEmitter {
}
}
+ if (checkEarliest) {
+ const currentEarliest = this.earliestPostFromPage[id] || {create_at: Number.MAX_SAFE_INTEGER};
+ const orderLength = newPosts.order.length;
+ if (orderLength >= 1) {
+ const newEarliestPost = newPosts.posts[newPosts.order[orderLength - 1]];
+ if (newEarliestPost.create_at < currentEarliest.create_at) {
+ this.earliestPostFromPage[id] = newEarliestPost;
+ }
+ }
+ }
+
const combinedPosts = makePostListNonNull(this.getAllPosts(id));
for (const pid in newPosts.posts) {
@@ -638,10 +638,10 @@ PostStore.dispatchToken = AppDispatcher.register((payload) => {
switch (action.type) {
case ActionTypes.RECEIVED_POSTS: {
if (PostStore.currentFocusedPostId !== null && action.isPost) {
- PostStore.storePosts(PostStore.currentFocusedPostId, makePostListNonNull(action.post_list), action.checkLatest);
+ PostStore.storePosts(PostStore.currentFocusedPostId, makePostListNonNull(action.post_list), action.checkLatest, action.checkEarliest);
PostStore.checkBounds(PostStore.currentFocusedPostId, action.numRequested, makePostListNonNull(action.post_list), action.before);
}
- PostStore.storePosts(action.id, makePostListNonNull(action.post_list), action.checkLatest);
+ PostStore.storePosts(action.id, makePostListNonNull(action.post_list), action.checkLatest, action.checkEarliest);
PostStore.checkBounds(action.id, action.numRequested, makePostListNonNull(action.post_list), action.before);
PostStore.emitChange();
break;
diff --git a/webapp/stores/team_store.jsx b/webapp/stores/team_store.jsx
index b2cb3ad26..6de47c65c 100644
--- a/webapp/stores/team_store.jsx
+++ b/webapp/stores/team_store.jsx
@@ -379,5 +379,7 @@ TeamStore.dispatchToken = AppDispatcher.register((payload) => {
}
});
+TeamStore.setMaxListeners(15);
+
window.TeamStore = TeamStore;
export default TeamStore;
diff --git a/webapp/stores/user_store.jsx b/webapp/stores/user_store.jsx
index 2369c38df..fb1e36590 100644
--- a/webapp/stores/user_store.jsx
+++ b/webapp/stores/user_store.jsx
@@ -576,7 +576,7 @@ class UserStoreClass extends EventEmitter {
var current = this.getCurrentUser();
if (current) {
- return Utils.isAdmin(current.roles);
+ return Utils.isSystemAdmin(current.roles);
}
return false;
diff --git a/webapp/tests/client_team.test.jsx b/webapp/tests/client_team.test.jsx
index 5fac2da6d..20610f676 100644
--- a/webapp/tests/client_team.test.jsx
+++ b/webapp/tests/client_team.test.jsx
@@ -23,19 +23,20 @@ describe('Client.Team', function() {
});
it('createTeam', function(done) {
- var client = TestHelper.createClient();
var team = TestHelper.fakeTeam();
- client.createTeam(
- team,
- function(data) {
- assert.equal(data.id.length > 0, true);
- assert.equal(data.name, team.name);
- done();
- },
- function(err) {
- done(new Error(err.message));
- }
- );
+ TestHelper.initBasic(() => {
+ TestHelper.basicClient().createTeam(
+ team,
+ function(data) {
+ assert.equal(data.id.length > 0, true);
+ assert.equal(data.name, team.name);
+ done();
+ },
+ function(err) {
+ done(new Error(err.message));
+ }
+ );
+ });
});
it('getAllTeams', function(done) {
diff --git a/webapp/tests/utils_get_nearest_point.test.jsx b/webapp/tests/utils_get_nearest_point.test.jsx
new file mode 100644
index 000000000..b0b0a2e0e
--- /dev/null
+++ b/webapp/tests/utils_get_nearest_point.test.jsx
@@ -0,0 +1,35 @@
+import assert from 'assert';
+import * as CommonUtils from 'utils/commons.jsx';
+
+describe('CommonUtils.getNearestPoint', function() {
+ this.timeout(10000);
+ it('should return nearest point', function() {
+ for (const data of [
+ {
+ points: [{x: 30, y: 40}, {x: 50, y: 50}, {x: 100, y: 2}, {x: 500, y: 200}, {x: 110, y: 20}, {x: 10, y: 20}],
+ pivotPoint: {x: 10, y: 20},
+ nearestPoint: {x: 10, y: 20},
+ nearestPointLte: {x: 10, y: 20}
+ },
+ {
+ points: [{x: 50, y: 50}, {x: 100, y: 2}, {x: 500, y: 200}, {x: 110, y: 20}, {x: 100, y: 90}, {x: 30, y: 40}],
+ pivotPoint: {x: 10, y: 20},
+ nearestPoint: {x: 30, y: 40},
+ nearestPointLte: {}
+ },
+ {
+ points: [{x: 50, y: 50}, {x: 1, y: 1}, {x: 15, y: 25}, {x: 100, y: 2}, {x: 500, y: 200}, {x: 110, y: 20}],
+ pivotPoint: {x: 10, y: 20},
+ nearestPoint: {x: 15, y: 25},
+ nearestPointLte: {x: 1, y: 1}
+ }
+ ]) {
+ const nearestPointData = CommonUtils.getNearestPoint(data.pivotPoint, data.points);
+
+ assert.equal(nearestPointData.nearestPoint.x, data.nearestPoint.x);
+ assert.equal(nearestPointData.nearestPoint.y, data.nearestPoint.y);
+ assert.equal(nearestPointData.nearestPointLte.x, data.nearestPointLte.x);
+ assert.equal(nearestPointData.nearestPointLte.y, data.nearestPointLte.y);
+ }
+ });
+});
diff --git a/webapp/utils/async_client.jsx b/webapp/utils/async_client.jsx
index 25724ec5e..cd38be811 100644
--- a/webapp/utils/async_client.jsx
+++ b/webapp/utils/async_client.jsx
@@ -1039,6 +1039,14 @@ export function getStandardAnalytics(teamId) {
if (data[index].name === 'total_read_db_connections') {
stats[StatTypes.TOTAL_READ_DB_CONNECTIONS] = data[index].value;
}
+
+ if (data[index].name === 'daily_active_users') {
+ stats[StatTypes.DAILY_ACTIVE_USERS] = data[index].value;
+ }
+
+ if (data[index].name === 'monthly_active_users') {
+ stats[StatTypes.MONTHLY_ACTIVE_USERS] = data[index].value;
+ }
}
AppDispatcher.handleServerAction({
diff --git a/webapp/utils/channel_intro_messages.jsx b/webapp/utils/channel_intro_messages.jsx
index 0c011734a..991bf54e8 100644
--- a/webapp/utils/channel_intro_messages.jsx
+++ b/webapp/utils/channel_intro_messages.jsx
@@ -48,7 +48,7 @@ export function createDMIntroMessage(channel, centeredIntro) {
<div className={'channel-intro ' + centeredIntro}>
<div className='post-profile-img__container channel-intro-img'>
<ProfilePicture
- src={Client.getUsersRoute() + '/' + teammate.id + '/image?time=' + teammate.update_at}
+ src={Client.getUsersRoute() + '/' + teammate.id + '/image?time=' + teammate.last_picture_update}
width='50'
height='50'
user={teammate}
diff --git a/webapp/utils/channel_utils.jsx b/webapp/utils/channel_utils.jsx
index ffc69d7b4..2189cd789 100644
--- a/webapp/utils/channel_utils.jsx
+++ b/webapp/utils/channel_utils.jsx
@@ -23,7 +23,7 @@ import LocalizationStore from 'stores/localization_store.jsx';
export function buildDisplayableChannelList(persistentChannels) {
const missingDMChannels = createMissingDirectChannels(persistentChannels);
- const channels = persistentChannels.concat(missingDMChannels).map(completeDirectChannelInfo);
+ const channels = persistentChannels.concat(missingDMChannels).map(completeDirectChannelInfo).filter(isNotDeletedChannel);
channels.sort(sortChannelsByDisplayName);
const favoriteChannels = channels.filter(isFavoriteChannel);
@@ -43,6 +43,14 @@ export function isFavoriteChannel(channel) {
return PreferenceStore.getBool(Preferences.CATEGORY_FAVORITE_CHANNEL, channel.id);
}
+export function isFavoriteChannelId(channelId) {
+ return PreferenceStore.getBool(Preferences.CATEGORY_FAVORITE_CHANNEL, channelId);
+}
+
+export function isNotDeletedChannel(channel) {
+ return channel.delete_at === 0;
+}
+
export function isOpenChannel(channel) {
return channel.type === Constants.OPEN_CHANNEL;
}
diff --git a/webapp/utils/commons.jsx b/webapp/utils/commons.jsx
new file mode 100644
index 000000000..1888869dc
--- /dev/null
+++ b/webapp/utils/commons.jsx
@@ -0,0 +1,36 @@
+export function getDistanceBW2Points(point1, point2, xAttr = 'x', yAttr = 'y') {
+ return Math.sqrt(Math.pow(point1[xAttr] - point2[xAttr], 2) + Math.pow(point1[yAttr] - point2[yAttr], 2));
+}
+
+/**
+ * Funtion to return nearest point of given pivot point.
+ * It return two points one nearest and other nearest but having both coorditanes smaller than the given point's coordinates.
+ */
+export function getNearestPoint(pivotPoint, points, xAttr = 'x', yAttr = 'y') {
+ var nearestPoint = {};
+ var nearestPointLte = {}; // Nearest point smaller than or equal to point
+ for (const point of points) {
+ if (typeof nearestPoint[xAttr] === 'undefined' || typeof nearestPoint[yAttr] === 'undefined') {
+ nearestPoint = point;
+ } else if (getDistanceBW2Points(point, pivotPoint, xAttr, yAttr) < getDistanceBW2Points(nearestPoint, pivotPoint, xAttr, yAttr)) {
+ // Check for bestImage
+ nearestPoint = point;
+ }
+
+ if (typeof nearestPointLte[xAttr] === 'undefined' || typeof nearestPointLte[yAttr] === 'undefined') {
+ if (point[xAttr] <= pivotPoint[xAttr] && point[yAttr] <= pivotPoint[yAttr]) {
+ nearestPointLte = point;
+ }
+ } else if (
+ // Check for bestImageLte
+ getDistanceBW2Points(point, pivotPoint, xAttr, yAttr) < getDistanceBW2Points(nearestPointLte, pivotPoint, xAttr, yAttr) &&
+ point[xAttr] <= pivotPoint[xAttr] && point[yAttr] <= pivotPoint[yAttr]
+ ) {
+ nearestPointLte = point;
+ }
+ }
+ return {
+ nearestPoint,
+ nearestPointLte
+ };
+}
diff --git a/webapp/utils/constants.jsx b/webapp/utils/constants.jsx
index 8162846e2..86147ee8c 100644
--- a/webapp/utils/constants.jsx
+++ b/webapp/utils/constants.jsx
@@ -74,6 +74,7 @@ export const ActionTypes = keyMirror({
RECEIVED_MORE_CHANNELS: null,
RECEIVED_CHANNEL_STATS: null,
RECEIVED_MY_CHANNEL_MEMBERS: null,
+ RECEIVED_MEMBERS_IN_CHANNEL: null,
FOCUS_POST: null,
RECEIVED_POSTS: null,
@@ -145,6 +146,9 @@ export const ActionTypes = keyMirror({
RECEIVED_LOCALE: null,
+ UPDATE_OPEN_GRAPH_METADATA: null,
+ RECIVED_OPEN_GRAPH_METADATA: null,
+
SHOW_SEARCH: null,
USER_TYPING: null,
@@ -256,7 +260,9 @@ export const Constants = {
NEWLY_CREATED_USERS: null,
TOTAL_WEBSOCKET_CONNECTIONS: null,
TOTAL_MASTER_DB_CONNECTIONS: null,
- TOTAL_READ_DB_CONNECTIONS: null
+ TOTAL_READ_DB_CONNECTIONS: null,
+ DAILY_ACTIVE_USERS: null,
+ MONTHLY_ACTIVE_USERS: null
}),
STAT_MAX_ACTIVE_USERS: 20,
STAT_MAX_NEW_USERS: 20,
@@ -737,63 +743,63 @@ export const Constants = {
},
CODE_PREVIEW_MAX_FILE_SIZE: 500000, // 500 KB
HighlightedLanguages: {
- actionscript: {name: 'ActionScript', extensions: ['as']},
+ actionscript: {name: 'ActionScript', extensions: ['as'], aliases: ['as', 'as3']},
applescript: {name: 'AppleScript', extensions: ['applescript', 'osascript', 'scpt']},
bash: {name: 'Bash', extensions: ['bash', 'sh', 'zsh']},
clojure: {name: 'Clojure', extensions: ['clj', 'boot', 'cl2', 'cljc', 'cljs', 'cljs.hl', 'cljscm', 'cljx', 'hic']},
- coffeescript: {name: 'CoffeeScript', extensions: ['coffee', '_coffee', 'cake', 'cjsx', 'cson', 'iced']},
- cpp: {name: 'C/C++', extensions: ['cpp', 'c', 'cc', 'h', 'c++', 'h++', 'hpp']},
- cs: {name: 'C#', extensions: ['cs', 'csharp']},
+ coffeescript: {name: 'CoffeeScript', extensions: ['coffee', '_coffee', 'cake', 'cjsx', 'cson', 'iced'], aliases: ['coffee', 'coffee-script']},
+ cpp: {name: 'C/C++', extensions: ['cpp', 'c', 'cc', 'h', 'c++', 'h++', 'hpp'], aliases: ['c++']},
+ cs: {name: 'C#', extensions: ['cs', 'csharp'], aliases: ['c#', 'csharp']},
css: {name: 'CSS', extensions: ['css']},
- d: {name: 'D', extensions: ['d', 'di']},
+ d: {name: 'D', extensions: ['d', 'di'], aliases: ['dlang']},
dart: {name: 'Dart', extensions: ['dart']},
delphi: {name: 'Delphi', extensions: ['delphi', 'dpr', 'dfm', 'pas', 'pascal', 'freepascal', 'lazarus', 'lpr', 'lfm']},
- diff: {name: 'Diff', extensions: ['diff', 'patch']},
+ diff: {name: 'Diff', extensions: ['diff', 'patch'], aliases: ['patch', 'udiff']},
django: {name: 'Django', extensions: ['django', 'jinja']},
- dockerfile: {name: 'Dockerfile', extensions: ['dockerfile', 'docker']},
- erlang: {name: 'Erlang', extensions: ['erl']},
+ dockerfile: {name: 'Dockerfile', extensions: ['dockerfile', 'docker'], aliases: ['docker']},
+ erlang: {name: 'Erlang', extensions: ['erl'], aliases: ['erl']},
fortran: {name: 'Fortran', extensions: ['f90', 'f95']},
fsharp: {name: 'F#', extensions: ['fsharp', 'fs']},
gcode: {name: 'G-Code', extensions: ['gcode', 'nc']},
- go: {name: 'Go', extensions: ['go']},
+ go: {name: 'Go', extensions: ['go'], aliases: ['golang']},
groovy: {name: 'Groovy', extensions: ['groovy']},
- handlebars: {name: 'Handlebars', extensions: ['handlebars', 'hbs', 'html.hbs', 'html.handlebars']},
- haskell: {name: 'Haskell', extensions: ['hs']},
+ handlebars: {name: 'Handlebars', extensions: ['handlebars', 'hbs', 'html.hbs', 'html.handlebars'], aliases: ['hbs', 'mustache']},
+ haskell: {name: 'Haskell', extensions: ['hs'], aliases: ['hs']},
haxe: {name: 'Haxe', extensions: ['hx']},
java: {name: 'Java', extensions: ['java', 'jsp']},
- javascript: {name: 'JavaScript', extensions: ['js', 'jsx']},
+ javascript: {name: 'JavaScript', extensions: ['js', 'jsx'], aliases: ['js']},
json: {name: 'JSON', extensions: ['json']},
- julia: {name: 'Julia', extensions: ['jl']},
+ julia: {name: 'Julia', extensions: ['jl'], aliases: ['jl']},
kotlin: {name: 'Kotlin', extensions: ['kt', 'ktm', 'kts']},
less: {name: 'Less', extensions: ['less']},
lisp: {name: 'Lisp', extensions: ['lisp']},
lua: {name: 'Lua', extensions: ['lua']},
- makefile: {name: 'Makefile', extensions: ['mk', 'mak']},
- markdown: {name: 'Markdown', extensions: ['md', 'mkdown', 'mkd']},
- matlab: {name: 'Matlab', extensions: ['matlab', 'm']},
- objectivec: {name: 'Objective C', extensions: ['mm', 'objc', 'obj-c']},
+ makefile: {name: 'Makefile', extensions: ['mk', 'mak'], aliases: ['make', 'mf', 'gnumake', 'bsdmake']},
+ markdown: {name: 'Markdown', extensions: ['md', 'mkdown', 'mkd'], aliases: ['md', 'mkd']},
+ matlab: {name: 'Matlab', extensions: ['matlab', 'm'], aliases: ['m']},
+ objectivec: {name: 'Objective C', extensions: ['mm', 'objc', 'obj-c'], aliases: ['objective_c', 'objc']},
ocaml: {name: 'OCaml', extensions: ['ml']},
- perl: {name: 'Perl', extensions: ['perl', 'pl']},
- php: {name: 'PHP', extensions: ['php', 'php3', 'php4', 'php5', 'php6']},
- powershell: {name: 'PowerShell', extensions: ['ps', 'ps1']},
- puppet: {name: 'Puppet', extensions: ['pp']},
- python: {name: 'Python', extensions: ['py', 'gyp']},
- r: {name: 'R', extensions: ['r']},
- ruby: {name: 'Ruby', extensions: ['ruby', 'rb', 'gemspec', 'podspec', 'thor', 'irb']},
- rust: {name: 'Rust', extensions: ['rs']},
+ perl: {name: 'Perl', extensions: ['perl', 'pl'], aliases: ['pl']},
+ php: {name: 'PHP', extensions: ['php', 'php3', 'php4', 'php5', 'php6'], aliases: ['php3', 'php4', 'php5']},
+ powershell: {name: 'PowerShell', extensions: ['ps', 'ps1'], aliases: ['posh']},
+ puppet: {name: 'Puppet', extensions: ['pp'], aliases: ['pp']},
+ python: {name: 'Python', extensions: ['py', 'gyp'], aliases: ['py']},
+ r: {name: 'R', extensions: ['r'], aliases: ['r', 's']},
+ ruby: {name: 'Ruby', extensions: ['ruby', 'rb', 'gemspec', 'podspec', 'thor', 'irb'], aliases: ['rb']},
+ rust: {name: 'Rust', extensions: ['rs'], aliases: ['rs']},
scala: {name: 'Scala', extensions: ['scala']},
scheme: {name: 'Scheme', extensions: ['scm', 'sld']},
scss: {name: 'SCSS', extensions: ['scss']},
- smalltalk: {name: 'Smalltalk', extensions: ['st']},
+ smalltalk: {name: 'Smalltalk', extensions: ['st'], aliases: ['st', 'squeak']},
sql: {name: 'SQL', extensions: ['sql']},
swift: {name: 'Swift', extensions: ['swift']},
- tex: {name: 'TeX', extensions: ['tex']},
+ tex: {name: 'TeX', extensions: ['tex'], aliases: ['latex']},
text: {name: 'Text', extensions: ['txt']},
- vbnet: {name: 'VB.Net', extensions: ['vbnet', 'vb', 'bas']},
+ vbnet: {name: 'VB.Net', extensions: ['vbnet', 'vb', 'bas'], aliases: ['vb', 'visualbasic']},
vbscript: {name: 'VBScript', extensions: ['vbs']},
verilog: {name: 'Verilog', extensions: ['v', 'veo']},
xml: {name: 'HTML, XML', extensions: ['xml', 'html', 'xhtml', 'rss', 'atom', 'xsl', 'plist']},
- yaml: {name: 'YAML', extensions: ['yaml']}
+ yaml: {name: 'YAML', extensions: ['yaml'], aliases: ['yml']}
},
PostsViewJumpTypes: {
BOTTOM: 1,
@@ -859,6 +865,13 @@ export const Constants = {
PERMISSIONS_ALL: 'all',
PERMISSIONS_TEAM_ADMIN: 'team_admin',
PERMISSIONS_SYSTEM_ADMIN: 'system_admin',
+ PERMISSIONS_DELETE_POST_ALL: 'all',
+ PERMISSIONS_DELETE_POST_TEAM_ADMIN: 'team_admin',
+ PERMISSIONS_DELETE_POST_SYSTEM_ADMIN: 'system_admin',
+ ALLOW_EDIT_POST_ALWAYS: 'always',
+ ALLOW_EDIT_POST_NEVER: 'never',
+ ALLOW_EDIT_POST_TIME_LIMIT: 'time_limit',
+ DEFAULT_POST_EDIT_TIME_LIMIT: 300,
MENTION_CHANNELS: 'mention.channels',
MENTION_MORE_CHANNELS: 'mention.morechannels',
MENTION_MEMBERS: 'mention.members',
diff --git a/webapp/utils/markdown.jsx b/webapp/utils/markdown.jsx
index 8a0b9ef0a..c84df0fa5 100644
--- a/webapp/utils/markdown.jsx
+++ b/webapp/utils/markdown.jsx
@@ -8,7 +8,18 @@ import marked from 'marked';
import katex from 'katex';
function markdownImageLoaded(image) {
- image.style.height = 'auto';
+ if (image.hasAttribute('height') && image.attributes.height.value !== 'auto') {
+ const maxHeight = parseInt(global.getComputedStyle(image).maxHeight, 10);
+
+ if (image.attributes.height.value > maxHeight) {
+ image.style.height = maxHeight + 'px';
+ image.style.width = ((maxHeight * image.attributes.width.value) / image.attributes.height.value) + 'px';
+ } else {
+ image.style.height = image.attributes.height.value + 'px';
+ }
+ } else {
+ image.style.height = 'auto';
+ }
}
global.markdownImageLoaded = markdownImageLoaded;
@@ -117,10 +128,29 @@ class MattermostMarkdownRenderer extends marked.Renderer {
}
image(href, title, text) {
- let out = '<img src="' + href + '" alt="' + text + '"';
+ let src = href;
+ let dimensions = [];
+ const parts = href.split(' ');
+ if (parts.length > 1) {
+ const lastPart = parts.pop();
+ src = parts.join(' ');
+ if (lastPart[0] === '=') {
+ dimensions = lastPart.substr(1).split('x');
+ if (dimensions.length === 2 && dimensions[1] === '') {
+ dimensions[1] = 'auto';
+ }
+ }
+ }
+ let out = '<img src="' + src + '" alt="' + text + '"';
if (title) {
out += ' title="' + title + '"';
}
+ if (dimensions.length > 0) {
+ out += ' width="' + dimensions[0] + '"';
+ }
+ if (dimensions.length > 1) {
+ out += ' height="' + dimensions[1] + '"';
+ }
out += ' onload="window.markdownImageLoaded(this)" onerror="window.markdownImageLoaded(this)" class="markdown-inline-img"';
out += this.options.xhtml ? '/>' : '>';
return out;
@@ -222,7 +252,8 @@ export function format(text, options = {}) {
renderer: new MattermostMarkdownRenderer(null, options),
sanitize: true,
gfm: true,
- tables: true
+ tables: true,
+ mangle: false
};
return marked(text, markdownOptions);
diff --git a/webapp/utils/post_utils.jsx b/webapp/utils/post_utils.jsx
index 4bba784cb..20993b95c 100644
--- a/webapp/utils/post_utils.jsx
+++ b/webapp/utils/post_utils.jsx
@@ -3,11 +3,19 @@
import Client from 'client/web_client.jsx';
import Constants from 'utils/constants.jsx';
+import * as Utils from 'utils/utils.jsx';
+
+import TeamStore from 'stores/team_store.jsx';
+import UserStore from 'stores/user_store.jsx';
export function isSystemMessage(post) {
return post.type && (post.type.lastIndexOf(Constants.SYSTEM_MESSAGE_PREFIX) === 0);
}
+export function isPostOwner(post) {
+ return UserStore.getCurrentId() === post.user_id;
+}
+
export function isComment(post) {
if ('root_id' in post) {
return post.root_id !== '' && post.root_id != null;
@@ -15,6 +23,10 @@ export function isComment(post) {
return false;
}
+export function isEdited(post) {
+ return post.edit_at > 0;
+}
+
export function getProfilePicSrcForPost(post, timestamp) {
let src = Client.getUsersRoute() + '/' + post.user_id + '/image?time=' + timestamp;
if (post.props && post.props.from_webhook && global.window.mm_config.EnablePostIconOverride === 'true') {
@@ -28,4 +40,37 @@ export function getProfilePicSrcForPost(post, timestamp) {
}
return src;
+}
+
+export function canDeletePost(post) {
+ var isOwner = isPostOwner(post);
+ var isAdmin = TeamStore.isTeamAdminForCurrentTeam() || UserStore.isSystemAdminForCurrentUser();
+ var isSystemAdmin = UserStore.isSystemAdminForCurrentUser();
+
+ if (global.window.mm_license.IsLicensed === 'true') {
+ return (global.window.mm_config.RestrictPostDelete === Constants.PERMISSIONS_DELETE_POST_ALL && (isOwner || isAdmin)) ||
+ (global.window.mm_config.RestrictPostDelete === Constants.PERMISSIONS_DELETE_POST_TEAM_ADMIN && isAdmin) ||
+ (global.window.mm_config.RestrictPostDelete === Constants.PERMISSIONS_DELETE_POST_SYSTEM_ADMIN && isSystemAdmin);
+ }
+ return isOwner || isAdmin;
+}
+
+export function canEditPost(post, editDisableAction) {
+ var isOwner = isPostOwner(post);
+
+ var canEdit = isOwner && !isSystemMessage(post);
+
+ if (canEdit && global.window.mm_license.IsLicensed === 'true') {
+ if (global.window.mm_config.AllowEditPost === Constants.ALLOW_EDIT_POST_NEVER) {
+ canEdit = false;
+ } else if (global.window.mm_config.AllowEditPost === Constants.ALLOW_EDIT_POST_TIME_LIMIT) {
+ var timeLeft = (post.create_at + (global.window.mm_config.PostEditTimeLimit * 1000)) - Utils.getTimestamp();
+ if (timeLeft > 0) {
+ editDisableAction.fireAfter(timeLeft + 1000);
+ } else {
+ canEdit = false;
+ }
+ }
+ }
+ return canEdit;
} \ No newline at end of file
diff --git a/webapp/utils/syntax_highlighting.jsx b/webapp/utils/syntax_highlighting.jsx
index 47ba5bd4e..73e1087cb 100644
--- a/webapp/utils/syntax_highlighting.jsx
+++ b/webapp/utils/syntax_highlighting.jsx
@@ -123,9 +123,9 @@ hlJS.registerLanguage('yaml', hljsYaml);
const HighlightedLanguages = Constants.HighlightedLanguages;
export function highlight(lang, code) {
- const language = lang.toLowerCase();
+ const language = getLanguageFromNameOrAlias(lang);
- if (HighlightedLanguages[language]) {
+ if (language) {
try {
return hlJS.highlight(language, code).value;
} catch (e) {
@@ -147,13 +147,25 @@ export function getLanguageFromFileExtension(extension) {
}
export function canHighlight(language) {
- return Boolean(HighlightedLanguages[language.toLowerCase()]);
+ return Boolean(getLanguageFromNameOrAlias(language));
}
export function getLanguageName(language) {
if (canHighlight(language)) {
- return HighlightedLanguages[language.toLowerCase()].name;
+ return HighlightedLanguages[getLanguageFromNameOrAlias(language)].name;
}
return '';
-} \ No newline at end of file
+}
+
+function getLanguageFromNameOrAlias(name) {
+ const langName = name.toLowerCase();
+ if (HighlightedLanguages[langName]) {
+ return langName;
+ }
+
+ return Object.keys(HighlightedLanguages).find((key) => {
+ const aliases = HighlightedLanguages[key].aliases;
+ return aliases && aliases.find((a) => a === langName);
+ });
+}
diff --git a/webapp/utils/text_formatting.jsx b/webapp/utils/text_formatting.jsx
index 9f983a1ee..171226558 100644
--- a/webapp/utils/text_formatting.jsx
+++ b/webapp/utils/text_formatting.jsx
@@ -395,7 +395,13 @@ function parseSearchTerms(searchTerm) {
}
// remove punctuation from each term
- terms = terms.map((term) => term.replace(puncStart, '').replace(puncEnd, ''));
+ terms = terms.map((term) => {
+ term.replace(puncStart, '');
+ if (term.charAt(term.length - 1) !== '*') {
+ term.replace(puncEnd, '');
+ }
+ return term;
+ });
return terms;
}
@@ -452,6 +458,11 @@ export function highlightSearchTerms(text, tokens, searchPatterns) {
output = output.replace(alias, newAlias);
}
+
+ // The pattern regexes are global, so calling pattern.test() above alters their
+ // state. Reset lastIndex to 0 between calls to test() to ensure it returns the
+ // same result every time it is called with the same value of token.originalText.
+ pattern.lastIndex = 0;
}
// the new tokens are stashed in a separate map since we can't add objects to a map during iteration
diff --git a/webapp/utils/utils.jsx b/webapp/utils/utils.jsx
index e7ed9567b..a0aecbdb3 100644
--- a/webapp/utils/utils.jsx
+++ b/webapp/utils/utils.jsx
@@ -54,6 +54,14 @@ export function isInRole(roles, inRole) {
return false;
}
+export function isChannelAdmin(roles) {
+ if (isInRole(roles, 'channel_admin')) {
+ return true;
+ }
+
+ return false;
+}
+
export function isAdmin(roles) {
if (isInRole(roles, 'team_admin')) {
return true;
@@ -1316,3 +1324,15 @@ export function handleFormattedTextClick(e) {
browserHistory.push('/' + TeamStore.getCurrent().name + '/channels/' + channelMentionAttribute.value);
}
}
+
+export function isEmptyObject(object) {
+ if (!object) {
+ return true;
+ }
+
+ if (Object.keys(object).length === 0) {
+ return true;
+ }
+
+ return false;
+}