Files
mattermost/mattermost.go

632 lines
17 KiB
Go
Raw Normal View History

// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
2015-06-14 23:53:32 -08:00
// See License.txt for license information.
package main
import (
"flag"
"fmt"
2015-10-05 17:33:45 -07:00
"io/ioutil"
"net/http"
2015-10-07 08:58:56 -07:00
"net/url"
2015-09-04 11:59:10 -07:00
"os"
"os/signal"
2015-10-01 13:02:04 -07:00
"runtime"
"strconv"
2015-09-04 11:59:10 -07:00
"strings"
"syscall"
"time"
2016-01-11 09:12:51 -06:00
l4g "github.com/alecthomas/log4go"
2015-06-14 23:53:32 -08:00
"github.com/mattermost/platform/api"
"github.com/mattermost/platform/manualtesting"
2015-09-04 11:59:10 -07:00
"github.com/mattermost/platform/model"
2015-06-14 23:53:32 -08:00
"github.com/mattermost/platform/utils"
"github.com/mattermost/platform/web"
2015-12-08 13:38:43 -05:00
// Plugins
_ "github.com/mattermost/platform/model/gitlab"
// Enterprise Deps
_ "github.com/go-ldap/ldap"
2015-06-14 23:53:32 -08:00
)
2016-02-02 08:31:37 -05:00
//ENTERPRISE_IMPORTS
2015-09-04 11:59:10 -07:00
var flagCmdCreateTeam bool
var flagCmdCreateUser bool
var flagCmdAssignRole bool
var flagCmdVersion bool
2015-09-04 11:59:10 -07:00
var flagCmdResetPassword bool
2015-11-18 08:53:17 -08:00
var flagCmdPermanentDeleteUser bool
var flagCmdPermanentDeleteTeam bool
2015-09-04 11:59:10 -07:00
var flagConfigFile string
var flagEmail string
var flagPassword string
var flagTeamName string
var flagRole string
var flagRunCmds bool
2015-06-14 23:53:32 -08:00
2015-09-04 11:59:10 -07:00
func main() {
2015-06-14 23:53:32 -08:00
2015-09-04 11:59:10 -07:00
parseCmds()
2015-06-14 23:53:32 -08:00
utils.InitTranslations()
utils.LoadConfig(flagConfigFile)
2015-09-04 11:59:10 -07:00
if flagRunCmds {
utils.ConfigureCmdLineLog()
}
2015-09-04 11:59:10 -07:00
pwd, _ := os.Getwd()
l4g.Info(utils.T("mattermost.current_version"), model.CurrentVersion, model.BuildNumber, model.BuildDate, model.BuildHash)
l4g.Info(utils.T("mattermost.entreprise_enabled"), model.BuildEnterpriseReady)
l4g.Info(utils.T("mattermost.working_dir"), pwd)
l4g.Info(utils.T("mattermost.config_file"), utils.FindConfigFile(flagConfigFile))
2015-09-04 11:59:10 -07:00
2015-06-14 23:53:32 -08:00
api.NewServer()
api.InitApi()
web.InitWeb()
if model.BuildEnterpriseReady == "true" {
2016-02-04 13:00:03 -05:00
loadLicense()
}
2016-01-04 12:44:22 -05:00
2015-09-04 11:59:10 -07:00
if flagRunCmds {
runCmds()
} else {
api.StartServer()
// If we allow testing then listen for manual testing URL hits
if utils.Cfg.ServiceSettings.EnableTesting {
2015-09-04 11:59:10 -07:00
manualtesting.InitManualTesting()
}
2015-11-30 22:38:38 -08:00
setDiagnosticId()
runSecurityAndDiagnosticsJobAndForget()
2015-10-01 13:02:04 -07:00
2015-09-04 11:59:10 -07:00
// wait for kill signal before attempting to gracefully shutdown
// the running service
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
<-c
api.StopServer()
}
}
2016-02-04 13:00:03 -05:00
func loadLicense() {
licenseId := ""
if result := <-api.Srv.Store.System().Get(); result.Err == nil {
props := result.Data.(model.StringMap)
licenseId = props[model.SYSTEM_ACTIVE_LICENSE_ID]
}
if len(licenseId) != 26 {
l4g.Warn(utils.T("mattermost.load_license.find.warn"))
return
}
if result := <-api.Srv.Store.License().Get(licenseId); result.Err == nil {
record := result.Data.(*model.LicenseRecord)
utils.LoadLicense([]byte(record.Bytes))
} else {
l4g.Warn(utils.T("mattermost.load_license.find.warn"))
}
}
2015-11-30 22:38:38 -08:00
func setDiagnosticId() {
if result := <-api.Srv.Store.System().Get(); result.Err == nil {
2015-11-30 22:38:38 -08:00
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)
2015-11-30 22:38:38 -08:00
}
utils.CfgDiagnosticId = id
}
}
func runSecurityAndDiagnosticsJobAndForget() {
2015-10-01 13:02:04 -07:00
go func() {
for {
2015-10-12 10:57:39 -07:00
if *utils.Cfg.ServiceSettings.EnableSecurityFixAlert {
if result := <-api.Srv.Store.System().Get(); result.Err == nil {
2015-10-01 13:02:04 -07:00
props := result.Data.(model.StringMap)
2015-10-15 11:16:26 -07:00
lastSecurityTime, _ := strconv.ParseInt(props[model.SYSTEM_LAST_SECURITY_TIME], 10, 0)
2015-10-01 13:02:04 -07:00
currentTime := model.GetMillis()
if (currentTime - lastSecurityTime) > 1000*60*60*24*1 {
l4g.Debug(utils.T("mattermost.security_checks.debug"))
2015-10-01 13:02:04 -07:00
v := url.Values{}
2015-11-30 22:38:38 -08:00
v.Set(utils.PROP_DIAGNOSTIC_ID, utils.CfgDiagnosticId)
v.Set(utils.PROP_DIAGNOSTIC_BUILD, model.CurrentVersion+"."+model.BuildNumber)
2015-12-08 13:38:43 -05:00
v.Set(utils.PROP_DIAGNOSTIC_ENTERPRISE_READY, model.BuildEnterpriseReady)
v.Set(utils.PROP_DIAGNOSTIC_DATABASE, utils.Cfg.SqlSettings.DriverName)
v.Set(utils.PROP_DIAGNOSTIC_OS, runtime.GOOS)
v.Set(utils.PROP_DIAGNOSTIC_CATEGORY, utils.VAL_DIAGNOSTIC_CATEGORY_DEFAULT)
2015-10-01 13:02:04 -07:00
2015-10-15 11:16:26 -07:00
if len(props[model.SYSTEM_RAN_UNIT_TESTS]) > 0 {
v.Set(utils.PROP_DIAGNOSTIC_UNIT_TESTS, "1")
} else {
v.Set(utils.PROP_DIAGNOSTIC_UNIT_TESTS, "0")
}
systemSecurityLastTime := &model.System{Name: model.SYSTEM_LAST_SECURITY_TIME, Value: strconv.FormatInt(currentTime, 10)}
2015-10-05 17:33:45 -07:00
if lastSecurityTime == 0 {
<-api.Srv.Store.System().Save(systemSecurityLastTime)
2015-10-01 13:02:04 -07:00
} else {
<-api.Srv.Store.System().Update(systemSecurityLastTime)
2015-10-01 13:02:04 -07:00
}
if ucr := <-api.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.User().GetTotalActiveUsersCount(); ucr.Err == nil {
v.Set(utils.PROP_DIAGNOSTIC_ACTIVE_USER_COUNT, strconv.FormatInt(ucr.Data.(int64), 10))
}
2015-10-07 09:23:26 -07:00
res, err := http.Get(utils.DIAGNOSTIC_URL + "/security?" + v.Encode())
2015-10-05 17:33:45 -07:00
if err != nil {
l4g.Error(utils.T("mattermost.security_info.error"))
2015-10-05 17:33:45 -07:00
return
}
bulletins := model.SecurityBulletinsFromJson(res.Body)
for _, bulletin := range bulletins {
if bulletin.AppliesToVersion == model.CurrentVersion {
if props["SecurityBulletin_"+bulletin.Id] == "" {
if results := <-api.Srv.Store.User().GetSystemAdminProfiles(); results.Err != nil {
l4g.Error(utils.T("mattermost.system_admins.error"))
2015-10-05 17:33:45 -07:00
return
} else {
users := results.Data.(map[string]*model.User)
resBody, err := http.Get(utils.DIAGNOSTIC_URL + "/bulletins/" + bulletin.Id)
if err != nil {
l4g.Error(utils.T("mattermost.security_bulletin.error"))
2015-10-05 17:33:45 -07:00
return
}
body, err := ioutil.ReadAll(resBody.Body)
res.Body.Close()
if err != nil || resBody.StatusCode != 200 {
l4g.Error(utils.T("mattermost.security_bulletin_read.error"))
2015-10-05 17:33:45 -07:00
return
}
for _, user := range users {
l4g.Info(utils.T("mattermost.send_bulletin.info"), bulletin.Id, user.Email)
utils.SendMail(user.Email, utils.T("mattermost.bulletin.subject"), string(body))
2015-10-05 17:33:45 -07:00
}
}
bulletinSeen := &model.System{Name: "SecurityBulletin_" + bulletin.Id, Value: bulletin.Id}
<-api.Srv.Store.System().Save(bulletinSeen)
2015-10-05 17:33:45 -07:00
}
}
}
2015-10-01 13:02:04 -07:00
}
}
}
2015-10-05 17:33:45 -07:00
time.Sleep(time.Hour * 4)
2015-10-01 13:02:04 -07:00
}
}()
}
2015-09-04 11:59:10 -07:00
func parseCmds() {
flag.Usage = func() {
fmt.Fprintln(os.Stderr, usage)
}
flag.StringVar(&flagConfigFile, "config", "config.json", "")
flag.StringVar(&flagEmail, "email", "", "")
flag.StringVar(&flagPassword, "password", "", "")
flag.StringVar(&flagTeamName, "team_name", "", "")
flag.StringVar(&flagRole, "role", "", "")
flag.BoolVar(&flagCmdCreateTeam, "create_team", false, "")
flag.BoolVar(&flagCmdCreateUser, "create_user", false, "")
flag.BoolVar(&flagCmdAssignRole, "assign_role", false, "")
flag.BoolVar(&flagCmdVersion, "version", false, "")
2015-09-04 11:59:10 -07:00
flag.BoolVar(&flagCmdResetPassword, "reset_password", false, "")
2015-11-18 08:53:17 -08:00
flag.BoolVar(&flagCmdPermanentDeleteUser, "permanent_delete_user", false, "")
flag.BoolVar(&flagCmdPermanentDeleteTeam, "permanent_delete_team", false, "")
2015-09-04 11:59:10 -07:00
flag.Parse()
2015-11-18 08:53:17 -08:00
flagRunCmds = (flagCmdCreateTeam ||
flagCmdCreateUser ||
flagCmdAssignRole ||
flagCmdResetPassword ||
flagCmdVersion ||
flagCmdPermanentDeleteUser ||
flagCmdPermanentDeleteTeam)
2015-09-04 11:59:10 -07:00
}
func runCmds() {
cmdVersion()
2015-09-04 11:59:10 -07:00
cmdCreateTeam()
cmdCreateUser()
cmdAssignRole()
cmdResetPassword()
2015-11-16 17:12:49 -08:00
cmdPermDeleteUser()
cmdPermDeleteTeam()
2015-09-04 11:59:10 -07:00
}
func cmdCreateTeam() {
if flagCmdCreateTeam {
if len(flagTeamName) == 0 {
fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name")
flag.Usage()
os.Exit(1)
}
if len(flagEmail) == 0 {
fmt.Fprintln(os.Stderr, "flag needs an argument: -email")
flag.Usage()
os.Exit(1)
}
2016-02-22 19:12:35 -05:00
c := getMockContext()
2015-09-04 11:59:10 -07:00
team := &model.Team{}
team.DisplayName = flagTeamName
team.Name = flagTeamName
team.Email = flagEmail
team.Type = model.TEAM_INVITE
api.CreateTeam(c, team)
if c.Err != nil {
if c.Err.Id != "store.sql_team.save.domain_exists.app_error" {
2015-09-04 11:59:10 -07:00
l4g.Error("%v", c.Err)
flushLogAndExit(1)
}
}
os.Exit(0)
}
}
func cmdCreateUser() {
if flagCmdCreateUser {
if len(flagTeamName) == 0 {
fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name")
flag.Usage()
os.Exit(1)
}
if len(flagEmail) == 0 {
fmt.Fprintln(os.Stderr, "flag needs an argument: -email")
flag.Usage()
os.Exit(1)
}
if len(flagPassword) == 0 {
fmt.Fprintln(os.Stderr, "flag needs an argument: -password")
flag.Usage()
os.Exit(1)
}
var team *model.Team
user := &model.User{}
user.Email = flagEmail
user.Password = flagPassword
splits := strings.Split(strings.Replace(flagEmail, "@", " ", -1), " ")
user.Username = splits[0]
if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
2015-09-04 11:59:10 -07:00
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
team = result.Data.(*model.Team)
user.TeamId = team.Id
}
_, err := api.CreateUser(team, user)
2015-12-08 13:38:43 -05:00
if err != nil {
if err.Id != "store.sql_user.save.email_exists.app_error" {
2015-12-08 13:38:43 -05:00
l4g.Error("%v", err)
2015-09-04 11:59:10 -07:00
flushLogAndExit(1)
}
}
os.Exit(0)
2015-06-14 23:53:32 -08:00
}
2015-09-04 11:59:10 -07:00
}
func cmdVersion() {
if flagCmdVersion {
2015-09-17 13:01:40 -07:00
fmt.Fprintln(os.Stderr, "Version: "+model.CurrentVersion)
fmt.Fprintln(os.Stderr, "Build Number: "+model.BuildNumber)
fmt.Fprintln(os.Stderr, "Build Date: "+model.BuildDate)
fmt.Fprintln(os.Stderr, "Build Hash: "+model.BuildHash)
2015-12-08 13:38:43 -05:00
fmt.Fprintln(os.Stderr, "Build Enterprise Ready: "+model.BuildEnterpriseReady)
os.Exit(0)
}
}
2015-09-04 11:59:10 -07:00
func cmdAssignRole() {
if flagCmdAssignRole {
if len(flagTeamName) == 0 {
fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name")
flag.Usage()
os.Exit(1)
}
if len(flagEmail) == 0 {
fmt.Fprintln(os.Stderr, "flag needs an argument: -email")
flag.Usage()
os.Exit(1)
}
if !model.IsValidRoles(flagRole) {
fmt.Fprintln(os.Stderr, "flag invalid argument: -role")
flag.Usage()
os.Exit(1)
}
2016-02-22 19:12:35 -05:00
c := getMockContext()
2015-06-14 23:53:32 -08:00
2015-09-04 11:59:10 -07:00
var team *model.Team
if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
2015-09-04 11:59:10 -07:00
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
team = result.Data.(*model.Team)
}
2015-06-14 23:53:32 -08:00
2015-09-04 11:59:10 -07:00
var user *model.User
if result := <-api.Srv.Store.User().GetByEmail(team.Id, flagEmail); result.Err != nil {
2015-09-04 11:59:10 -07:00
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
user = result.Data.(*model.User)
}
if !user.IsInRole(flagRole) {
api.UpdateRoles(c, user, flagRole)
}
os.Exit(0)
}
2015-06-14 23:53:32 -08:00
}
2015-09-04 11:59:10 -07:00
func cmdResetPassword() {
if flagCmdResetPassword {
if len(flagTeamName) == 0 {
fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name")
flag.Usage()
os.Exit(1)
}
if len(flagEmail) == 0 {
fmt.Fprintln(os.Stderr, "flag needs an argument: -email")
flag.Usage()
os.Exit(1)
}
if len(flagPassword) == 0 {
fmt.Fprintln(os.Stderr, "flag needs an argument: -password")
flag.Usage()
os.Exit(1)
}
2015-09-04 16:56:18 -07:00
if len(flagPassword) < 5 {
fmt.Fprintln(os.Stderr, "flag invalid argument needs to be more than 4 characters: -password")
flag.Usage()
os.Exit(1)
}
2015-09-04 11:59:10 -07:00
var team *model.Team
if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
2015-09-04 11:59:10 -07:00
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
team = result.Data.(*model.Team)
}
var user *model.User
if result := <-api.Srv.Store.User().GetByEmail(team.Id, flagEmail); result.Err != nil {
2015-09-04 11:59:10 -07:00
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 {
2015-09-04 11:59:10 -07:00
l4g.Error("%v", result.Err)
flushLogAndExit(1)
}
os.Exit(0)
}
}
2015-11-16 17:12:49 -08:00
func cmdPermDeleteUser() {
2015-11-18 08:53:17 -08:00
if flagCmdPermanentDeleteUser {
2015-11-16 17:12:49 -08:00
if len(flagTeamName) == 0 {
fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name")
flag.Usage()
os.Exit(1)
}
if len(flagEmail) == 0 {
fmt.Fprintln(os.Stderr, "flag needs an argument: -email")
flag.Usage()
os.Exit(1)
}
2016-02-22 19:12:35 -05:00
c := getMockContext()
2015-11-16 17:12:49 -08:00
var team *model.Team
if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
2015-11-16 17:12:49 -08:00
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
team = result.Data.(*model.Team)
}
var user *model.User
if result := <-api.Srv.Store.User().GetByEmail(team.Id, flagEmail); result.Err != nil {
2015-11-16 17:12:49 -08:00
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
user = result.Data.(*model.User)
}
var confirmBackup string
fmt.Print("Have you performed a database backup? (YES/NO): ")
fmt.Scanln(&confirmBackup)
if confirmBackup != "YES" {
flushLogAndExit(1)
}
var confirm string
fmt.Printf("Are you sure you want to delete the user %v? All data will be permanently deleted? (YES/NO): ", user.Email)
fmt.Scanln(&confirm)
if confirm != "YES" {
flushLogAndExit(1)
}
if err := api.PermanentDeleteUser(c, user); err != nil {
l4g.Error("%v", err)
flushLogAndExit(1)
} else {
flushLogAndExit(0)
}
}
}
func cmdPermDeleteTeam() {
2015-11-18 08:53:17 -08:00
if flagCmdPermanentDeleteTeam {
2015-11-16 17:12:49 -08:00
if len(flagTeamName) == 0 {
fmt.Fprintln(os.Stderr, "flag needs an argument: -team_name")
flag.Usage()
os.Exit(1)
}
2016-02-22 19:12:35 -05:00
c := getMockContext()
2015-11-16 17:12:49 -08:00
var team *model.Team
if result := <-api.Srv.Store.Team().GetByName(flagTeamName); result.Err != nil {
2015-11-16 17:12:49 -08:00
l4g.Error("%v", result.Err)
flushLogAndExit(1)
} else {
team = result.Data.(*model.Team)
}
var confirmBackup string
fmt.Print("Have you performed a database backup? (YES/NO): ")
fmt.Scanln(&confirmBackup)
if confirmBackup != "YES" {
flushLogAndExit(1)
}
var confirm string
fmt.Printf("Are you sure you want to delete the team %v? All data will be permanently deleted? (YES/NO): ", team.Name)
fmt.Scanln(&confirm)
if confirm != "YES" {
flushLogAndExit(1)
}
if err := api.PermanentDeleteTeam(c, team); err != nil {
l4g.Error("%v", err)
flushLogAndExit(1)
} else {
flushLogAndExit(0)
}
}
}
2015-09-04 11:59:10 -07:00
func flushLogAndExit(code int) {
l4g.Close()
time.Sleep(time.Second)
os.Exit(code)
}
2016-02-22 19:12:35 -05:00
func getMockContext() *api.Context {
c := &api.Context{}
c.RequestId = model.NewId()
c.IpAddress = "cmd_line"
c.T = utils.TfuncWithFallback(model.DEFAULT_LOCALE)
c.Locale = model.DEFAULT_LOCALE
return c
}
2015-09-04 11:59:10 -07:00
var usage = `Mattermost commands to help configure the system
2015-12-04 16:20:48 -08:00
NAME:
platform -- platform configuation tool
USAGE:
2015-09-04 11:59:10 -07:00
platform [options]
2015-12-04 16:20:48 -08:00
FLAGS:
2015-09-04 11:59:10 -07:00
-config="config.json" Path to the config file
-email="user@example.com" Email address used in other commands
-password="mypassword" Password used in other commands
-team_name="name" The team name used in other commands
-role="admin" The role used in other commands
valid values are
"" - The empty role is basic user
2015-09-04 11:59:10 -07:00
permissions
"admin" - Represents a team admin and
is used to help administer one team.
2015-09-04 11:59:10 -07:00
"system_admin" - Represents a system
admin who has access to all teams
2015-12-04 16:20:48 -08:00
and configuration settings.
COMMANDS:
2015-09-15 19:56:21 -07:00
-create_team Creates a team. It requires the -team_name
2015-09-04 11:59:10 -07:00
and -email flag to create a team.
Example:
platform -create_team -team_name="name" -email="user@example.com"
2015-09-15 19:56:21 -07:00
-create_user Creates a user. It requires the -team_name,
2015-09-04 11:59:10 -07:00
-email and -password flag to create a user.
Example:
platform -create_user -team_name="name" -email="user@example.com" -password="mypassword"
2015-09-15 19:56:21 -07:00
-assign_role Assigns role to a user. It requires the -role,
2015-12-04 16:20:48 -08:00
-email and -team_name flag. You may need to log out
2015-09-15 09:19:29 -07:00
of your current sessions for the new role to be
applied.
2015-09-04 11:59:10 -07:00
Example:
platform -assign_role -team_name="name" -email="user@example.com" -role="admin"
2015-09-15 19:56:21 -07:00
-reset_password Resets the password for a user. It requires the
2015-09-04 11:59:10 -07:00
-team_name, -email and -password flag.
Example:
2015-10-01 16:37:15 -04:00
platform -reset_password -team_name="name" -email="user@example.com" -password="newpassword"
2015-09-04 11:59:10 -07:00
2015-11-16 17:12:49 -08:00
-permanent_delete_user Permanently deletes a user and all related information
2015-12-04 16:20:48 -08:00
including posts from the database. It requires the
2015-11-16 18:30:04 -08:00
-team_name, and -email flag. You may need to restart the
2015-12-04 16:20:48 -08:00
server to invalidate the cache
2015-11-16 17:12:49 -08:00
Example:
platform -permanent_delete_user -team_name="name" -email="user@example.com"
-permanent_delete_team Permanently deletes a team and all users along with
2015-12-04 16:20:48 -08:00
all related information including posts from the database.
2015-11-16 18:30:04 -08:00
It requires the -team_name flag. You may need to restart
the server to invalidate the cache.
2015-11-16 17:12:49 -08:00
Example:
platform -permanent_delete_team -team_name="name"
2015-12-04 16:20:48 -08:00
-version Display the current of the Mattermost platform
2015-09-04 11:59:10 -07:00
2015-12-04 16:20:48 -08:00
-help Displays this help page`