Files
mattermost/model/client4.go

4818 lines
171 KiB
Go
Raw Normal View History

// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
const (
HEADER_REQUEST_ID = "X-Request-ID"
HEADER_VERSION_ID = "X-Version-ID"
HEADER_CLUSTER_ID = "X-Cluster-ID"
HEADER_ETAG_SERVER = "ETag"
HEADER_ETAG_CLIENT = "If-None-Match"
HEADER_FORWARDED = "X-Forwarded-For"
HEADER_REAL_IP = "X-Real-IP"
HEADER_FORWARDED_PROTO = "X-Forwarded-Proto"
HEADER_TOKEN = "token"
HEADER_CSRF_TOKEN = "X-CSRF-Token"
HEADER_BEARER = "BEARER"
HEADER_AUTH = "Authorization"
HEADER_REQUESTED_WITH = "X-Requested-With"
HEADER_REQUESTED_WITH_XML = "XMLHttpRequest"
STATUS = "status"
STATUS_OK = "OK"
STATUS_FAIL = "FAIL"
STATUS_UNHEALTHY = "UNHEALTHY"
STATUS_REMOVE = "REMOVE"
CLIENT_DIR = "client"
API_URL_SUFFIX_V1 = "/api/v1"
API_URL_SUFFIX_V4 = "/api/v4"
API_URL_SUFFIX = API_URL_SUFFIX_V4
)
type Response struct {
StatusCode int
Error *AppError
RequestId string
Etag string
ServerVersion string
Header http.Header
}
type Client4 struct {
Url string // The location of the server, for example "http://localhost:8065"
ApiUrl string // The api location of the server, for example "http://localhost:8065/api/v4"
HttpClient *http.Client // The http client
AuthToken string
AuthType string
HttpHeader map[string]string // Headers to be copied over for each request
}
func closeBody(r *http.Response) {
if r.Body != nil {
_, _ = ioutil.ReadAll(r.Body)
_ = r.Body.Close()
}
}
// Must is a convenience function used for testing.
func (c *Client4) Must(result interface{}, resp *Response) interface{} {
if resp.Error != nil {
time.Sleep(time.Second)
panic(resp.Error)
}
return result
}
func NewAPIv4Client(url string) *Client4 {
return &Client4{url, url + API_URL_SUFFIX, &http.Client{}, "", "", map[string]string{}}
}
2017-06-21 15:50:51 -04:00
func BuildErrorResponse(r *http.Response, err *AppError) *Response {
var statusCode int
var header http.Header
if r != nil {
statusCode = r.StatusCode
header = r.Header
2017-06-21 15:50:51 -04:00
} else {
statusCode = 0
2017-10-30 11:57:24 -05:00
header = make(http.Header)
}
return &Response{
StatusCode: statusCode,
Error: err,
Header: header,
2017-06-21 15:50:51 -04:00
}
}
func BuildResponse(r *http.Response) *Response {
return &Response{
StatusCode: r.StatusCode,
RequestId: r.Header.Get(HEADER_REQUEST_ID),
Etag: r.Header.Get(HEADER_ETAG_SERVER),
ServerVersion: r.Header.Get(HEADER_VERSION_ID),
Header: r.Header,
}
}
func (c *Client4) SetToken(token string) {
c.AuthToken = token
c.AuthType = HEADER_BEARER
}
// MockSession is deprecated in favour of SetToken
func (c *Client4) MockSession(token string) {
c.SetToken(token)
}
func (c *Client4) SetOAuthToken(token string) {
c.AuthToken = token
c.AuthType = HEADER_TOKEN
}
func (c *Client4) ClearOAuthToken() {
c.AuthToken = ""
c.AuthType = HEADER_BEARER
}
func (c *Client4) GetUsersRoute() string {
return fmt.Sprintf("/users")
}
func (c *Client4) GetUserRoute(userId string) string {
return fmt.Sprintf(c.GetUsersRoute()+"/%v", userId)
}
func (c *Client4) GetUserAccessTokensRoute() string {
return fmt.Sprintf(c.GetUsersRoute() + "/tokens")
}
func (c *Client4) GetUserAccessTokenRoute(tokenId string) string {
return fmt.Sprintf(c.GetUsersRoute()+"/tokens/%v", tokenId)
}
func (c *Client4) GetUserByUsernameRoute(userName string) string {
return fmt.Sprintf(c.GetUsersRoute()+"/username/%v", userName)
}
func (c *Client4) GetUserByEmailRoute(email string) string {
return fmt.Sprintf(c.GetUsersRoute()+"/email/%v", email)
}
MM-12393 Server side of bot accounts. (#10378) * bots model, store and api (#9903) * bots model, store and api Fixes: MM-13100, MM-13101, MM-13103, MM-13105, MMM-13119 * uncomment tests incorrectly commented, and fix merge issues * add etags support * add missing licenses * remove unused sqlbuilder.go (for now...) * rejig permissions * split out READ_BOTS into READ_BOTS and READ_OTHERS_BOTS, the latter implicitly allowing the former * make MANAGE_OTHERS_BOTS imply MANAGE_BOTS * conform to general rest api pattern * eliminate redundant http.StatusOK * Update api4/bot.go Co-Authored-By: lieut-data <jesse.hallam@gmail.com> * s/model.UserFromBotModel/model.UserFromBot/g * Update model/bot.go Co-Authored-By: lieut-data <jesse.hallam@gmail.com> * Update model/client4.go Co-Authored-By: lieut-data <jesse.hallam@gmail.com> * move sessionHasPermissionToManageBot to app/authorization.go * use api.ApiSessionRequired for createBot * introduce BOT_DESCRIPTION_MAX_RUNES constant * MM-13512 Prevent getting a user by email based on privacy settings (#10021) * MM-13512 Prevent getting a user by email based on privacy settings * Add additional config settings to tests * upgrade db to 5.7 (#10019) * MM-13526 Add validation when setting a user's Locale field (#10022) * Fix typos (#10024) * Fixing first user being created with system admin privilages without being explicity specified. (#10014) * Revert "Support for Embeded chat (#9129)" (#10017) This reverts commit 3fcecd521a5c6ccfdb52fb4c3fb1f8c6ea528a4e. * s/DisableBot/UpdateBotActive * add permissions on upgrade * Update NOTICE.txt (#10054) - add new dependency (text) - handle switch to forked dependency (go-gomail -> go-mail) - misc copyright owner updates * avoid leaking bot knowledge without permission * [GH-6798] added a new api endpoint to get the bulk reactions for posts (#10049) * 6798 added a new api to get the bulk reactions for posts * 6798 added the permsission check before getting the reactions * GH-6798 added a new app function for the new endpoint * 6798 added a store method to get reactions for multiple posts * 6798 connected the app function with the new store function * 6798 fixed the review comments * MM-13559 Update model.post.is_valid.file_ids.app_error text per report (#10055) Ticket: https://mattermost.atlassian.net/browse/MM-13559 Report: https://github.com/mattermost/mattermost-server/issues/10023 * Trigger Login Hooks with OAuth (#10061) * make BotStore.GetAll deterministic even on duplicate CreateAt * fix spurious TestMuteCommandSpecificChannel test failure See https://community-daily.mattermost.com/core/pl/px9p8s3dzbg1pf3ddrm5cr36uw * fix race in TestExportUserChannels * TestExportUserChannels: remove SaveMember call, as it is redundant and used to be silently failing anyway * MM-13117: bot tokens (#10111) * eliminate redundant Client/AdminClient declarations * harden TestUpdateChannelScheme to API failures * eliminate unnecessary config restoration * minor cleanup * make TestGenerateMfaSecret config dependency explicit * TestCreateUserAccessToken for bots * TestGetUserAccessToken* for bots * leverage SessionHasPermissionToUserOrBot for user token APIs * Test(Revoke|Disable|Enable)UserAccessToken * make EnableUserAccessTokens explicit, so as to not rely on local config.json * uncomment TestResetPassword, but still skip * mark assert(Invalid)Token as helper * fix whitespace issues * fix mangled comments * MM-13116: bot plugin api (#10113) * MM-13117: expose bot API to plugins This also changes the `CreatorId` column definition to allow for plugin ids, as the default unless the plugin overrides is to use the plugin id here. This branch hasn't hit master yet, so no migration needed. * gofmt issues * expunge use of BotList in plugin/client API * introduce model.BotGetOptions * use botUserId term for clarity * MM-13129 Adding functionality to deal with orphaned bots (#10238) * Add way to list orphaned bots. * Add /assign route to modify ownership of bot accounts. * Apply suggestions from code review Co-Authored-By: crspeller <crspeller@gmail.com> * MM-13120: add IsBot field to returned user objects (#10103) * MM-13104: forbid bot login (#10251) * MM-13104: disallow bot login * fix shadowing * MM-13136 Disable user bots when user is disabled. (#10293) * Disable user bots when user is disabled. * Grammer. Co-Authored-By: crspeller <crspeller@gmail.com> * Fixing bot branch for test changes. * Don't use external dependancies in bot plugin tests. * Rename bot CreatorId to OwnerId * Adding ability to re-enable bots * Fixing IsBot to not attempt to be saved to DB. * Adding diagnostics and licencing counting for bot accounts. * Modifying gorp to allow reading of '-' fields. * Removing unnessisary nil values from UserCountOptions. * Changing comment to GoDoc format * Improving user count SQL * Some improvments from feedback. * Omit empty on User.IsBot
2019-03-05 07:06:45 -08:00
func (c *Client4) GetBotsRoute() string {
return fmt.Sprintf("/bots")
}
func (c *Client4) GetBotRoute(botUserId string) string {
return fmt.Sprintf("%s/%s", c.GetBotsRoute(), botUserId)
}
func (c *Client4) GetTeamsRoute() string {
return fmt.Sprintf("/teams")
}
func (c *Client4) GetTeamRoute(teamId string) string {
return fmt.Sprintf(c.GetTeamsRoute()+"/%v", teamId)
}
func (c *Client4) GetTeamAutoCompleteCommandsRoute(teamId string) string {
return fmt.Sprintf(c.GetTeamsRoute()+"/%v/commands/autocomplete", teamId)
}
func (c *Client4) GetTeamByNameRoute(teamName string) string {
return fmt.Sprintf(c.GetTeamsRoute()+"/name/%v", teamName)
}
func (c *Client4) GetTeamMemberRoute(teamId, userId string) string {
return fmt.Sprintf(c.GetTeamRoute(teamId)+"/members/%v", userId)
}
func (c *Client4) GetTeamMembersRoute(teamId string) string {
return fmt.Sprintf(c.GetTeamRoute(teamId) + "/members")
}
func (c *Client4) GetTeamStatsRoute(teamId string) string {
return fmt.Sprintf(c.GetTeamRoute(teamId) + "/stats")
}
func (c *Client4) GetTeamImportRoute(teamId string) string {
return fmt.Sprintf(c.GetTeamRoute(teamId) + "/import")
}
func (c *Client4) GetChannelsRoute() string {
return fmt.Sprintf("/channels")
}
func (c *Client4) GetChannelsForTeamRoute(teamId string) string {
return fmt.Sprintf(c.GetTeamRoute(teamId) + "/channels")
}
func (c *Client4) GetChannelRoute(channelId string) string {
return fmt.Sprintf(c.GetChannelsRoute()+"/%v", channelId)
}
func (c *Client4) GetChannelByNameRoute(channelName, teamId string) string {
return fmt.Sprintf(c.GetTeamRoute(teamId)+"/channels/name/%v", channelName)
}
func (c *Client4) GetChannelByNameForTeamNameRoute(channelName, teamName string) string {
return fmt.Sprintf(c.GetTeamByNameRoute(teamName)+"/channels/name/%v", channelName)
}
func (c *Client4) GetChannelMembersRoute(channelId string) string {
return fmt.Sprintf(c.GetChannelRoute(channelId) + "/members")
}
func (c *Client4) GetChannelMemberRoute(channelId, userId string) string {
return fmt.Sprintf(c.GetChannelMembersRoute(channelId)+"/%v", userId)
}
func (c *Client4) GetPostsRoute() string {
return fmt.Sprintf("/posts")
}
func (c *Client4) GetPostsEphemeralRoute() string {
return fmt.Sprintf("/posts/ephemeral")
}
func (c *Client4) GetConfigRoute() string {
return fmt.Sprintf("/config")
}
func (c *Client4) GetLicenseRoute() string {
return fmt.Sprintf("/license")
}
func (c *Client4) GetPostRoute(postId string) string {
return fmt.Sprintf(c.GetPostsRoute()+"/%v", postId)
}
func (c *Client4) GetFilesRoute() string {
return fmt.Sprintf("/files")
}
func (c *Client4) GetFileRoute(fileId string) string {
return fmt.Sprintf(c.GetFilesRoute()+"/%v", fileId)
}
func (c *Client4) GetPluginsRoute() string {
return fmt.Sprintf("/plugins")
}
func (c *Client4) GetPluginRoute(pluginId string) string {
return fmt.Sprintf(c.GetPluginsRoute()+"/%v", pluginId)
}
func (c *Client4) GetSystemRoute() string {
return fmt.Sprintf("/system")
}
func (c *Client4) GetTestEmailRoute() string {
return fmt.Sprintf("/email/test")
}
func (c *Client4) GetTestSiteURLRoute() string {
return fmt.Sprintf("/site_url/test")
}
func (c *Client4) GetTestS3Route() string {
return fmt.Sprintf("/file/s3_test")
}
func (c *Client4) GetDatabaseRoute() string {
return fmt.Sprintf("/database")
}
func (c *Client4) GetCacheRoute() string {
return fmt.Sprintf("/caches")
}
func (c *Client4) GetClusterRoute() string {
return fmt.Sprintf("/cluster")
}
func (c *Client4) GetIncomingWebhooksRoute() string {
return fmt.Sprintf("/hooks/incoming")
}
func (c *Client4) GetIncomingWebhookRoute(hookID string) string {
return fmt.Sprintf(c.GetIncomingWebhooksRoute()+"/%v", hookID)
}
func (c *Client4) GetComplianceReportsRoute() string {
return fmt.Sprintf("/compliance/reports")
}
func (c *Client4) GetComplianceReportRoute(reportId string) string {
return fmt.Sprintf("/compliance/reports/%v", reportId)
}
func (c *Client4) GetOutgoingWebhooksRoute() string {
return fmt.Sprintf("/hooks/outgoing")
}
func (c *Client4) GetOutgoingWebhookRoute(hookID string) string {
return fmt.Sprintf(c.GetOutgoingWebhooksRoute()+"/%v", hookID)
}
func (c *Client4) GetPreferencesRoute(userId string) string {
return fmt.Sprintf(c.GetUserRoute(userId) + "/preferences")
}
func (c *Client4) GetUserStatusRoute(userId string) string {
return fmt.Sprintf(c.GetUserRoute(userId) + "/status")
}
func (c *Client4) GetUserStatusesRoute() string {
return fmt.Sprintf(c.GetUsersRoute() + "/status")
}
func (c *Client4) GetSamlRoute() string {
return fmt.Sprintf("/saml")
}
func (c *Client4) GetLdapRoute() string {
return fmt.Sprintf("/ldap")
}
func (c *Client4) GetBrandRoute() string {
return fmt.Sprintf("/brand")
}
func (c *Client4) GetDataRetentionRoute() string {
return fmt.Sprintf("/data_retention")
}
func (c *Client4) GetElasticsearchRoute() string {
return fmt.Sprintf("/elasticsearch")
}
func (c *Client4) GetCommandsRoute() string {
return fmt.Sprintf("/commands")
}
func (c *Client4) GetCommandRoute(commandId string) string {
return fmt.Sprintf(c.GetCommandsRoute()+"/%v", commandId)
}
func (c *Client4) GetEmojisRoute() string {
return fmt.Sprintf("/emoji")
}
func (c *Client4) GetEmojiRoute(emojiId string) string {
return fmt.Sprintf(c.GetEmojisRoute()+"/%v", emojiId)
}
func (c *Client4) GetEmojiByNameRoute(name string) string {
return fmt.Sprintf(c.GetEmojisRoute()+"/name/%v", name)
}
func (c *Client4) GetReactionsRoute() string {
return fmt.Sprintf("/reactions")
}
func (c *Client4) GetOAuthAppsRoute() string {
return fmt.Sprintf("/oauth/apps")
}
func (c *Client4) GetOAuthAppRoute(appId string) string {
return fmt.Sprintf("/oauth/apps/%v", appId)
}
func (c *Client4) GetOpenGraphRoute() string {
return fmt.Sprintf("/opengraph")
}
func (c *Client4) GetJobsRoute() string {
return fmt.Sprintf("/jobs")
}
func (c *Client4) GetRolesRoute() string {
return fmt.Sprintf("/roles")
}
func (c *Client4) GetSchemesRoute() string {
return fmt.Sprintf("/schemes")
}
func (c *Client4) GetSchemeRoute(id string) string {
return c.GetSchemesRoute() + fmt.Sprintf("/%v", id)
}
func (c *Client4) GetAnalyticsRoute() string {
return fmt.Sprintf("/analytics")
}
func (c *Client4) GetTimezonesRoute() string {
return fmt.Sprintf(c.GetSystemRoute() + "/timezones")
}
func (c *Client4) GetChannelSchemeRoute(channelId string) string {
return fmt.Sprintf(c.GetChannelsRoute()+"/%v/scheme", channelId)
}
func (c *Client4) GetTeamSchemeRoute(teamId string) string {
return fmt.Sprintf(c.GetTeamsRoute()+"/%v/scheme", teamId)
}
func (c *Client4) GetTotalUsersStatsRoute() string {
return fmt.Sprintf(c.GetUsersRoute() + "/stats")
}
func (c *Client4) GetRedirectLocationRoute() string {
return fmt.Sprintf("/redirect_location")
}
func (c *Client4) GetServerBusyRoute() string {
return "/server_busy"
}
func (c *Client4) GetUserTermsOfServiceRoute(userId string) string {
return c.GetUserRoute(userId) + "/terms_of_service"
}
func (c *Client4) GetTermsOfServiceRoute() string {
return "/terms_of_service"
}
Ldap groups phase1 (#9752) * Initial models, API, app, and persistence of groups and group syncing. * Consistent letter casing in ldif. * Moves group-specific migrations into func. * Adds API endpoint to retrieve LDAP groups (and associated MM groups) one tree level at a time. * Adds mattermost group id to SCIMGroup (if available). * Splits user and group creation so that memberOf works. Returns users from ldap interface. * Updates method name. * Returns users IDs instead of User. * Removes non-essential group data. * MM-11807: Add GroupFilter to LDAP config. (#9513) * MM-11807: Add GroupFilter to LDAP config. * Add diagnostic. * Adds new config option for using 'memberOf' overlay. * Adds API endpoint to link a group. * Removes debug statements. * Adds unlink group API endpoint. * Fix to LDAP API. Adds API method to client4 and app. * Adds some missing app methods. Renames API unexported func. * Fixes link/unlink API path to accept valid DNs. * Allow any character for DN portion of path. * Switches from DN to objectGUID or entryUUID as the remote identifier linking LDAP groups to MM groups. * Formatting. * Formatting. * Setting group name field to an ID for phase 1. * Adds an LDAP config field to Setting up configuration for local LDAP. * Changes to LDAP and GroupStore interfaces. * Draft of nesting groups in API response. * Removes unnecessary tree models. * Updates group membershipt create store method to also restore. * Adds new config to test config. * Accept AD format length. * Switches to SetUniqueTogether method. * Updates revert. * Tweaks to syncing queries . * Updates query for pending team and channel memberships. * Removes old GroupSyncableScanner usage. Some formatting and renaming. * Fixes bug setting syncable type in selecting paged. * Adds tests for syncables populator. * Only add users to teams and channels that are not deleted. * Renames method. * Updates test LDAP setup. * Removes memberof config stuff. * Renames. * Updates test data. * Fix for gofmt. * Adds missing license. * Adds missing teardowns. * Test fix. * Adds a cycle to the groups test data. * Changes API to return flat list. * Removes some unused interface and app methods. * Returns empty braces if results are empty. * Adds more LDAP test data. * Fix for test data error. * Adds error. * Moves test groups. * Adds OU for load test data. * Moves load test ou creation to load data. * Adds a new bool flag to SCIMGroups. * Removes SCIMGroup completely. * Removes FULL JOIN because it is not supported in MySQL. * Adds tests for sync queries; renames constant. * Bad merge fix. * Vet fix. * Returning OK on delete ldap group link * Removes foreign key constraints. * Adding total to the ldap getAllGroups api endpoint * Adds get group members page. * Removes pagination from groups syncables list API. * Adding syncable check now that foreign key constraint is removes. * Joins teams and channels to group syncables. * Adds group member count. * Adding GetAllChannels and SearchAllChannels for system admins only * Fix. * Test fix from pagination removal. * Orders groupmembers by createat. * Fixing search of all channels * Test fix after removing pagination. * JSON syntax error fix. * Changing tests (for now) pending investigation. * Adding GetAllChannels and SearchAllChannels tests for the store * Adding GetAllChannels and SearchAllChannels API tests * Omit empty JSON values of group syncables. * Fixing GetAllChannels and SearchAllChannels tests * Fixing GetAllChannels and SearchAllChannels store tests * Fixing GetAllChannels api tests * Adds 'LDAP groups' feature flag. (#9861) * Migrate new client functions to idiomatic error handling * Test fixes. * Simplification of groups api (#9860) * Simplification of groups api * Fixing RequireSyncableType * Test fix. * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Fix copy/paste error. * Fix copy/paste error. * Adds missing return, changes to correct HTTP status code. * Adds missing return, changes status codes. * Check for license. * Renames variable for new signature. * Adds client method to get a group. * Adds client method and tests for PatchGroup. * Adds more API tests. * Adds groups API tests. * Adds client method and tests for getting group syncables. * Adds tests for patching group teams and channels. * Update to translations. * Removes test. * Fix incorrect conditional. * Removes unnecessary nil check. * Removes unnecessary return. * Updates comment, removes unused variable. * Uses consistent JSON unmarshal pattern. * Uses consistent JSON unmarshal pattern. * Moves const block. * Switches 'already linked' from error to success response. * Removes commented-out code. * Switched to status ok. * Add parens for readability. * Fix copy/paste error. * Unexport some structs. * Removes repeated validity check. * Return without attempting commit if there's a rollback. * Fix incorrect HTTP status code. * Update store/sqlstore/group_supplier.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Adds utility methods for going from groupsyncable to groupteam and groupchannel. * Fixing george suggestions (#9911) * Test fix. * Adds QA data to VC with visualization. * Fixes typo in graph image. * Update display name when re-linking in case it has changed in LDAP. * Adds ability to configure group display name and unique identifier. (#9923) * Adds ability to configure group display name and unique identifier. * Adds some configs to confi-ldap make command. * Fix for move of session. * Exposes method for use by SAML package. * Switches GroupSyncableType from int to string. * Update Jenkins build files. * Removes unused variable assignment. * Removes old unnecessary early return. * Removes unnecessary variable. * Moves param parsing before license and permissions checks. * Removes old code. * Compares agains underlying error rather than error id. * Switches tests to assertions. * Adds more assertions. * Adds missing return. * Adds space after comma for added legibility. * Moves a view model to the api package. * Unexports method. * Uses id validator function. * Fix docker-compose flag. * Typo fix. * Moves index creation to supplier. * Removes bad merge. * Renames parameter. * Re-adds space. * Removes unnecessary transaction. * Escapes the Groups table name with backticks because it is a reserved keyword. * Fix roles cache bug * Removing unnecesiary deserializing function * Switches table name rather than custom SQL everywhere for Postgres without backticks. * Removes redundant check for sql.ErrNoRows. * Removes redundant check for sql.ErrNoRows. * Removes data integrity check and redundant nil conditional. * Removes redundant check for sql.ErrNoRows. * Removes unnecessary query. * Removes ID length validation from persistence tier. * Makes some supplier methods idempotent. * Removes some empty switch defaults. * Renames Group Type field to Source. * Fix for mistaken field name change. * Uses IsValidId function. * Removes comment. * Changes json key name. * Removes test because no longer validating user. * Moves model state validation to app layer. * Don't create Groups.CanLeave column until phase 2. * Removes state validation until properties are used in phase 2. * Removes duplicated check. * Removes state validation until properties are used in phase 2. * Removes some tests until phase 2. * Comment-out a bunch of test related to CanLeave. * Extra unmarshal validation check. Removes more code for CanLeave. * Removes tests for CanLeave. * Explict error msg. * Rewrite queries. * Changes index name. Adds index. * Removes assertion. * Adds experimental feature flag.
2019-01-10 15:17:31 -05:00
func (c *Client4) GetGroupsRoute() string {
return "/groups"
}
func (c *Client4) GetGroupRoute(groupID string) string {
return fmt.Sprintf("%s/%s", c.GetGroupsRoute(), groupID)
}
func (c *Client4) GetGroupSyncableRoute(groupID, syncableID string, syncableType GroupSyncableType) string {
return fmt.Sprintf("%s/%ss/%s", c.GetGroupRoute(groupID), strings.ToLower(syncableType.String()), syncableID)
}
func (c *Client4) GetGroupSyncablesRoute(groupID string, syncableType GroupSyncableType) string {
return fmt.Sprintf("%s/%ss", c.GetGroupRoute(groupID), strings.ToLower(syncableType.String()))
}
func (c *Client4) DoApiGet(url string, etag string) (*http.Response, *AppError) {
return c.DoApiRequest(http.MethodGet, c.ApiUrl+url, "", etag)
}
func (c *Client4) DoApiPost(url string, data string) (*http.Response, *AppError) {
return c.DoApiRequest(http.MethodPost, c.ApiUrl+url, data, "")
}
func (c *Client4) doApiPostBytes(url string, data []byte) (*http.Response, *AppError) {
return c.doApiRequestBytes(http.MethodPost, c.ApiUrl+url, data, "")
}
func (c *Client4) DoApiPut(url string, data string) (*http.Response, *AppError) {
return c.DoApiRequest(http.MethodPut, c.ApiUrl+url, data, "")
}
MM-12393 Server side of bot accounts. (#10378) * bots model, store and api (#9903) * bots model, store and api Fixes: MM-13100, MM-13101, MM-13103, MM-13105, MMM-13119 * uncomment tests incorrectly commented, and fix merge issues * add etags support * add missing licenses * remove unused sqlbuilder.go (for now...) * rejig permissions * split out READ_BOTS into READ_BOTS and READ_OTHERS_BOTS, the latter implicitly allowing the former * make MANAGE_OTHERS_BOTS imply MANAGE_BOTS * conform to general rest api pattern * eliminate redundant http.StatusOK * Update api4/bot.go Co-Authored-By: lieut-data <jesse.hallam@gmail.com> * s/model.UserFromBotModel/model.UserFromBot/g * Update model/bot.go Co-Authored-By: lieut-data <jesse.hallam@gmail.com> * Update model/client4.go Co-Authored-By: lieut-data <jesse.hallam@gmail.com> * move sessionHasPermissionToManageBot to app/authorization.go * use api.ApiSessionRequired for createBot * introduce BOT_DESCRIPTION_MAX_RUNES constant * MM-13512 Prevent getting a user by email based on privacy settings (#10021) * MM-13512 Prevent getting a user by email based on privacy settings * Add additional config settings to tests * upgrade db to 5.7 (#10019) * MM-13526 Add validation when setting a user's Locale field (#10022) * Fix typos (#10024) * Fixing first user being created with system admin privilages without being explicity specified. (#10014) * Revert "Support for Embeded chat (#9129)" (#10017) This reverts commit 3fcecd521a5c6ccfdb52fb4c3fb1f8c6ea528a4e. * s/DisableBot/UpdateBotActive * add permissions on upgrade * Update NOTICE.txt (#10054) - add new dependency (text) - handle switch to forked dependency (go-gomail -> go-mail) - misc copyright owner updates * avoid leaking bot knowledge without permission * [GH-6798] added a new api endpoint to get the bulk reactions for posts (#10049) * 6798 added a new api to get the bulk reactions for posts * 6798 added the permsission check before getting the reactions * GH-6798 added a new app function for the new endpoint * 6798 added a store method to get reactions for multiple posts * 6798 connected the app function with the new store function * 6798 fixed the review comments * MM-13559 Update model.post.is_valid.file_ids.app_error text per report (#10055) Ticket: https://mattermost.atlassian.net/browse/MM-13559 Report: https://github.com/mattermost/mattermost-server/issues/10023 * Trigger Login Hooks with OAuth (#10061) * make BotStore.GetAll deterministic even on duplicate CreateAt * fix spurious TestMuteCommandSpecificChannel test failure See https://community-daily.mattermost.com/core/pl/px9p8s3dzbg1pf3ddrm5cr36uw * fix race in TestExportUserChannels * TestExportUserChannels: remove SaveMember call, as it is redundant and used to be silently failing anyway * MM-13117: bot tokens (#10111) * eliminate redundant Client/AdminClient declarations * harden TestUpdateChannelScheme to API failures * eliminate unnecessary config restoration * minor cleanup * make TestGenerateMfaSecret config dependency explicit * TestCreateUserAccessToken for bots * TestGetUserAccessToken* for bots * leverage SessionHasPermissionToUserOrBot for user token APIs * Test(Revoke|Disable|Enable)UserAccessToken * make EnableUserAccessTokens explicit, so as to not rely on local config.json * uncomment TestResetPassword, but still skip * mark assert(Invalid)Token as helper * fix whitespace issues * fix mangled comments * MM-13116: bot plugin api (#10113) * MM-13117: expose bot API to plugins This also changes the `CreatorId` column definition to allow for plugin ids, as the default unless the plugin overrides is to use the plugin id here. This branch hasn't hit master yet, so no migration needed. * gofmt issues * expunge use of BotList in plugin/client API * introduce model.BotGetOptions * use botUserId term for clarity * MM-13129 Adding functionality to deal with orphaned bots (#10238) * Add way to list orphaned bots. * Add /assign route to modify ownership of bot accounts. * Apply suggestions from code review Co-Authored-By: crspeller <crspeller@gmail.com> * MM-13120: add IsBot field to returned user objects (#10103) * MM-13104: forbid bot login (#10251) * MM-13104: disallow bot login * fix shadowing * MM-13136 Disable user bots when user is disabled. (#10293) * Disable user bots when user is disabled. * Grammer. Co-Authored-By: crspeller <crspeller@gmail.com> * Fixing bot branch for test changes. * Don't use external dependancies in bot plugin tests. * Rename bot CreatorId to OwnerId * Adding ability to re-enable bots * Fixing IsBot to not attempt to be saved to DB. * Adding diagnostics and licencing counting for bot accounts. * Modifying gorp to allow reading of '-' fields. * Removing unnessisary nil values from UserCountOptions. * Changing comment to GoDoc format * Improving user count SQL * Some improvments from feedback. * Omit empty on User.IsBot
2019-03-05 07:06:45 -08:00
func (c *Client4) doApiPutBytes(url string, data []byte) (*http.Response, *AppError) {
return c.doApiRequestBytes(http.MethodPut, c.ApiUrl+url, data, "")
}
func (c *Client4) DoApiDelete(url string) (*http.Response, *AppError) {
return c.DoApiRequest(http.MethodDelete, c.ApiUrl+url, "", "")
}
func (c *Client4) DoApiRequest(method, url, data, etag string) (*http.Response, *AppError) {
return c.doApiRequestReader(method, url, strings.NewReader(data), etag)
}
func (c *Client4) doApiRequestBytes(method, url string, data []byte, etag string) (*http.Response, *AppError) {
return c.doApiRequestReader(method, url, bytes.NewReader(data), etag)
}
func (c *Client4) doApiRequestReader(method, url string, data io.Reader, etag string) (*http.Response, *AppError) {
rq, err := http.NewRequest(method, url, data)
if err != nil {
return nil, NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)
}
if len(etag) > 0 {
rq.Header.Set(HEADER_ETAG_CLIENT, etag)
}
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
}
if c.HttpHeader != nil && len(c.HttpHeader) > 0 {
for k, v := range c.HttpHeader {
rq.Header.Set(k, v)
}
}
rp, err := c.HttpClient.Do(rq)
if err != nil || rp == nil {
return nil, NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), 0)
}
if rp.StatusCode == 304 {
return rp, nil
}
if rp.StatusCode >= 300 {
defer closeBody(rp)
return rp, AppErrorFromJson(rp.Body)
}
return rp, nil
}
func (c *Client4) DoUploadFile(url string, data []byte, contentType string) (*FileUploadResponse, *Response) {
MM-7633: Optimize memory utilization during file uploads (#9835) * MM-7633: Optimize memory utilization during file uploads Refactored the file upload code to reduce redundant buffering and stream directly to the file store. Added tests. Benchmark results: ``` levs-mbp:mattermost-server levb$ go test -v -run nothing -bench Upload -benchmem ./app ... BenchmarkUploadFile/random-5Mb-gif-raw-ish_DoUploadFile-4 10 122598031 ns/op 21211370 B/op 1008 allocs/op BenchmarkUploadFile/random-5Mb-gif-raw_UploadFileTask-4 100 20211926 ns/op 5678750 B/op 126 allocs/op BenchmarkUploadFile/random-5Mb-gif-UploadFiles-4 2 1037051184 ns/op 81806360 B/op 3705013 allocs/op BenchmarkUploadFile/random-5Mb-gif-UploadFileTask-4 2 933644431 ns/op 67015868 B/op 3704410 allocs/op BenchmarkUploadFile/random-2Mb-jpg-raw-ish_DoUploadFile-4 100 13110509 ns/op 6032614 B/op 8052 allocs/op BenchmarkUploadFile/random-2Mb-jpg-raw_UploadFileTask-4 100 10729867 ns/op 1738303 B/op 125 allocs/op BenchmarkUploadFile/random-2Mb-jpg-UploadFiles-4 2 925274912 ns/op 70326352 B/op 3718856 allocs/op BenchmarkUploadFile/random-2Mb-jpg-UploadFileTask-4 2 995033336 ns/op 58113796 B/op 3710943 allocs/op BenchmarkUploadFile/zero-10Mb-raw-ish_DoUploadFile-4 30 50777211 ns/op 54791929 B/op 2714 allocs/op BenchmarkUploadFile/zero-10Mb-raw_UploadFileTask-4 50 36387339 ns/op 10503920 B/op 126 allocs/op BenchmarkUploadFile/zero-10Mb-UploadFiles-4 30 48657678 ns/op 54791948 B/op 2719 allocs/op BenchmarkUploadFile/zero-10Mb-UploadFileTask-4 50 37506467 ns/op 31492060 B/op 131 allocs/op ... ``` https://mattermost.atlassian.net/browse/MM-7633 https://github.com/mattermost/mattermost-server/issues/7801 [Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fields.] - [x] Added or updated unit tests (required for all new features) - [ ] Added API documentation (required for all new APIs) - [ ] All new/modified APIs include changes to the drivers *N/A*??? - [x] Includes text changes and localization file ([.../i18n/en.json](https://github.com/mattermost/mattermost-server/blob/master/i18n/en.json)) updates Overview of changes: - api4 - Replaced `uploadFile` handler with `uploadFileStream` that reduces unnecessary buffering. - Added/refactored tests for the new API. - Refactored apitestlib/Check...Status functions. - app - Added App.UploadFileTask, a more efficient refactor of UploadFile. - Consistently set `FileInfo.HasPreviewImage` - Added benchmarks for the new and prior implementations - Replaced passing around `*image.Image` with `image.Image` in the existing code. - model - Added a more capable `client4.UploadFiles` API to match the new server API’s capabilities. - I18n - Replaced `api.file.upload_file.bad_parse.app_error` with a more generic `api.file.upload_file.read_request.app_error` - plugin - Removed type `plugin.multiPluginHookRunnerFunc` in favor of using `func(hooks Hooks) bool` explicitly, to help with testing - tests - Added test files for testing images Still remaining, but can be separate PRs - please let me know the preferred course of action - Investigate JS client API - how does it do multipart? - Performance loss from old code on (small) image processing? - Deprecate the old functions, change other API implementations to use UploadFileTask Definitely separate future PRs - should I file tickets foe these? - Only invoke t.readAll() if there are indeed applicable plugins to run - Find a way to leverage goexif buffer rather than re-reading Suggested long-term improvements - should I file separate tickets for these? - Actually allow uploading of large (GB-sized) files. This may require a change in how the file is passed to plugins. - (Many) api4 tests should probably be subtests and share a server setup - will be much faster - Performance improvements in image processing (goexif/thumbnail/preview) (maybe use https://mattermost.atlassian.net/browse/MM-10188 for this) Questions: 1. I am commiting MBs of test images, are there better alternatives? I can probably create much less dense images that would take up considerably less space, even at pretty large sizes 2. I18n: Do I need to do anything special for the string change? Or just wait until it gets picked up and translated/updated? 3. The image dimensions are flipped in resulting FileInfo to match the actual orientation. Is this by design? Should add a test for it, perhaps? 4. What to do in the case of partial success? S3 but not DB, some files but not others? For now, just doing what the old code did, I think. 5. Make maxUploadDrainBytes configurable? Also, should this be the systemic behavior of all APIs with non-empty body? Otherwise dropped altogether? Check all other ioutil.ReadAll() from sockets. Find a way to set a total byte limit on request Body? * WIP - Fixed for GetPluginsEnvironment() changes * WIP - PR feedback 1. Refactored checkHTTPStatus to improve failure messages 2. Use `var name []type` rather than `name := ([]type)(nil)` 3. Replaced single-letter `p` with a more intention-revealing `part` 4. Added tests for full image size, `HasPreviewImage` * WIP - rebased (c.Session->c.App.Session) * WIP - PR feedback: eliminated use of Request.MultipartReader Instead of hacking the request object to use r.MultipartReader now have own functions `parseMultipartRequestHeader` and `multipartReader` eliminating the need to hack the request object to use Request.MultipartReader limitations. * WIP - PR feedback: UploadFileX with functional options * WIP - PR feedback: style * WIP - PR feedback: errors cleanup * WIP - clarified File Upload benchmarks * WIP - PR feedback: display the value of erroneous formname * WIP - PR feedback: fixed handling of multiple channel_ids * WIP - rebased from master - fixed tests * PR Feedback * PR feedback - moved client4.UploadFiles to _test for now
2018-12-13 13:32:07 -08:00
return c.doUploadFile(url, bytes.NewReader(data), contentType, 0)
}
func (c *Client4) doUploadFile(url string, body io.Reader, contentType string, contentLength int64) (*FileUploadResponse, *Response) {
rq, err := http.NewRequest("POST", c.ApiUrl+url, body)
if err != nil {
return nil, &Response{Error: NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)}
}
MM-7633: Optimize memory utilization during file uploads (#9835) * MM-7633: Optimize memory utilization during file uploads Refactored the file upload code to reduce redundant buffering and stream directly to the file store. Added tests. Benchmark results: ``` levs-mbp:mattermost-server levb$ go test -v -run nothing -bench Upload -benchmem ./app ... BenchmarkUploadFile/random-5Mb-gif-raw-ish_DoUploadFile-4 10 122598031 ns/op 21211370 B/op 1008 allocs/op BenchmarkUploadFile/random-5Mb-gif-raw_UploadFileTask-4 100 20211926 ns/op 5678750 B/op 126 allocs/op BenchmarkUploadFile/random-5Mb-gif-UploadFiles-4 2 1037051184 ns/op 81806360 B/op 3705013 allocs/op BenchmarkUploadFile/random-5Mb-gif-UploadFileTask-4 2 933644431 ns/op 67015868 B/op 3704410 allocs/op BenchmarkUploadFile/random-2Mb-jpg-raw-ish_DoUploadFile-4 100 13110509 ns/op 6032614 B/op 8052 allocs/op BenchmarkUploadFile/random-2Mb-jpg-raw_UploadFileTask-4 100 10729867 ns/op 1738303 B/op 125 allocs/op BenchmarkUploadFile/random-2Mb-jpg-UploadFiles-4 2 925274912 ns/op 70326352 B/op 3718856 allocs/op BenchmarkUploadFile/random-2Mb-jpg-UploadFileTask-4 2 995033336 ns/op 58113796 B/op 3710943 allocs/op BenchmarkUploadFile/zero-10Mb-raw-ish_DoUploadFile-4 30 50777211 ns/op 54791929 B/op 2714 allocs/op BenchmarkUploadFile/zero-10Mb-raw_UploadFileTask-4 50 36387339 ns/op 10503920 B/op 126 allocs/op BenchmarkUploadFile/zero-10Mb-UploadFiles-4 30 48657678 ns/op 54791948 B/op 2719 allocs/op BenchmarkUploadFile/zero-10Mb-UploadFileTask-4 50 37506467 ns/op 31492060 B/op 131 allocs/op ... ``` https://mattermost.atlassian.net/browse/MM-7633 https://github.com/mattermost/mattermost-server/issues/7801 [Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fields.] - [x] Added or updated unit tests (required for all new features) - [ ] Added API documentation (required for all new APIs) - [ ] All new/modified APIs include changes to the drivers *N/A*??? - [x] Includes text changes and localization file ([.../i18n/en.json](https://github.com/mattermost/mattermost-server/blob/master/i18n/en.json)) updates Overview of changes: - api4 - Replaced `uploadFile` handler with `uploadFileStream` that reduces unnecessary buffering. - Added/refactored tests for the new API. - Refactored apitestlib/Check...Status functions. - app - Added App.UploadFileTask, a more efficient refactor of UploadFile. - Consistently set `FileInfo.HasPreviewImage` - Added benchmarks for the new and prior implementations - Replaced passing around `*image.Image` with `image.Image` in the existing code. - model - Added a more capable `client4.UploadFiles` API to match the new server API’s capabilities. - I18n - Replaced `api.file.upload_file.bad_parse.app_error` with a more generic `api.file.upload_file.read_request.app_error` - plugin - Removed type `plugin.multiPluginHookRunnerFunc` in favor of using `func(hooks Hooks) bool` explicitly, to help with testing - tests - Added test files for testing images Still remaining, but can be separate PRs - please let me know the preferred course of action - Investigate JS client API - how does it do multipart? - Performance loss from old code on (small) image processing? - Deprecate the old functions, change other API implementations to use UploadFileTask Definitely separate future PRs - should I file tickets foe these? - Only invoke t.readAll() if there are indeed applicable plugins to run - Find a way to leverage goexif buffer rather than re-reading Suggested long-term improvements - should I file separate tickets for these? - Actually allow uploading of large (GB-sized) files. This may require a change in how the file is passed to plugins. - (Many) api4 tests should probably be subtests and share a server setup - will be much faster - Performance improvements in image processing (goexif/thumbnail/preview) (maybe use https://mattermost.atlassian.net/browse/MM-10188 for this) Questions: 1. I am commiting MBs of test images, are there better alternatives? I can probably create much less dense images that would take up considerably less space, even at pretty large sizes 2. I18n: Do I need to do anything special for the string change? Or just wait until it gets picked up and translated/updated? 3. The image dimensions are flipped in resulting FileInfo to match the actual orientation. Is this by design? Should add a test for it, perhaps? 4. What to do in the case of partial success? S3 but not DB, some files but not others? For now, just doing what the old code did, I think. 5. Make maxUploadDrainBytes configurable? Also, should this be the systemic behavior of all APIs with non-empty body? Otherwise dropped altogether? Check all other ioutil.ReadAll() from sockets. Find a way to set a total byte limit on request Body? * WIP - Fixed for GetPluginsEnvironment() changes * WIP - PR feedback 1. Refactored checkHTTPStatus to improve failure messages 2. Use `var name []type` rather than `name := ([]type)(nil)` 3. Replaced single-letter `p` with a more intention-revealing `part` 4. Added tests for full image size, `HasPreviewImage` * WIP - rebased (c.Session->c.App.Session) * WIP - PR feedback: eliminated use of Request.MultipartReader Instead of hacking the request object to use r.MultipartReader now have own functions `parseMultipartRequestHeader` and `multipartReader` eliminating the need to hack the request object to use Request.MultipartReader limitations. * WIP - PR feedback: UploadFileX with functional options * WIP - PR feedback: style * WIP - PR feedback: errors cleanup * WIP - clarified File Upload benchmarks * WIP - PR feedback: display the value of erroneous formname * WIP - PR feedback: fixed handling of multiple channel_ids * WIP - rebased from master - fixed tests * PR Feedback * PR feedback - moved client4.UploadFiles to _test for now
2018-12-13 13:32:07 -08:00
if contentLength != 0 {
rq.ContentLength = contentLength
}
rq.Header.Set("Content-Type", contentType)
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
}
rp, err := c.HttpClient.Do(rq)
if err != nil || rp == nil {
return nil, BuildErrorResponse(rp, NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), 0))
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return nil, BuildErrorResponse(rp, AppErrorFromJson(rp.Body))
}
return FileUploadResponseFromJson(rp.Body), BuildResponse(rp)
}
func (c *Client4) DoEmojiUploadFile(url string, data []byte, contentType string) (*Emoji, *Response) {
rq, err := http.NewRequest("POST", c.ApiUrl+url, bytes.NewReader(data))
if err != nil {
return nil, &Response{Error: NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)}
}
rq.Header.Set("Content-Type", contentType)
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
}
rp, err := c.HttpClient.Do(rq)
if err != nil || rp == nil {
return nil, BuildErrorResponse(rp, NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), 0))
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return nil, BuildErrorResponse(rp, AppErrorFromJson(rp.Body))
}
return EmojiFromJson(rp.Body), BuildResponse(rp)
}
func (c *Client4) DoUploadImportTeam(url string, data []byte, contentType string) (map[string]string, *Response) {
rq, err := http.NewRequest("POST", c.ApiUrl+url, bytes.NewReader(data))
if err != nil {
return nil, &Response{Error: NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)}
}
rq.Header.Set("Content-Type", contentType)
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
}
rp, err := c.HttpClient.Do(rq)
if err != nil || rp == nil {
return nil, BuildErrorResponse(rp, NewAppError(url, "model.client.connecting.app_error", nil, err.Error(), 0))
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return nil, BuildErrorResponse(rp, AppErrorFromJson(rp.Body))
}
return MapFromJson(rp.Body), BuildResponse(rp)
}
// CheckStatusOK is a convenience function for checking the standard OK response
// from the web service.
func CheckStatusOK(r *http.Response) bool {
m := MapFromJson(r.Body)
defer closeBody(r)
if m != nil && m[STATUS] == STATUS_OK {
return true
}
return false
}
// Authentication Section
// LoginById authenticates a user by user id and password.
func (c *Client4) LoginById(id string, password string) (*User, *Response) {
m := make(map[string]string)
m["id"] = id
m["password"] = password
return c.login(m)
}
// Login authenticates a user by login id, which can be username, email or some sort
// of SSO identifier based on server configuration, and a password.
func (c *Client4) Login(loginId string, password string) (*User, *Response) {
m := make(map[string]string)
m["login_id"] = loginId
m["password"] = password
return c.login(m)
}
// LoginByLdap authenticates a user by LDAP id and password.
func (c *Client4) LoginByLdap(loginId string, password string) (*User, *Response) {
m := make(map[string]string)
m["login_id"] = loginId
m["password"] = password
m["ldap_only"] = "true"
return c.login(m)
}
// LoginWithDevice authenticates a user by login id (username, email or some sort
// of SSO identifier based on configuration), password and attaches a device id to
// the session.
func (c *Client4) LoginWithDevice(loginId string, password string, deviceId string) (*User, *Response) {
m := make(map[string]string)
m["login_id"] = loginId
m["password"] = password
m["device_id"] = deviceId
return c.login(m)
}
// LoginWithMFA logs a user in with a MFA token
func (c *Client4) LoginWithMFA(loginId, password, mfaToken string) (*User, *Response) {
m := make(map[string]string)
m["login_id"] = loginId
m["password"] = password
m["token"] = mfaToken
return c.login(m)
}
func (c *Client4) login(m map[string]string) (*User, *Response) {
r, err := c.DoApiPost("/users/login", MapToJson(m))
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
c.AuthToken = r.Header.Get(HEADER_TOKEN)
c.AuthType = HEADER_BEARER
return UserFromJson(r.Body), BuildResponse(r)
}
// Logout terminates the current user's session.
func (c *Client4) Logout() (bool, *Response) {
r, err := c.DoApiPost("/users/logout", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
c.AuthToken = ""
c.AuthType = HEADER_BEARER
return CheckStatusOK(r), BuildResponse(r)
}
// SwitchAccountType changes a user's login type from one type to another.
func (c *Client4) SwitchAccountType(switchRequest *SwitchRequest) (string, *Response) {
r, err := c.DoApiPost(c.GetUsersRoute()+"/login/switch", switchRequest.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return "", BuildErrorResponse(r, err)
}
defer closeBody(r)
return MapFromJson(r.Body)["follow_link"], BuildResponse(r)
}
// User Section
// CreateUser creates a user in the system based on the provided user struct.
func (c *Client4) CreateUser(user *User) (*User, *Response) {
r, err := c.DoApiPost(c.GetUsersRoute(), user.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserFromJson(r.Body), BuildResponse(r)
}
// CreateUserWithToken creates a user in the system based on the provided tokenId.
func (c *Client4) CreateUserWithToken(user *User, tokenId string) (*User, *Response) {
if tokenId == "" {
err := NewAppError("MissingHashOrData", "api.user.create_user.missing_token.app_error", nil, "", http.StatusBadRequest)
return nil, &Response{StatusCode: err.StatusCode, Error: err}
}
query := fmt.Sprintf("?t=%v", tokenId)
r, err := c.DoApiPost(c.GetUsersRoute()+query, user.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserFromJson(r.Body), BuildResponse(r)
}
// CreateUserWithInviteId creates a user in the system based on the provided invited id.
func (c *Client4) CreateUserWithInviteId(user *User, inviteId string) (*User, *Response) {
if inviteId == "" {
err := NewAppError("MissingInviteId", "api.user.create_user.missing_invite_id.app_error", nil, "", http.StatusBadRequest)
return nil, &Response{StatusCode: err.StatusCode, Error: err}
}
query := fmt.Sprintf("?iid=%v", url.QueryEscape(inviteId))
r, err := c.DoApiPost(c.GetUsersRoute()+query, user.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserFromJson(r.Body), BuildResponse(r)
}
// GetMe returns the logged in user.
func (c *Client4) GetMe(etag string) (*User, *Response) {
r, err := c.DoApiGet(c.GetUserRoute(ME), etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserFromJson(r.Body), BuildResponse(r)
}
// GetUser returns a user based on the provided user id string.
func (c *Client4) GetUser(userId, etag string) (*User, *Response) {
r, err := c.DoApiGet(c.GetUserRoute(userId), etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserFromJson(r.Body), BuildResponse(r)
}
// GetUserByUsername returns a user based on the provided user name string.
func (c *Client4) GetUserByUsername(userName, etag string) (*User, *Response) {
r, err := c.DoApiGet(c.GetUserByUsernameRoute(userName), etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserFromJson(r.Body), BuildResponse(r)
}
// GetUserByEmail returns a user based on the provided user email string.
func (c *Client4) GetUserByEmail(email, etag string) (*User, *Response) {
r, err := c.DoApiGet(c.GetUserByEmailRoute(email), etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserFromJson(r.Body), BuildResponse(r)
}
// AutocompleteUsersInTeam returns the users on a team based on search term.
func (c *Client4) AutocompleteUsersInTeam(teamId string, username string, limit int, etag string) (*UserAutocomplete, *Response) {
query := fmt.Sprintf("?in_team=%v&name=%v&limit=%d", teamId, username, limit)
r, err := c.DoApiGet(c.GetUsersRoute()+"/autocomplete"+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserAutocompleteFromJson(r.Body), BuildResponse(r)
}
// AutocompleteUsersInChannel returns the users in a channel based on search term.
func (c *Client4) AutocompleteUsersInChannel(teamId string, channelId string, username string, limit int, etag string) (*UserAutocomplete, *Response) {
query := fmt.Sprintf("?in_team=%v&in_channel=%v&name=%v&limit=%d", teamId, channelId, username, limit)
r, err := c.DoApiGet(c.GetUsersRoute()+"/autocomplete"+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserAutocompleteFromJson(r.Body), BuildResponse(r)
}
// AutocompleteUsers returns the users in the system based on search term.
func (c *Client4) AutocompleteUsers(username string, limit int, etag string) (*UserAutocomplete, *Response) {
query := fmt.Sprintf("?name=%v&limit=%d", username, limit)
r, err := c.DoApiGet(c.GetUsersRoute()+"/autocomplete"+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserAutocompleteFromJson(r.Body), BuildResponse(r)
}
// GetDefaultProfileImage gets the default user's profile image. Must be logged in.
func (c *Client4) GetDefaultProfileImage(userId string) ([]byte, *Response) {
r, appErr := c.DoApiGet(c.GetUserRoute(userId)+"/image/default", "")
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, BuildErrorResponse(r, NewAppError("GetDefaultProfileImage", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode))
}
return data, BuildResponse(r)
}
// GetProfileImage gets user's profile image. Must be logged in.
func (c *Client4) GetProfileImage(userId, etag string) ([]byte, *Response) {
r, appErr := c.DoApiGet(c.GetUserRoute(userId)+"/image", etag)
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, BuildErrorResponse(r, NewAppError("GetProfileImage", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode))
}
return data, BuildResponse(r)
}
// GetUsers returns a page of users on the system. Page counting starts at 0.
func (c *Client4) GetUsers(page int, perPage int, etag string) ([]*User, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetUsersRoute()+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserListFromJson(r.Body), BuildResponse(r)
}
// GetUsersInTeam returns a page of users on a team. Page counting starts at 0.
func (c *Client4) GetUsersInTeam(teamId string, page int, perPage int, etag string) ([]*User, *Response) {
query := fmt.Sprintf("?in_team=%v&page=%v&per_page=%v", teamId, page, perPage)
r, err := c.DoApiGet(c.GetUsersRoute()+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserListFromJson(r.Body), BuildResponse(r)
}
// GetNewUsersInTeam returns a page of users on a team. Page counting starts at 0.
func (c *Client4) GetNewUsersInTeam(teamId string, page int, perPage int, etag string) ([]*User, *Response) {
query := fmt.Sprintf("?sort=create_at&in_team=%v&page=%v&per_page=%v", teamId, page, perPage)
r, err := c.DoApiGet(c.GetUsersRoute()+query, etag)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserListFromJson(r.Body), BuildResponse(r)
}
// GetRecentlyActiveUsersInTeam returns a page of users on a team. Page counting starts at 0.
func (c *Client4) GetRecentlyActiveUsersInTeam(teamId string, page int, perPage int, etag string) ([]*User, *Response) {
query := fmt.Sprintf("?sort=last_activity_at&in_team=%v&page=%v&per_page=%v", teamId, page, perPage)
r, err := c.DoApiGet(c.GetUsersRoute()+query, etag)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserListFromJson(r.Body), BuildResponse(r)
}
// GetUsersNotInTeam returns a page of users who are not in a team. Page counting starts at 0.
func (c *Client4) GetUsersNotInTeam(teamId string, page int, perPage int, etag string) ([]*User, *Response) {
query := fmt.Sprintf("?not_in_team=%v&page=%v&per_page=%v", teamId, page, perPage)
r, err := c.DoApiGet(c.GetUsersRoute()+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserListFromJson(r.Body), BuildResponse(r)
}
// GetUsersInChannel returns a page of users in a channel. Page counting starts at 0.
func (c *Client4) GetUsersInChannel(channelId string, page int, perPage int, etag string) ([]*User, *Response) {
query := fmt.Sprintf("?in_channel=%v&page=%v&per_page=%v", channelId, page, perPage)
r, err := c.DoApiGet(c.GetUsersRoute()+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserListFromJson(r.Body), BuildResponse(r)
}
GH-9636 plugins api GetUsersInChannelByStatus (#9645) * adds GetUsersInChannelByStatus to plugin api with generated rpc code. * fixed typo in comment with actual func name * replaced Response model with AppError in output of GetUsersInChannelByStatus * removed etag param from GetUsersInChannelByStatus since it is not used * plugin api for GetUsersInChannelByStatus updated to take the limit, conforming to the app api. * fixed an issue in my own logic on app/plugin integration. * adds GetUsersInChannelByStatus to plugin api with generated rpc code. * fixed typo in comment with actual func name * replaced Response model with AppError in output of GetUsersInChannelByStatus * removed etag param from GetUsersInChannelByStatus since it is not used * plugin api for GetUsersInChannelByStatus updated to take the limit, conforming to the app api. * fixed an issue in my own logic on app/plugin integration. * GetUsersInChannelByStatus changed to more generic GetUsersInChannel which takes a sortBy parameter, allowing for more granular/extensible sorting functionality in the future * GetUsersInChannel accepts sort parameter of 'username' and 'status'. Both values are consts in model pkg. * Documents minimum server version for GetUsersInChannel. * adds GetUsersInChannelByStatus to plugin api with generated rpc code. * fixed typo in comment with actual func name * replaced Response model with AppError in output of GetUsersInChannelByStatus * removed etag param from GetUsersInChannelByStatus since it is not used * plugin api for GetUsersInChannelByStatus updated to take the limit, conforming to the app api. * fixed an issue in my own logic on app/plugin integration. * adds GetUsersInChannelByStatus to plugin api with generated rpc code. * Resolved conflict on rebase * replaced Response model with AppError in output of GetUsersInChannelByStatus * removed etag param from GetUsersInChannelByStatus since it is not used * plugin api for GetUsersInChannelByStatus updated to take the limit, conforming to the app api. * fixed an issue in my own logic on app/plugin integration. * GetUsersInChannelByStatus changed to more generic GetUsersInChannel which takes a sortBy parameter, allowing for more granular/extensible sorting functionality in the future * GetUsersInChannel accepts sort parameter of 'username' and 'status'. Both values are consts in model pkg. * Documents minimum server version for GetUsersInChannel. * replaces GetUsersInChannel from #9608 / #9643 with sortBy functionality
2018-10-22 08:49:50 -04:00
// GetUsersInChannelByStatus returns a page of users in a channel. Page counting starts at 0. Sorted by Status
func (c *Client4) GetUsersInChannelByStatus(channelId string, page int, perPage int, etag string) ([]*User, *Response) {
query := fmt.Sprintf("?in_channel=%v&page=%v&per_page=%v&sort=status", channelId, page, perPage)
r, err := c.DoApiGet(c.GetUsersRoute()+query, etag)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserListFromJson(r.Body), BuildResponse(r)
}
// GetUsersNotInChannel returns a page of users not in a channel. Page counting starts at 0.
func (c *Client4) GetUsersNotInChannel(teamId, channelId string, page int, perPage int, etag string) ([]*User, *Response) {
query := fmt.Sprintf("?in_team=%v&not_in_channel=%v&page=%v&per_page=%v", teamId, channelId, page, perPage)
r, err := c.DoApiGet(c.GetUsersRoute()+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserListFromJson(r.Body), BuildResponse(r)
}
// GetUsersWithoutTeam returns a page of users on the system that aren't on any teams. Page counting starts at 0.
func (c *Client4) GetUsersWithoutTeam(page int, perPage int, etag string) ([]*User, *Response) {
query := fmt.Sprintf("?without_team=1&page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetUsersRoute()+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserListFromJson(r.Body), BuildResponse(r)
}
// GetUsersByIds returns a list of users based on the provided user ids.
func (c *Client4) GetUsersByIds(userIds []string) ([]*User, *Response) {
r, err := c.DoApiPost(c.GetUsersRoute()+"/ids", ArrayToJson(userIds))
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserListFromJson(r.Body), BuildResponse(r)
}
// GetUsersByIds returns a list of users based on the provided user ids.
func (c *Client4) GetUsersByIdsWithOptions(userIds []string, options *UserGetByIdsOptions) ([]*User, *Response) {
v := url.Values{}
if options.Since != 0 {
v.Set("since", fmt.Sprintf("%d", options.Since))
}
url := c.GetUsersRoute() + "/ids"
if len(v) > 0 {
url += "?" + v.Encode()
}
r, err := c.DoApiPost(url, ArrayToJson(userIds))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserListFromJson(r.Body), BuildResponse(r)
}
// GetUsersByUsernames returns a list of users based on the provided usernames.
func (c *Client4) GetUsersByUsernames(usernames []string) ([]*User, *Response) {
r, err := c.DoApiPost(c.GetUsersRoute()+"/usernames", ArrayToJson(usernames))
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserListFromJson(r.Body), BuildResponse(r)
}
// GetUsersByGroupChannelIds returns a map with channel ids as keys
// and a list of users as values based on the provided user ids.
func (c *Client4) GetUsersByGroupChannelIds(groupChannelIds []string) (map[string][]*User, *Response) {
r, err := c.DoApiPost(c.GetUsersRoute()+"/group_channels", ArrayToJson(groupChannelIds))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
usersByChannelId := map[string][]*User{}
json.NewDecoder(r.Body).Decode(&usersByChannelId)
return usersByChannelId, BuildResponse(r)
}
// SearchUsers returns a list of users based on some search criteria.
func (c *Client4) SearchUsers(search *UserSearch) ([]*User, *Response) {
r, err := c.doApiPostBytes(c.GetUsersRoute()+"/search", search.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserListFromJson(r.Body), BuildResponse(r)
}
// UpdateUser updates a user in the system based on the provided user struct.
func (c *Client4) UpdateUser(user *User) (*User, *Response) {
r, err := c.DoApiPut(c.GetUserRoute(user.Id), user.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserFromJson(r.Body), BuildResponse(r)
}
// PatchUser partially updates a user in the system. Any missing fields are not updated.
func (c *Client4) PatchUser(userId string, patch *UserPatch) (*User, *Response) {
r, err := c.DoApiPut(c.GetUserRoute(userId)+"/patch", patch.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserFromJson(r.Body), BuildResponse(r)
}
// UpdateUserAuth updates a user AuthData (uthData, authService and password) in the system.
func (c *Client4) UpdateUserAuth(userId string, userAuth *UserAuth) (*UserAuth, *Response) {
r, err := c.DoApiPut(c.GetUserRoute(userId)+"/auth", userAuth.ToJson())
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserAuthFromJson(r.Body), BuildResponse(r)
}
// UpdateUserMfa activates multi-factor authentication for a user if activate
// is true and a valid code is provided. If activate is false, then code is not
// required and multi-factor authentication is disabled for the user.
func (c *Client4) UpdateUserMfa(userId, code string, activate bool) (bool, *Response) {
requestBody := make(map[string]interface{})
requestBody["activate"] = activate
requestBody["code"] = code
r, err := c.DoApiPut(c.GetUserRoute(userId)+"/mfa", StringInterfaceToJson(requestBody))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// CheckUserMfa checks whether a user has MFA active on their account or not based on the
// provided login id.
// Deprecated: Clients should use Login method and check for MFA Error
func (c *Client4) CheckUserMfa(loginId string) (bool, *Response) {
requestBody := make(map[string]interface{})
requestBody["login_id"] = loginId
r, err := c.DoApiPost(c.GetUsersRoute()+"/mfa", StringInterfaceToJson(requestBody))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
data := StringInterfaceFromJson(r.Body)
mfaRequired, ok := data["mfa_required"].(bool)
if !ok {
return false, BuildResponse(r)
}
return mfaRequired, BuildResponse(r)
}
// GenerateMfaSecret will generate a new MFA secret for a user and return it as a string and
// as a base64 encoded image QR code.
func (c *Client4) GenerateMfaSecret(userId string) (*MfaSecret, *Response) {
r, err := c.DoApiPost(c.GetUserRoute(userId)+"/mfa/generate", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return MfaSecretFromJson(r.Body), BuildResponse(r)
}
// UpdateUserPassword updates a user's password. Must be logged in as the user or be a system administrator.
func (c *Client4) UpdateUserPassword(userId, currentPassword, newPassword string) (bool, *Response) {
requestBody := map[string]string{"current_password": currentPassword, "new_password": newPassword}
r, err := c.DoApiPut(c.GetUserRoute(userId)+"/password", MapToJson(requestBody))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
Guest accounts feature (#11428) * MM-14139: Creating permissions for invite/promote/demote guests (#10778) * MM-14139: Creating permissions for invite/promote/demote guests * Fixing tests * Adding invite guest api endpoint (#10792) * Adding invite guest api endpoint * Adding i18n * Adding some tests * WIP * Migrating Token.Extra info to bigger size (2048) * Fixing tests * Adding client function for invite guests * Adding send guests invites tests * Renaming file from guest to guest_invite * Adding Promote/Demote users from/to guest endpoints (#10791) * Adding Promote/Demote users from/to guest endpoints * Adding i18n translations * Adding the client functions * Using getQueryBuilder function * Addressing PR review comments * Adding default channels to users on promte from guest (#10851) * Adding default channels to users on promte from guest * Addressing PR review comments * Fixing merge problems * Sending websockets events on promote/demote (#11403) * Sending websockets events on promote/demote * Fixing merge problems * Fixing govet shadowing problem * Fixing feature branch tests * Avoiding leaking users data through websockets for guest accounts (#11489) * Avoiding leaking users data through websockets for guest accounts * Adding tests and fixing code error * Fixing i18n * Allow to enable/disable guests and other extra config settings (#11481) * Allow to enable/disable guests and other extra config settings * Fixing tests and moving license and config validation to api level * Update api4/role_test.go Co-Authored-By: George Goldberg <george@gberg.me> * Update api4/role_test.go Co-Authored-By: George Goldberg <george@gberg.me> * Fixing typo * fixing tests * Managing correctly the guest channel leave behavior (#11578) * MM-15134: Removing guests from teams or system on leave channels if needed * WIP * No deactivating the guest user when leave the last team * Adding a couple of tests * Fixing shadow variables * Fixing tests * fixing tests * fixing shadow variables * Adding guest counts for channel stats (#11646) * Adding guest counts for channel stats * Adding tests * Fixing tests * Fixing guest domain restrictions (#11660) * Adding needed migration for the database * Fixing migration
2019-07-22 22:13:39 +02:00
// PromoteGuestToUser convert a guest into a regular user
func (c *Client4) PromoteGuestToUser(guestId string) (bool, *Response) {
r, err := c.DoApiPost(c.GetUserRoute(guestId)+"/promote", "")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// DemoteUserToGuest convert a regular user into a guest
func (c *Client4) DemoteUserToGuest(guestId string) (bool, *Response) {
r, err := c.DoApiPost(c.GetUserRoute(guestId)+"/demote", "")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// UpdateUserRoles updates a user's roles in the system. A user can have "system_user" and "system_admin" roles.
func (c *Client4) UpdateUserRoles(userId, roles string) (bool, *Response) {
requestBody := map[string]string{"roles": roles}
r, err := c.DoApiPut(c.GetUserRoute(userId)+"/roles", MapToJson(requestBody))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// UpdateUserActive updates status of a user whether active or not.
func (c *Client4) UpdateUserActive(userId string, active bool) (bool, *Response) {
requestBody := make(map[string]interface{})
requestBody["active"] = active
r, err := c.DoApiPut(c.GetUserRoute(userId)+"/active", StringInterfaceToJson(requestBody))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// DeleteUser deactivates a user in the system based on the provided user id string.
func (c *Client4) DeleteUser(userId string) (bool, *Response) {
r, err := c.DoApiDelete(c.GetUserRoute(userId))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// SendPasswordResetEmail will send a link for password resetting to a user with the
// provided email.
func (c *Client4) SendPasswordResetEmail(email string) (bool, *Response) {
requestBody := map[string]string{"email": email}
r, err := c.DoApiPost(c.GetUsersRoute()+"/password/reset/send", MapToJson(requestBody))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// ResetPassword uses a recovery code to update reset a user's password.
func (c *Client4) ResetPassword(token, newPassword string) (bool, *Response) {
requestBody := map[string]string{"token": token, "new_password": newPassword}
r, err := c.DoApiPost(c.GetUsersRoute()+"/password/reset", MapToJson(requestBody))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// GetSessions returns a list of sessions based on the provided user id string.
func (c *Client4) GetSessions(userId, etag string) ([]*Session, *Response) {
r, err := c.DoApiGet(c.GetUserRoute(userId)+"/sessions", etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return SessionsFromJson(r.Body), BuildResponse(r)
}
// RevokeSession revokes a user session based on the provided user id and session id strings.
func (c *Client4) RevokeSession(userId, sessionId string) (bool, *Response) {
requestBody := map[string]string{"session_id": sessionId}
r, err := c.DoApiPost(c.GetUserRoute(userId)+"/sessions/revoke", MapToJson(requestBody))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// RevokeAllSessions revokes all sessions for the provided user id string.
func (c *Client4) RevokeAllSessions(userId string) (bool, *Response) {
r, err := c.DoApiPost(c.GetUserRoute(userId)+"/sessions/revoke/all", "")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// RevokeAllSessions revokes all sessions for all the users.
func (c *Client4) RevokeSessionsFromAllUsers() (bool, *Response) {
r, err := c.DoApiPost(c.GetUsersRoute()+"/sessions/revoke/all", "")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// AttachDeviceId attaches a mobile device ID to the current session.
func (c *Client4) AttachDeviceId(deviceId string) (bool, *Response) {
requestBody := map[string]string{"device_id": deviceId}
r, err := c.DoApiPut(c.GetUsersRoute()+"/sessions/device", MapToJson(requestBody))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// GetTeamsUnreadForUser will return an array with TeamUnread objects that contain the amount
// of unread messages and mentions the current user has for the teams it belongs to.
// An optional team ID can be set to exclude that team from the results. Must be authenticated.
func (c *Client4) GetTeamsUnreadForUser(userId, teamIdToExclude string) ([]*TeamUnread, *Response) {
var optional string
if teamIdToExclude != "" {
optional += fmt.Sprintf("?exclude_team=%s", url.QueryEscape(teamIdToExclude))
}
r, err := c.DoApiGet(c.GetUserRoute(userId)+"/teams/unread"+optional, "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TeamsUnreadFromJson(r.Body), BuildResponse(r)
}
// GetUserAudits returns a list of audit based on the provided user id string.
func (c *Client4) GetUserAudits(userId string, page int, perPage int, etag string) (Audits, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetUserRoute(userId)+"/audits"+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return AuditsFromJson(r.Body), BuildResponse(r)
}
// VerifyUserEmail will verify a user's email using the supplied token.
func (c *Client4) VerifyUserEmail(token string) (bool, *Response) {
requestBody := map[string]string{"token": token}
r, err := c.DoApiPost(c.GetUsersRoute()+"/email/verify", MapToJson(requestBody))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// SendVerificationEmail will send an email to the user with the provided email address, if
// that user exists. The email will contain a link that can be used to verify the user's
// email address.
func (c *Client4) SendVerificationEmail(email string) (bool, *Response) {
requestBody := map[string]string{"email": email}
r, err := c.DoApiPost(c.GetUsersRoute()+"/email/verify/send", MapToJson(requestBody))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// SetDefaultProfileImage resets the profile image to a default generated one.
func (c *Client4) SetDefaultProfileImage(userId string) (bool, *Response) {
r, err := c.DoApiDelete(c.GetUserRoute(userId) + "/image")
if err != nil {
return false, BuildErrorResponse(r, err)
}
return CheckStatusOK(r), BuildResponse(r)
}
// SetProfileImage sets profile image of the user.
func (c *Client4) SetProfileImage(userId string, data []byte) (bool, *Response) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("image", "profile.png")
if err != nil {
return false, &Response{Error: NewAppError("SetProfileImage", "model.client.set_profile_user.no_file.app_error", nil, err.Error(), http.StatusBadRequest)}
}
if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil {
return false, &Response{Error: NewAppError("SetProfileImage", "model.client.set_profile_user.no_file.app_error", nil, err.Error(), http.StatusBadRequest)}
}
if err = writer.Close(); err != nil {
return false, &Response{Error: NewAppError("SetProfileImage", "model.client.set_profile_user.writer.app_error", nil, err.Error(), http.StatusBadRequest)}
}
rq, err := http.NewRequest("POST", c.ApiUrl+c.GetUserRoute(userId)+"/image", bytes.NewReader(body.Bytes()))
if err != nil {
return false, &Response{Error: NewAppError("SetProfileImage", "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)}
}
rq.Header.Set("Content-Type", writer.FormDataContentType())
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
}
rp, err := c.HttpClient.Do(rq)
if err != nil || rp == nil {
return false, &Response{StatusCode: http.StatusForbidden, Error: NewAppError(c.GetUserRoute(userId)+"/image", "model.client.connecting.app_error", nil, err.Error(), http.StatusForbidden)}
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return false, BuildErrorResponse(rp, AppErrorFromJson(rp.Body))
}
return CheckStatusOK(rp), BuildResponse(rp)
}
// CreateUserAccessToken will generate a user access token that can be used in place
// of a session token to access the REST API. Must have the 'create_user_access_token'
// permission and if generating for another user, must have the 'edit_other_users'
// permission. A non-blank description is required.
func (c *Client4) CreateUserAccessToken(userId, description string) (*UserAccessToken, *Response) {
requestBody := map[string]string{"description": description}
r, err := c.DoApiPost(c.GetUserRoute(userId)+"/tokens", MapToJson(requestBody))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserAccessTokenFromJson(r.Body), BuildResponse(r)
}
// GetUserAccessTokens will get a page of access tokens' id, description, is_active
// and the user_id in the system. The actual token will not be returned. Must have
// the 'manage_system' permission.
func (c *Client4) GetUserAccessTokens(page int, perPage int) ([]*UserAccessToken, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetUserAccessTokensRoute()+query, "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserAccessTokenListFromJson(r.Body), BuildResponse(r)
}
// GetUserAccessToken will get a user access tokens' id, description, is_active
// and the user_id of the user it is for. The actual token will not be returned.
// Must have the 'read_user_access_token' permission and if getting for another
// user, must have the 'edit_other_users' permission.
func (c *Client4) GetUserAccessToken(tokenId string) (*UserAccessToken, *Response) {
r, err := c.DoApiGet(c.GetUserAccessTokenRoute(tokenId), "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserAccessTokenFromJson(r.Body), BuildResponse(r)
}
// GetUserAccessTokensForUser will get a paged list of user access tokens showing id,
// description and user_id for each. The actual tokens will not be returned. Must have
// the 'read_user_access_token' permission and if getting for another user, must have the
// 'edit_other_users' permission.
func (c *Client4) GetUserAccessTokensForUser(userId string, page, perPage int) ([]*UserAccessToken, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetUserRoute(userId)+"/tokens"+query, "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserAccessTokenListFromJson(r.Body), BuildResponse(r)
}
// RevokeUserAccessToken will revoke a user access token by id. Must have the
// 'revoke_user_access_token' permission and if revoking for another user, must have the
// 'edit_other_users' permission.
func (c *Client4) RevokeUserAccessToken(tokenId string) (bool, *Response) {
requestBody := map[string]string{"token_id": tokenId}
r, err := c.DoApiPost(c.GetUsersRoute()+"/tokens/revoke", MapToJson(requestBody))
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// SearchUserAccessTokens returns user access tokens matching the provided search term.
func (c *Client4) SearchUserAccessTokens(search *UserAccessTokenSearch) ([]*UserAccessToken, *Response) {
r, err := c.DoApiPost(c.GetUsersRoute()+"/tokens/search", search.ToJson())
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserAccessTokenListFromJson(r.Body), BuildResponse(r)
}
// DisableUserAccessToken will disable a user access token by id. Must have the
// 'revoke_user_access_token' permission and if disabling for another user, must have the
// 'edit_other_users' permission.
func (c *Client4) DisableUserAccessToken(tokenId string) (bool, *Response) {
requestBody := map[string]string{"token_id": tokenId}
r, err := c.DoApiPost(c.GetUsersRoute()+"/tokens/disable", MapToJson(requestBody))
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// EnableUserAccessToken will enable a user access token by id. Must have the
// 'create_user_access_token' permission and if enabling for another user, must have the
// 'edit_other_users' permission.
func (c *Client4) EnableUserAccessToken(tokenId string) (bool, *Response) {
requestBody := map[string]string{"token_id": tokenId}
r, err := c.DoApiPost(c.GetUsersRoute()+"/tokens/enable", MapToJson(requestBody))
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
MM-12393 Server side of bot accounts. (#10378) * bots model, store and api (#9903) * bots model, store and api Fixes: MM-13100, MM-13101, MM-13103, MM-13105, MMM-13119 * uncomment tests incorrectly commented, and fix merge issues * add etags support * add missing licenses * remove unused sqlbuilder.go (for now...) * rejig permissions * split out READ_BOTS into READ_BOTS and READ_OTHERS_BOTS, the latter implicitly allowing the former * make MANAGE_OTHERS_BOTS imply MANAGE_BOTS * conform to general rest api pattern * eliminate redundant http.StatusOK * Update api4/bot.go Co-Authored-By: lieut-data <jesse.hallam@gmail.com> * s/model.UserFromBotModel/model.UserFromBot/g * Update model/bot.go Co-Authored-By: lieut-data <jesse.hallam@gmail.com> * Update model/client4.go Co-Authored-By: lieut-data <jesse.hallam@gmail.com> * move sessionHasPermissionToManageBot to app/authorization.go * use api.ApiSessionRequired for createBot * introduce BOT_DESCRIPTION_MAX_RUNES constant * MM-13512 Prevent getting a user by email based on privacy settings (#10021) * MM-13512 Prevent getting a user by email based on privacy settings * Add additional config settings to tests * upgrade db to 5.7 (#10019) * MM-13526 Add validation when setting a user's Locale field (#10022) * Fix typos (#10024) * Fixing first user being created with system admin privilages without being explicity specified. (#10014) * Revert "Support for Embeded chat (#9129)" (#10017) This reverts commit 3fcecd521a5c6ccfdb52fb4c3fb1f8c6ea528a4e. * s/DisableBot/UpdateBotActive * add permissions on upgrade * Update NOTICE.txt (#10054) - add new dependency (text) - handle switch to forked dependency (go-gomail -> go-mail) - misc copyright owner updates * avoid leaking bot knowledge without permission * [GH-6798] added a new api endpoint to get the bulk reactions for posts (#10049) * 6798 added a new api to get the bulk reactions for posts * 6798 added the permsission check before getting the reactions * GH-6798 added a new app function for the new endpoint * 6798 added a store method to get reactions for multiple posts * 6798 connected the app function with the new store function * 6798 fixed the review comments * MM-13559 Update model.post.is_valid.file_ids.app_error text per report (#10055) Ticket: https://mattermost.atlassian.net/browse/MM-13559 Report: https://github.com/mattermost/mattermost-server/issues/10023 * Trigger Login Hooks with OAuth (#10061) * make BotStore.GetAll deterministic even on duplicate CreateAt * fix spurious TestMuteCommandSpecificChannel test failure See https://community-daily.mattermost.com/core/pl/px9p8s3dzbg1pf3ddrm5cr36uw * fix race in TestExportUserChannels * TestExportUserChannels: remove SaveMember call, as it is redundant and used to be silently failing anyway * MM-13117: bot tokens (#10111) * eliminate redundant Client/AdminClient declarations * harden TestUpdateChannelScheme to API failures * eliminate unnecessary config restoration * minor cleanup * make TestGenerateMfaSecret config dependency explicit * TestCreateUserAccessToken for bots * TestGetUserAccessToken* for bots * leverage SessionHasPermissionToUserOrBot for user token APIs * Test(Revoke|Disable|Enable)UserAccessToken * make EnableUserAccessTokens explicit, so as to not rely on local config.json * uncomment TestResetPassword, but still skip * mark assert(Invalid)Token as helper * fix whitespace issues * fix mangled comments * MM-13116: bot plugin api (#10113) * MM-13117: expose bot API to plugins This also changes the `CreatorId` column definition to allow for plugin ids, as the default unless the plugin overrides is to use the plugin id here. This branch hasn't hit master yet, so no migration needed. * gofmt issues * expunge use of BotList in plugin/client API * introduce model.BotGetOptions * use botUserId term for clarity * MM-13129 Adding functionality to deal with orphaned bots (#10238) * Add way to list orphaned bots. * Add /assign route to modify ownership of bot accounts. * Apply suggestions from code review Co-Authored-By: crspeller <crspeller@gmail.com> * MM-13120: add IsBot field to returned user objects (#10103) * MM-13104: forbid bot login (#10251) * MM-13104: disallow bot login * fix shadowing * MM-13136 Disable user bots when user is disabled. (#10293) * Disable user bots when user is disabled. * Grammer. Co-Authored-By: crspeller <crspeller@gmail.com> * Fixing bot branch for test changes. * Don't use external dependancies in bot plugin tests. * Rename bot CreatorId to OwnerId * Adding ability to re-enable bots * Fixing IsBot to not attempt to be saved to DB. * Adding diagnostics and licencing counting for bot accounts. * Modifying gorp to allow reading of '-' fields. * Removing unnessisary nil values from UserCountOptions. * Changing comment to GoDoc format * Improving user count SQL * Some improvments from feedback. * Omit empty on User.IsBot
2019-03-05 07:06:45 -08:00
// Bots section
// CreateBot creates a bot in the system based on the provided bot struct.
func (c *Client4) CreateBot(bot *Bot) (*Bot, *Response) {
r, err := c.doApiPostBytes(c.GetBotsRoute(), bot.ToJson())
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return BotFromJson(r.Body), BuildResponse(r)
}
// PatchBot partially updates a bot. Any missing fields are not updated.
func (c *Client4) PatchBot(userId string, patch *BotPatch) (*Bot, *Response) {
r, err := c.doApiPutBytes(c.GetBotRoute(userId), patch.ToJson())
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return BotFromJson(r.Body), BuildResponse(r)
}
// GetBot fetches the given, undeleted bot.
func (c *Client4) GetBot(userId string, etag string) (*Bot, *Response) {
r, err := c.DoApiGet(c.GetBotRoute(userId), etag)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return BotFromJson(r.Body), BuildResponse(r)
}
// GetBot fetches the given bot, even if it is deleted.
func (c *Client4) GetBotIncludeDeleted(userId string, etag string) (*Bot, *Response) {
r, err := c.DoApiGet(c.GetBotRoute(userId)+"?include_deleted=true", etag)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return BotFromJson(r.Body), BuildResponse(r)
}
// GetBots fetches the given page of bots, excluding deleted.
func (c *Client4) GetBots(page, perPage int, etag string) ([]*Bot, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetBotsRoute()+query, etag)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return BotListFromJson(r.Body), BuildResponse(r)
}
// GetBotsIncludeDeleted fetches the given page of bots, including deleted.
func (c *Client4) GetBotsIncludeDeleted(page, perPage int, etag string) ([]*Bot, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v&include_deleted=true", page, perPage)
r, err := c.DoApiGet(c.GetBotsRoute()+query, etag)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return BotListFromJson(r.Body), BuildResponse(r)
}
// GetBotsOrphaned fetches the given page of bots, only including orphanded bots.
func (c *Client4) GetBotsOrphaned(page, perPage int, etag string) ([]*Bot, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v&only_orphaned=true", page, perPage)
r, err := c.DoApiGet(c.GetBotsRoute()+query, etag)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return BotListFromJson(r.Body), BuildResponse(r)
}
// DisableBot disables the given bot in the system.
func (c *Client4) DisableBot(botUserId string) (*Bot, *Response) {
r, err := c.doApiPostBytes(c.GetBotRoute(botUserId)+"/disable", nil)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return BotFromJson(r.Body), BuildResponse(r)
}
// EnableBot disables the given bot in the system.
func (c *Client4) EnableBot(botUserId string) (*Bot, *Response) {
r, err := c.doApiPostBytes(c.GetBotRoute(botUserId)+"/enable", nil)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return BotFromJson(r.Body), BuildResponse(r)
}
// AssignBot assigns the given bot to the given user
func (c *Client4) AssignBot(botUserId, newOwnerId string) (*Bot, *Response) {
r, err := c.doApiPostBytes(c.GetBotRoute(botUserId)+"/assign/"+newOwnerId, nil)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return BotFromJson(r.Body), BuildResponse(r)
}
// SetBotIconImage sets LHS bot icon image.
func (c *Client4) SetBotIconImage(botUserId string, data []byte) (bool, *Response) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("image", "icon.svg")
if err != nil {
return false, &Response{Error: NewAppError("SetBotIconImage", "model.client.set_bot_icon_image.no_file.app_error", nil, err.Error(), http.StatusBadRequest)}
}
if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil {
return false, &Response{Error: NewAppError("SetBotIconImage", "model.client.set_bot_icon_image.no_file.app_error", nil, err.Error(), http.StatusBadRequest)}
}
if err = writer.Close(); err != nil {
return false, &Response{Error: NewAppError("SetBotIconImage", "model.client.set_bot_icon_image.writer.app_error", nil, err.Error(), http.StatusBadRequest)}
}
rq, err := http.NewRequest("POST", c.ApiUrl+c.GetBotRoute(botUserId)+"/icon", bytes.NewReader(body.Bytes()))
if err != nil {
return false, &Response{Error: NewAppError("SetBotIconImage", "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)}
}
rq.Header.Set("Content-Type", writer.FormDataContentType())
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
}
rp, err := c.HttpClient.Do(rq)
if err != nil || rp == nil {
return false, &Response{StatusCode: http.StatusForbidden, Error: NewAppError(c.GetBotRoute(botUserId)+"/icon", "model.client.connecting.app_error", nil, err.Error(), http.StatusForbidden)}
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return false, BuildErrorResponse(rp, AppErrorFromJson(rp.Body))
}
return CheckStatusOK(rp), BuildResponse(rp)
}
// GetBotIconImage gets LHS bot icon image. Must be logged in.
func (c *Client4) GetBotIconImage(botUserId string) ([]byte, *Response) {
r, appErr := c.DoApiGet(c.GetBotRoute(botUserId)+"/icon", "")
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, BuildErrorResponse(r, NewAppError("GetBotIconImage", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode))
}
return data, BuildResponse(r)
}
// DeleteBotIconImage deletes LHS bot icon image. Must be logged in.
func (c *Client4) DeleteBotIconImage(botUserId string) (bool, *Response) {
r, appErr := c.DoApiDelete(c.GetBotRoute(botUserId) + "/icon")
if appErr != nil {
return false, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// Team Section
2017-01-31 07:59:36 -05:00
// CreateTeam creates a team in the system based on the provided team struct.
func (c *Client4) CreateTeam(team *Team) (*Team, *Response) {
r, err := c.DoApiPost(c.GetTeamsRoute(), team.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
2017-01-31 07:59:36 -05:00
}
defer closeBody(r)
return TeamFromJson(r.Body), BuildResponse(r)
2017-01-31 07:59:36 -05:00
}
// GetTeam returns a team based on the provided team id string.
func (c *Client4) GetTeam(teamId, etag string) (*Team, *Response) {
r, err := c.DoApiGet(c.GetTeamRoute(teamId), etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TeamFromJson(r.Body), BuildResponse(r)
}
2017-02-28 10:41:38 +02:00
// GetAllTeams returns all teams based on permissions.
func (c *Client4) GetAllTeams(etag string, page int, perPage int) ([]*Team, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetTeamsRoute()+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
2017-02-28 10:41:38 +02:00
}
defer closeBody(r)
return TeamListFromJson(r.Body), BuildResponse(r)
2017-02-28 10:41:38 +02:00
}
// GetAllTeamsWithTotalCount returns all teams based on permissions.
func (c *Client4) GetAllTeamsWithTotalCount(etag string, page int, perPage int) ([]*Team, int64, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v&include_total_count=true", page, perPage)
r, err := c.DoApiGet(c.GetTeamsRoute()+query, etag)
if err != nil {
return nil, 0, BuildErrorResponse(r, err)
}
defer closeBody(r)
teamsListWithCount := TeamsWithCountFromJson(r.Body)
return teamsListWithCount.Teams, teamsListWithCount.TotalCount, BuildResponse(r)
}
// GetTeamByName returns a team based on the provided team name string.
func (c *Client4) GetTeamByName(name, etag string) (*Team, *Response) {
r, err := c.DoApiGet(c.GetTeamByNameRoute(name), etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TeamFromJson(r.Body), BuildResponse(r)
}
2017-04-04 02:34:14 +09:00
// SearchTeams returns teams matching the provided search term.
func (c *Client4) SearchTeams(search *TeamSearch) ([]*Team, *Response) {
r, err := c.DoApiPost(c.GetTeamsRoute()+"/search", search.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
2017-04-04 02:34:14 +09:00
}
defer closeBody(r)
return TeamListFromJson(r.Body), BuildResponse(r)
2017-04-04 02:34:14 +09:00
}
// SearchTeamsPaged returns a page of teams and the total count matching the provided search term.
func (c *Client4) SearchTeamsPaged(search *TeamSearch) ([]*Team, int64, *Response) {
if search.Page == nil {
search.Page = NewInt(0)
}
if search.PerPage == nil {
search.PerPage = NewInt(100)
}
r, err := c.DoApiPost(c.GetTeamsRoute()+"/search", search.ToJson())
if err != nil {
return nil, 0, BuildErrorResponse(r, err)
}
defer closeBody(r)
twc := TeamsWithCountFromJson(r.Body)
return twc.Teams, twc.TotalCount, BuildResponse(r)
}
// TeamExists returns true or false if the team exist or not.
func (c *Client4) TeamExists(name, etag string) (bool, *Response) {
r, err := c.DoApiGet(c.GetTeamByNameRoute(name)+"/exists", etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return MapBoolFromJson(r.Body)["exists"], BuildResponse(r)
}
// GetTeamsForUser returns a list of teams a user is on. Must be logged in as the user
// or be a system administrator.
func (c *Client4) GetTeamsForUser(userId, etag string) ([]*Team, *Response) {
r, err := c.DoApiGet(c.GetUserRoute(userId)+"/teams", etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TeamListFromJson(r.Body), BuildResponse(r)
}
// GetTeamMember returns a team member based on the provided team and user id strings.
func (c *Client4) GetTeamMember(teamId, userId, etag string) (*TeamMember, *Response) {
r, err := c.DoApiGet(c.GetTeamMemberRoute(teamId, userId), etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TeamMemberFromJson(r.Body), BuildResponse(r)
}
// UpdateTeamMemberRoles will update the roles on a team for a user.
func (c *Client4) UpdateTeamMemberRoles(teamId, userId, newRoles string) (bool, *Response) {
requestBody := map[string]string{"roles": newRoles}
r, err := c.DoApiPut(c.GetTeamMemberRoute(teamId, userId)+"/roles", MapToJson(requestBody))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// UpdateTeamMemberSchemeRoles will update the scheme-derived roles on a team for a user.
func (c *Client4) UpdateTeamMemberSchemeRoles(teamId string, userId string, schemeRoles *SchemeRoles) (bool, *Response) {
r, err := c.DoApiPut(c.GetTeamMemberRoute(teamId, userId)+"/schemeRoles", schemeRoles.ToJson())
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// UpdateTeam will update a team.
func (c *Client4) UpdateTeam(team *Team) (*Team, *Response) {
r, err := c.DoApiPut(c.GetTeamRoute(team.Id), team.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TeamFromJson(r.Body), BuildResponse(r)
}
// PatchTeam partially updates a team. Any missing fields are not updated.
func (c *Client4) PatchTeam(teamId string, patch *TeamPatch) (*Team, *Response) {
r, err := c.DoApiPut(c.GetTeamRoute(teamId)+"/patch", patch.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TeamFromJson(r.Body), BuildResponse(r)
}
// RegenerateTeamInviteId requests a new invite ID to be generated.
func (c *Client4) RegenerateTeamInviteId(teamId string) (*Team, *Response) {
r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/regenerate_invite_id", "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TeamFromJson(r.Body), BuildResponse(r)
}
2017-04-04 02:35:29 +09:00
// SoftDeleteTeam deletes the team softly (archive only, not permanent delete).
func (c *Client4) SoftDeleteTeam(teamId string) (bool, *Response) {
r, err := c.DoApiDelete(c.GetTeamRoute(teamId))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
2017-04-04 02:35:29 +09:00
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
2017-04-04 02:35:29 +09:00
}
// PermanentDeleteTeam deletes the team, should only be used when needed for
// compliance and the like.
func (c *Client4) PermanentDeleteTeam(teamId string) (bool, *Response) {
r, err := c.DoApiDelete(c.GetTeamRoute(teamId) + "?permanent=true")
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// GetTeamMembers returns team members based on the provided team id string.
func (c *Client4) GetTeamMembers(teamId string, page int, perPage int, etag string) ([]*TeamMember, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetTeamMembersRoute(teamId)+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TeamMembersFromJson(r.Body), BuildResponse(r)
}
// GetTeamMembersForUser returns the team members for a user.
func (c *Client4) GetTeamMembersForUser(userId string, etag string) ([]*TeamMember, *Response) {
r, err := c.DoApiGet(c.GetUserRoute(userId)+"/teams/members", etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TeamMembersFromJson(r.Body), BuildResponse(r)
}
// GetTeamMembersByIds will return an array of team members based on the
// team id and a list of user ids provided. Must be authenticated.
func (c *Client4) GetTeamMembersByIds(teamId string, userIds []string) ([]*TeamMember, *Response) {
r, err := c.DoApiPost(fmt.Sprintf("/teams/%v/members/ids", teamId), ArrayToJson(userIds))
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TeamMembersFromJson(r.Body), BuildResponse(r)
}
// AddTeamMember adds user to a team and return a team member.
func (c *Client4) AddTeamMember(teamId, userId string) (*TeamMember, *Response) {
member := &TeamMember{TeamId: teamId, UserId: userId}
r, err := c.DoApiPost(c.GetTeamMembersRoute(teamId), member.ToJson())
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TeamMemberFromJson(r.Body), BuildResponse(r)
}
// AddTeamMemberFromInvite adds a user to a team and return a team member using an invite id
// or an invite token/data pair.
func (c *Client4) AddTeamMemberFromInvite(token, inviteId string) (*TeamMember, *Response) {
var query string
if inviteId != "" {
query += fmt.Sprintf("?invite_id=%v", inviteId)
}
if token != "" {
query += fmt.Sprintf("?token=%v", token)
}
r, err := c.DoApiPost(c.GetTeamsRoute()+"/members/invite"+query, "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TeamMemberFromJson(r.Body), BuildResponse(r)
}
// AddTeamMembers adds a number of users to a team and returns the team members.
func (c *Client4) AddTeamMembers(teamId string, userIds []string) ([]*TeamMember, *Response) {
var members []*TeamMember
for _, userId := range userIds {
member := &TeamMember{TeamId: teamId, UserId: userId}
members = append(members, member)
}
r, err := c.DoApiPost(c.GetTeamMembersRoute(teamId)+"/batch", TeamMembersToJson(members))
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TeamMembersFromJson(r.Body), BuildResponse(r)
}
// RemoveTeamMember will remove a user from a team.
func (c *Client4) RemoveTeamMember(teamId, userId string) (bool, *Response) {
r, err := c.DoApiDelete(c.GetTeamMemberRoute(teamId, userId))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// GetTeamStats returns a team stats based on the team id string.
// Must be authenticated.
func (c *Client4) GetTeamStats(teamId, etag string) (*TeamStats, *Response) {
r, err := c.DoApiGet(c.GetTeamStatsRoute(teamId), etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TeamStatsFromJson(r.Body), BuildResponse(r)
}
// GetTotalUsersStats returns a total system user stats.
// Must be authenticated.
func (c *Client4) GetTotalUsersStats(etag string) (*UsersStats, *Response) {
r, err := c.DoApiGet(c.GetTotalUsersStatsRoute(), etag)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UsersStatsFromJson(r.Body), BuildResponse(r)
}
// GetTeamUnread will return a TeamUnread object that contains the amount of
// unread messages and mentions the user has for the specified team.
// Must be authenticated.
func (c *Client4) GetTeamUnread(teamId, userId string) (*TeamUnread, *Response) {
r, err := c.DoApiGet(c.GetUserRoute(userId)+c.GetTeamRoute(teamId)+"/unread", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TeamUnreadFromJson(r.Body), BuildResponse(r)
}
// ImportTeam will import an exported team from other app into a existing team.
func (c *Client4) ImportTeam(data []byte, filesize int, importFrom, filename, teamId string) (map[string]string, *Response) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("file", filename)
if err != nil {
return nil, &Response{Error: NewAppError("UploadImportTeam", "model.client.upload_post_attachment.file.app_error", nil, err.Error(), http.StatusBadRequest)}
}
if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil {
return nil, &Response{Error: NewAppError("UploadImportTeam", "model.client.upload_post_attachment.file.app_error", nil, err.Error(), http.StatusBadRequest)}
}
part, err = writer.CreateFormField("filesize")
if err != nil {
return nil, &Response{Error: NewAppError("UploadImportTeam", "model.client.upload_post_attachment.file_size.app_error", nil, err.Error(), http.StatusBadRequest)}
}
if _, err = io.Copy(part, strings.NewReader(strconv.Itoa(filesize))); err != nil {
return nil, &Response{Error: NewAppError("UploadImportTeam", "model.client.upload_post_attachment.file_size.app_error", nil, err.Error(), http.StatusBadRequest)}
}
part, err = writer.CreateFormField("importFrom")
if err != nil {
return nil, &Response{Error: NewAppError("UploadImportTeam", "model.client.upload_post_attachment.import_from.app_error", nil, err.Error(), http.StatusBadRequest)}
}
if _, err := io.Copy(part, strings.NewReader(importFrom)); err != nil {
return nil, &Response{Error: NewAppError("UploadImportTeam", "model.client.upload_post_attachment.import_from.app_error", nil, err.Error(), http.StatusBadRequest)}
}
if err := writer.Close(); err != nil {
return nil, &Response{Error: NewAppError("UploadImportTeam", "model.client.upload_post_attachment.writer.app_error", nil, err.Error(), http.StatusBadRequest)}
}
return c.DoUploadImportTeam(c.GetTeamImportRoute(teamId), body.Bytes(), writer.FormDataContentType())
}
// InviteUsersToTeam invite users by email to the team.
func (c *Client4) InviteUsersToTeam(teamId string, userEmails []string) (bool, *Response) {
r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/invite/email", ArrayToJson(userEmails))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
Guest accounts feature (#11428) * MM-14139: Creating permissions for invite/promote/demote guests (#10778) * MM-14139: Creating permissions for invite/promote/demote guests * Fixing tests * Adding invite guest api endpoint (#10792) * Adding invite guest api endpoint * Adding i18n * Adding some tests * WIP * Migrating Token.Extra info to bigger size (2048) * Fixing tests * Adding client function for invite guests * Adding send guests invites tests * Renaming file from guest to guest_invite * Adding Promote/Demote users from/to guest endpoints (#10791) * Adding Promote/Demote users from/to guest endpoints * Adding i18n translations * Adding the client functions * Using getQueryBuilder function * Addressing PR review comments * Adding default channels to users on promte from guest (#10851) * Adding default channels to users on promte from guest * Addressing PR review comments * Fixing merge problems * Sending websockets events on promote/demote (#11403) * Sending websockets events on promote/demote * Fixing merge problems * Fixing govet shadowing problem * Fixing feature branch tests * Avoiding leaking users data through websockets for guest accounts (#11489) * Avoiding leaking users data through websockets for guest accounts * Adding tests and fixing code error * Fixing i18n * Allow to enable/disable guests and other extra config settings (#11481) * Allow to enable/disable guests and other extra config settings * Fixing tests and moving license and config validation to api level * Update api4/role_test.go Co-Authored-By: George Goldberg <george@gberg.me> * Update api4/role_test.go Co-Authored-By: George Goldberg <george@gberg.me> * Fixing typo * fixing tests * Managing correctly the guest channel leave behavior (#11578) * MM-15134: Removing guests from teams or system on leave channels if needed * WIP * No deactivating the guest user when leave the last team * Adding a couple of tests * Fixing shadow variables * Fixing tests * fixing tests * fixing shadow variables * Adding guest counts for channel stats (#11646) * Adding guest counts for channel stats * Adding tests * Fixing tests * Fixing guest domain restrictions (#11660) * Adding needed migration for the database * Fixing migration
2019-07-22 22:13:39 +02:00
// InviteGuestsToTeam invite guest by email to some channels in a team.
func (c *Client4) InviteGuestsToTeam(teamId string, userEmails []string, channels []string, message string) (bool, *Response) {
guestsInvite := GuestsInvite{
Emails: userEmails,
Channels: channels,
Message: message,
}
r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/invite-guests/email", guestsInvite.ToJson())
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// InvalidateEmailInvites will invalidate active email invitations that have not been accepted by the user.
func (c *Client4) InvalidateEmailInvites() (bool, *Response) {
r, err := c.DoApiDelete(c.GetTeamsRoute() + "/invites/email")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// GetTeamInviteInfo returns a team object from an invite id containing sanitized information.
func (c *Client4) GetTeamInviteInfo(inviteId string) (*Team, *Response) {
r, err := c.DoApiGet(c.GetTeamsRoute()+"/invite/"+inviteId, "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TeamFromJson(r.Body), BuildResponse(r)
}
// SetTeamIcon sets team icon of the team.
func (c *Client4) SetTeamIcon(teamId string, data []byte) (bool, *Response) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("image", "teamIcon.png")
if err != nil {
return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.set_team_icon.no_file.app_error", nil, err.Error(), http.StatusBadRequest)}
}
if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil {
return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.set_team_icon.no_file.app_error", nil, err.Error(), http.StatusBadRequest)}
}
if err = writer.Close(); err != nil {
return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.set_team_icon.writer.app_error", nil, err.Error(), http.StatusBadRequest)}
}
rq, err := http.NewRequest("POST", c.ApiUrl+c.GetTeamRoute(teamId)+"/image", bytes.NewReader(body.Bytes()))
if err != nil {
return false, &Response{Error: NewAppError("SetTeamIcon", "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)}
}
rq.Header.Set("Content-Type", writer.FormDataContentType())
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
}
rp, err := c.HttpClient.Do(rq)
if err != nil || rp == nil {
// set to http.StatusForbidden(403)
return false, &Response{StatusCode: http.StatusForbidden, Error: NewAppError(c.GetTeamRoute(teamId)+"/image", "model.client.connecting.app_error", nil, err.Error(), 403)}
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return false, BuildErrorResponse(rp, AppErrorFromJson(rp.Body))
}
return CheckStatusOK(rp), BuildResponse(rp)
}
// GetTeamIcon gets the team icon of the team.
func (c *Client4) GetTeamIcon(teamId, etag string) ([]byte, *Response) {
r, appErr := c.DoApiGet(c.GetTeamRoute(teamId)+"/image", etag)
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, BuildErrorResponse(r, NewAppError("GetTeamIcon", "model.client.get_team_icon.app_error", nil, err.Error(), r.StatusCode))
}
return data, BuildResponse(r)
}
// RemoveTeamIcon updates LastTeamIconUpdate to 0 which indicates team icon is removed.
func (c *Client4) RemoveTeamIcon(teamId string) (bool, *Response) {
r, err := c.DoApiDelete(c.GetTeamRoute(teamId) + "/image")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// Channel Section
Ldap groups phase1 (#9752) * Initial models, API, app, and persistence of groups and group syncing. * Consistent letter casing in ldif. * Moves group-specific migrations into func. * Adds API endpoint to retrieve LDAP groups (and associated MM groups) one tree level at a time. * Adds mattermost group id to SCIMGroup (if available). * Splits user and group creation so that memberOf works. Returns users from ldap interface. * Updates method name. * Returns users IDs instead of User. * Removes non-essential group data. * MM-11807: Add GroupFilter to LDAP config. (#9513) * MM-11807: Add GroupFilter to LDAP config. * Add diagnostic. * Adds new config option for using 'memberOf' overlay. * Adds API endpoint to link a group. * Removes debug statements. * Adds unlink group API endpoint. * Fix to LDAP API. Adds API method to client4 and app. * Adds some missing app methods. Renames API unexported func. * Fixes link/unlink API path to accept valid DNs. * Allow any character for DN portion of path. * Switches from DN to objectGUID or entryUUID as the remote identifier linking LDAP groups to MM groups. * Formatting. * Formatting. * Setting group name field to an ID for phase 1. * Adds an LDAP config field to Setting up configuration for local LDAP. * Changes to LDAP and GroupStore interfaces. * Draft of nesting groups in API response. * Removes unnecessary tree models. * Updates group membershipt create store method to also restore. * Adds new config to test config. * Accept AD format length. * Switches to SetUniqueTogether method. * Updates revert. * Tweaks to syncing queries . * Updates query for pending team and channel memberships. * Removes old GroupSyncableScanner usage. Some formatting and renaming. * Fixes bug setting syncable type in selecting paged. * Adds tests for syncables populator. * Only add users to teams and channels that are not deleted. * Renames method. * Updates test LDAP setup. * Removes memberof config stuff. * Renames. * Updates test data. * Fix for gofmt. * Adds missing license. * Adds missing teardowns. * Test fix. * Adds a cycle to the groups test data. * Changes API to return flat list. * Removes some unused interface and app methods. * Returns empty braces if results are empty. * Adds more LDAP test data. * Fix for test data error. * Adds error. * Moves test groups. * Adds OU for load test data. * Moves load test ou creation to load data. * Adds a new bool flag to SCIMGroups. * Removes SCIMGroup completely. * Removes FULL JOIN because it is not supported in MySQL. * Adds tests for sync queries; renames constant. * Bad merge fix. * Vet fix. * Returning OK on delete ldap group link * Removes foreign key constraints. * Adding total to the ldap getAllGroups api endpoint * Adds get group members page. * Removes pagination from groups syncables list API. * Adding syncable check now that foreign key constraint is removes. * Joins teams and channels to group syncables. * Adds group member count. * Adding GetAllChannels and SearchAllChannels for system admins only * Fix. * Test fix from pagination removal. * Orders groupmembers by createat. * Fixing search of all channels * Test fix after removing pagination. * JSON syntax error fix. * Changing tests (for now) pending investigation. * Adding GetAllChannels and SearchAllChannels tests for the store * Adding GetAllChannels and SearchAllChannels API tests * Omit empty JSON values of group syncables. * Fixing GetAllChannels and SearchAllChannels tests * Fixing GetAllChannels and SearchAllChannels store tests * Fixing GetAllChannels api tests * Adds 'LDAP groups' feature flag. (#9861) * Migrate new client functions to idiomatic error handling * Test fixes. * Simplification of groups api (#9860) * Simplification of groups api * Fixing RequireSyncableType * Test fix. * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Fix copy/paste error. * Fix copy/paste error. * Adds missing return, changes to correct HTTP status code. * Adds missing return, changes status codes. * Check for license. * Renames variable for new signature. * Adds client method to get a group. * Adds client method and tests for PatchGroup. * Adds more API tests. * Adds groups API tests. * Adds client method and tests for getting group syncables. * Adds tests for patching group teams and channels. * Update to translations. * Removes test. * Fix incorrect conditional. * Removes unnecessary nil check. * Removes unnecessary return. * Updates comment, removes unused variable. * Uses consistent JSON unmarshal pattern. * Uses consistent JSON unmarshal pattern. * Moves const block. * Switches 'already linked' from error to success response. * Removes commented-out code. * Switched to status ok. * Add parens for readability. * Fix copy/paste error. * Unexport some structs. * Removes repeated validity check. * Return without attempting commit if there's a rollback. * Fix incorrect HTTP status code. * Update store/sqlstore/group_supplier.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Adds utility methods for going from groupsyncable to groupteam and groupchannel. * Fixing george suggestions (#9911) * Test fix. * Adds QA data to VC with visualization. * Fixes typo in graph image. * Update display name when re-linking in case it has changed in LDAP. * Adds ability to configure group display name and unique identifier. (#9923) * Adds ability to configure group display name and unique identifier. * Adds some configs to confi-ldap make command. * Fix for move of session. * Exposes method for use by SAML package. * Switches GroupSyncableType from int to string. * Update Jenkins build files. * Removes unused variable assignment. * Removes old unnecessary early return. * Removes unnecessary variable. * Moves param parsing before license and permissions checks. * Removes old code. * Compares agains underlying error rather than error id. * Switches tests to assertions. * Adds more assertions. * Adds missing return. * Adds space after comma for added legibility. * Moves a view model to the api package. * Unexports method. * Uses id validator function. * Fix docker-compose flag. * Typo fix. * Moves index creation to supplier. * Removes bad merge. * Renames parameter. * Re-adds space. * Removes unnecessary transaction. * Escapes the Groups table name with backticks because it is a reserved keyword. * Fix roles cache bug * Removing unnecesiary deserializing function * Switches table name rather than custom SQL everywhere for Postgres without backticks. * Removes redundant check for sql.ErrNoRows. * Removes redundant check for sql.ErrNoRows. * Removes data integrity check and redundant nil conditional. * Removes redundant check for sql.ErrNoRows. * Removes unnecessary query. * Removes ID length validation from persistence tier. * Makes some supplier methods idempotent. * Removes some empty switch defaults. * Renames Group Type field to Source. * Fix for mistaken field name change. * Uses IsValidId function. * Removes comment. * Changes json key name. * Removes test because no longer validating user. * Moves model state validation to app layer. * Don't create Groups.CanLeave column until phase 2. * Removes state validation until properties are used in phase 2. * Removes duplicated check. * Removes state validation until properties are used in phase 2. * Removes some tests until phase 2. * Comment-out a bunch of test related to CanLeave. * Extra unmarshal validation check. Removes more code for CanLeave. * Removes tests for CanLeave. * Explict error msg. * Rewrite queries. * Changes index name. Adds index. * Removes assertion. * Adds experimental feature flag.
2019-01-10 15:17:31 -05:00
// GetAllChannels get all the channels. Must be a system administrator.
func (c *Client4) GetAllChannels(page int, perPage int, etag string) (*ChannelListWithTeamData, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetChannelsRoute()+query, etag)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelListWithTeamDataFromJson(r.Body), BuildResponse(r)
}
// GetAllChannelsWithCount get all the channels including the total count. Must be a system administrator.
func (c *Client4) GetAllChannelsWithCount(page int, perPage int, etag string) (*ChannelListWithTeamData, int64, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v&include_total_count=true", page, perPage)
r, err := c.DoApiGet(c.GetChannelsRoute()+query, etag)
if err != nil {
return nil, 0, BuildErrorResponse(r, err)
}
defer closeBody(r)
cwc := ChannelsWithCountFromJson(r.Body)
return cwc.Channels, cwc.TotalCount, BuildResponse(r)
}
// CreateChannel creates a channel based on the provided channel struct.
func (c *Client4) CreateChannel(channel *Channel) (*Channel, *Response) {
r, err := c.DoApiPost(c.GetChannelsRoute(), channel.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelFromJson(r.Body), BuildResponse(r)
}
// UpdateChannel updates a channel based on the provided channel struct.
func (c *Client4) UpdateChannel(channel *Channel) (*Channel, *Response) {
r, err := c.DoApiPut(c.GetChannelRoute(channel.Id), channel.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelFromJson(r.Body), BuildResponse(r)
}
// PatchChannel partially updates a channel. Any missing fields are not updated.
func (c *Client4) PatchChannel(channelId string, patch *ChannelPatch) (*Channel, *Response) {
r, err := c.DoApiPut(c.GetChannelRoute(channelId)+"/patch", patch.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelFromJson(r.Body), BuildResponse(r)
}
// ConvertChannelToPrivate converts public to private channel.
func (c *Client4) ConvertChannelToPrivate(channelId string) (*Channel, *Response) {
r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/convert", "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelFromJson(r.Body), BuildResponse(r)
}
// UpdateChannelPrivacy updates channel privacy
func (c *Client4) UpdateChannelPrivacy(channelId string, privacy string) (*Channel, *Response) {
requestBody := map[string]string{"privacy": privacy}
r, err := c.DoApiPut(c.GetChannelRoute(channelId)+"/privacy", MapToJson(requestBody))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelFromJson(r.Body), BuildResponse(r)
}
// RestoreChannel restores a previously deleted channel. Any missing fields are not updated.
func (c *Client4) RestoreChannel(channelId string) (*Channel, *Response) {
r, err := c.DoApiPost(c.GetChannelRoute(channelId)+"/restore", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelFromJson(r.Body), BuildResponse(r)
}
// CreateDirectChannel creates a direct message channel based on the two user
// ids provided.
func (c *Client4) CreateDirectChannel(userId1, userId2 string) (*Channel, *Response) {
requestBody := []string{userId1, userId2}
r, err := c.DoApiPost(c.GetChannelsRoute()+"/direct", ArrayToJson(requestBody))
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelFromJson(r.Body), BuildResponse(r)
}
// CreateGroupChannel creates a group message channel based on userIds provided.
2017-04-20 22:55:43 +09:00
func (c *Client4) CreateGroupChannel(userIds []string) (*Channel, *Response) {
r, err := c.DoApiPost(c.GetChannelsRoute()+"/group", ArrayToJson(userIds))
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
2017-04-20 22:55:43 +09:00
}
defer closeBody(r)
return ChannelFromJson(r.Body), BuildResponse(r)
2017-04-20 22:55:43 +09:00
}
// GetChannel returns a channel based on the provided channel id string.
func (c *Client4) GetChannel(channelId, etag string) (*Channel, *Response) {
r, err := c.DoApiGet(c.GetChannelRoute(channelId), etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelFromJson(r.Body), BuildResponse(r)
}
// GetChannelStats returns statistics for a channel.
func (c *Client4) GetChannelStats(channelId string, etag string) (*ChannelStats, *Response) {
r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/stats", etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelStatsFromJson(r.Body), BuildResponse(r)
}
// GetChannelMembersTimezones gets a list of timezones for a channel.
func (c *Client4) GetChannelMembersTimezones(channelId string) ([]string, *Response) {
r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/timezones", "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ArrayFromJson(r.Body), BuildResponse(r)
}
// GetPinnedPosts gets a list of pinned posts.
func (c *Client4) GetPinnedPosts(channelId string, etag string) (*PostList, *Response) {
r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/pinned", etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return PostListFromJson(r.Body), BuildResponse(r)
}
// GetPublicChannelsForTeam returns a list of public channels based on the provided team id string.
func (c *Client4) GetPublicChannelsForTeam(teamId string, page int, perPage int, etag string) ([]*Channel, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetChannelsForTeamRoute(teamId)+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelSliceFromJson(r.Body), BuildResponse(r)
}
// GetDeletedChannelsForTeam returns a list of public channels based on the provided team id string.
func (c *Client4) GetDeletedChannelsForTeam(teamId string, page int, perPage int, etag string) ([]*Channel, *Response) {
query := fmt.Sprintf("/deleted?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetChannelsForTeamRoute(teamId)+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelSliceFromJson(r.Body), BuildResponse(r)
}
// GetPublicChannelsByIdsForTeam returns a list of public channels based on provided team id string.
func (c *Client4) GetPublicChannelsByIdsForTeam(teamId string, channelIds []string) ([]*Channel, *Response) {
r, err := c.DoApiPost(c.GetChannelsForTeamRoute(teamId)+"/ids", ArrayToJson(channelIds))
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelSliceFromJson(r.Body), BuildResponse(r)
}
// GetChannelsForTeamForUser returns a list channels of on a team for a user.
func (c *Client4) GetChannelsForTeamForUser(teamId, userId, etag string) ([]*Channel, *Response) {
r, err := c.DoApiGet(c.GetUserRoute(userId)+c.GetTeamRoute(teamId)+"/channels", etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelSliceFromJson(r.Body), BuildResponse(r)
}
// SearchChannels returns the channels on a team matching the provided search term.
func (c *Client4) SearchChannels(teamId string, search *ChannelSearch) ([]*Channel, *Response) {
r, err := c.DoApiPost(c.GetChannelsForTeamRoute(teamId)+"/search", search.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelSliceFromJson(r.Body), BuildResponse(r)
}
// SearchArchivedChannels returns the archived channels on a team matching the provided search term.
func (c *Client4) SearchArchivedChannels(teamId string, search *ChannelSearch) ([]*Channel, *Response) {
r, err := c.DoApiPost(c.GetChannelsForTeamRoute(teamId)+"/search_archived", search.ToJson())
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelSliceFromJson(r.Body), BuildResponse(r)
}
Ldap groups phase1 (#9752) * Initial models, API, app, and persistence of groups and group syncing. * Consistent letter casing in ldif. * Moves group-specific migrations into func. * Adds API endpoint to retrieve LDAP groups (and associated MM groups) one tree level at a time. * Adds mattermost group id to SCIMGroup (if available). * Splits user and group creation so that memberOf works. Returns users from ldap interface. * Updates method name. * Returns users IDs instead of User. * Removes non-essential group data. * MM-11807: Add GroupFilter to LDAP config. (#9513) * MM-11807: Add GroupFilter to LDAP config. * Add diagnostic. * Adds new config option for using 'memberOf' overlay. * Adds API endpoint to link a group. * Removes debug statements. * Adds unlink group API endpoint. * Fix to LDAP API. Adds API method to client4 and app. * Adds some missing app methods. Renames API unexported func. * Fixes link/unlink API path to accept valid DNs. * Allow any character for DN portion of path. * Switches from DN to objectGUID or entryUUID as the remote identifier linking LDAP groups to MM groups. * Formatting. * Formatting. * Setting group name field to an ID for phase 1. * Adds an LDAP config field to Setting up configuration for local LDAP. * Changes to LDAP and GroupStore interfaces. * Draft of nesting groups in API response. * Removes unnecessary tree models. * Updates group membershipt create store method to also restore. * Adds new config to test config. * Accept AD format length. * Switches to SetUniqueTogether method. * Updates revert. * Tweaks to syncing queries . * Updates query for pending team and channel memberships. * Removes old GroupSyncableScanner usage. Some formatting and renaming. * Fixes bug setting syncable type in selecting paged. * Adds tests for syncables populator. * Only add users to teams and channels that are not deleted. * Renames method. * Updates test LDAP setup. * Removes memberof config stuff. * Renames. * Updates test data. * Fix for gofmt. * Adds missing license. * Adds missing teardowns. * Test fix. * Adds a cycle to the groups test data. * Changes API to return flat list. * Removes some unused interface and app methods. * Returns empty braces if results are empty. * Adds more LDAP test data. * Fix for test data error. * Adds error. * Moves test groups. * Adds OU for load test data. * Moves load test ou creation to load data. * Adds a new bool flag to SCIMGroups. * Removes SCIMGroup completely. * Removes FULL JOIN because it is not supported in MySQL. * Adds tests for sync queries; renames constant. * Bad merge fix. * Vet fix. * Returning OK on delete ldap group link * Removes foreign key constraints. * Adding total to the ldap getAllGroups api endpoint * Adds get group members page. * Removes pagination from groups syncables list API. * Adding syncable check now that foreign key constraint is removes. * Joins teams and channels to group syncables. * Adds group member count. * Adding GetAllChannels and SearchAllChannels for system admins only * Fix. * Test fix from pagination removal. * Orders groupmembers by createat. * Fixing search of all channels * Test fix after removing pagination. * JSON syntax error fix. * Changing tests (for now) pending investigation. * Adding GetAllChannels and SearchAllChannels tests for the store * Adding GetAllChannels and SearchAllChannels API tests * Omit empty JSON values of group syncables. * Fixing GetAllChannels and SearchAllChannels tests * Fixing GetAllChannels and SearchAllChannels store tests * Fixing GetAllChannels api tests * Adds 'LDAP groups' feature flag. (#9861) * Migrate new client functions to idiomatic error handling * Test fixes. * Simplification of groups api (#9860) * Simplification of groups api * Fixing RequireSyncableType * Test fix. * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Fix copy/paste error. * Fix copy/paste error. * Adds missing return, changes to correct HTTP status code. * Adds missing return, changes status codes. * Check for license. * Renames variable for new signature. * Adds client method to get a group. * Adds client method and tests for PatchGroup. * Adds more API tests. * Adds groups API tests. * Adds client method and tests for getting group syncables. * Adds tests for patching group teams and channels. * Update to translations. * Removes test. * Fix incorrect conditional. * Removes unnecessary nil check. * Removes unnecessary return. * Updates comment, removes unused variable. * Uses consistent JSON unmarshal pattern. * Uses consistent JSON unmarshal pattern. * Moves const block. * Switches 'already linked' from error to success response. * Removes commented-out code. * Switched to status ok. * Add parens for readability. * Fix copy/paste error. * Unexport some structs. * Removes repeated validity check. * Return without attempting commit if there's a rollback. * Fix incorrect HTTP status code. * Update store/sqlstore/group_supplier.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Adds utility methods for going from groupsyncable to groupteam and groupchannel. * Fixing george suggestions (#9911) * Test fix. * Adds QA data to VC with visualization. * Fixes typo in graph image. * Update display name when re-linking in case it has changed in LDAP. * Adds ability to configure group display name and unique identifier. (#9923) * Adds ability to configure group display name and unique identifier. * Adds some configs to confi-ldap make command. * Fix for move of session. * Exposes method for use by SAML package. * Switches GroupSyncableType from int to string. * Update Jenkins build files. * Removes unused variable assignment. * Removes old unnecessary early return. * Removes unnecessary variable. * Moves param parsing before license and permissions checks. * Removes old code. * Compares agains underlying error rather than error id. * Switches tests to assertions. * Adds more assertions. * Adds missing return. * Adds space after comma for added legibility. * Moves a view model to the api package. * Unexports method. * Uses id validator function. * Fix docker-compose flag. * Typo fix. * Moves index creation to supplier. * Removes bad merge. * Renames parameter. * Re-adds space. * Removes unnecessary transaction. * Escapes the Groups table name with backticks because it is a reserved keyword. * Fix roles cache bug * Removing unnecesiary deserializing function * Switches table name rather than custom SQL everywhere for Postgres without backticks. * Removes redundant check for sql.ErrNoRows. * Removes redundant check for sql.ErrNoRows. * Removes data integrity check and redundant nil conditional. * Removes redundant check for sql.ErrNoRows. * Removes unnecessary query. * Removes ID length validation from persistence tier. * Makes some supplier methods idempotent. * Removes some empty switch defaults. * Renames Group Type field to Source. * Fix for mistaken field name change. * Uses IsValidId function. * Removes comment. * Changes json key name. * Removes test because no longer validating user. * Moves model state validation to app layer. * Don't create Groups.CanLeave column until phase 2. * Removes state validation until properties are used in phase 2. * Removes duplicated check. * Removes state validation until properties are used in phase 2. * Removes some tests until phase 2. * Comment-out a bunch of test related to CanLeave. * Extra unmarshal validation check. Removes more code for CanLeave. * Removes tests for CanLeave. * Explict error msg. * Rewrite queries. * Changes index name. Adds index. * Removes assertion. * Adds experimental feature flag.
2019-01-10 15:17:31 -05:00
// SearchAllChannels search in all the channels. Must be a system administrator.
func (c *Client4) SearchAllChannels(search *ChannelSearch) (*ChannelListWithTeamData, *Response) {
r, err := c.DoApiPost(c.GetChannelsRoute()+"/search", search.ToJson())
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelListWithTeamDataFromJson(r.Body), BuildResponse(r)
}
// SearchAllChannelsPaged searches all the channels and returns the results paged with the total count.
func (c *Client4) SearchAllChannelsPaged(search *ChannelSearch) (*ChannelsWithCount, *Response) {
r, err := c.DoApiPost(c.GetChannelsRoute()+"/search", search.ToJson())
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelsWithCountFromJson(r.Body), BuildResponse(r)
}
// SearchGroupChannels returns the group channels of the user whose members' usernames match the search term.
func (c *Client4) SearchGroupChannels(search *ChannelSearch) ([]*Channel, *Response) {
r, err := c.DoApiPost(c.GetChannelsRoute()+"/group/search", search.ToJson())
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelSliceFromJson(r.Body), BuildResponse(r)
}
// DeleteChannel deletes channel based on the provided channel id string.
func (c *Client4) DeleteChannel(channelId string) (bool, *Response) {
r, err := c.DoApiDelete(c.GetChannelRoute(channelId))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// GetChannelByName returns a channel based on the provided channel name and team id strings.
func (c *Client4) GetChannelByName(channelName, teamId string, etag string) (*Channel, *Response) {
r, err := c.DoApiGet(c.GetChannelByNameRoute(channelName, teamId), etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelFromJson(r.Body), BuildResponse(r)
}
// GetChannelByNameIncludeDeleted returns a channel based on the provided channel name and team id strings. Other then GetChannelByName it will also return deleted channels.
func (c *Client4) GetChannelByNameIncludeDeleted(channelName, teamId string, etag string) (*Channel, *Response) {
r, err := c.DoApiGet(c.GetChannelByNameRoute(channelName, teamId)+"?include_deleted=true", etag)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelFromJson(r.Body), BuildResponse(r)
}
// GetChannelByNameForTeamName returns a channel based on the provided channel name and team name strings.
func (c *Client4) GetChannelByNameForTeamName(channelName, teamName string, etag string) (*Channel, *Response) {
r, err := c.DoApiGet(c.GetChannelByNameForTeamNameRoute(channelName, teamName), etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelFromJson(r.Body), BuildResponse(r)
}
// GetChannelByNameForTeamNameIncludeDeleted returns a channel based on the provided channel name and team name strings. Other then GetChannelByNameForTeamName it will also return deleted channels.
func (c *Client4) GetChannelByNameForTeamNameIncludeDeleted(channelName, teamName string, etag string) (*Channel, *Response) {
r, err := c.DoApiGet(c.GetChannelByNameForTeamNameRoute(channelName, teamName)+"?include_deleted=true", etag)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelFromJson(r.Body), BuildResponse(r)
}
// GetChannelMembers gets a page of channel members.
func (c *Client4) GetChannelMembers(channelId string, page, perPage int, etag string) (*ChannelMembers, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetChannelMembersRoute(channelId)+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelMembersFromJson(r.Body), BuildResponse(r)
}
// GetChannelMembersByIds gets the channel members in a channel for a list of user ids.
func (c *Client4) GetChannelMembersByIds(channelId string, userIds []string) (*ChannelMembers, *Response) {
r, err := c.DoApiPost(c.GetChannelMembersRoute(channelId)+"/ids", ArrayToJson(userIds))
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelMembersFromJson(r.Body), BuildResponse(r)
}
// GetChannelMember gets a channel member.
func (c *Client4) GetChannelMember(channelId, userId, etag string) (*ChannelMember, *Response) {
r, err := c.DoApiGet(c.GetChannelMemberRoute(channelId, userId), etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelMemberFromJson(r.Body), BuildResponse(r)
}
// GetChannelMembersForUser gets all the channel members for a user on a team.
func (c *Client4) GetChannelMembersForUser(userId, teamId, etag string) (*ChannelMembers, *Response) {
r, err := c.DoApiGet(fmt.Sprintf(c.GetUserRoute(userId)+"/teams/%v/channels/members", teamId), etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelMembersFromJson(r.Body), BuildResponse(r)
}
// ViewChannel performs a view action for a user. Synonymous with switching channels or marking channels as read by a user.
func (c *Client4) ViewChannel(userId string, view *ChannelView) (*ChannelViewResponse, *Response) {
url := fmt.Sprintf(c.GetChannelsRoute()+"/members/%v/view", userId)
r, err := c.DoApiPost(url, view.ToJson())
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelViewResponseFromJson(r.Body), BuildResponse(r)
}
// GetChannelUnread will return a ChannelUnread object that contains the number of
// unread messages and mentions for a user.
func (c *Client4) GetChannelUnread(channelId, userId string) (*ChannelUnread, *Response) {
r, err := c.DoApiGet(c.GetUserRoute(userId)+c.GetChannelRoute(channelId)+"/unread", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelUnreadFromJson(r.Body), BuildResponse(r)
}
// UpdateChannelRoles will update the roles on a channel for a user.
func (c *Client4) UpdateChannelRoles(channelId, userId, roles string) (bool, *Response) {
requestBody := map[string]string{"roles": roles}
r, err := c.DoApiPut(c.GetChannelMemberRoute(channelId, userId)+"/roles", MapToJson(requestBody))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// UpdateChannelMemberSchemeRoles will update the scheme-derived roles on a channel for a user.
func (c *Client4) UpdateChannelMemberSchemeRoles(channelId string, userId string, schemeRoles *SchemeRoles) (bool, *Response) {
r, err := c.DoApiPut(c.GetChannelMemberRoute(channelId, userId)+"/schemeRoles", schemeRoles.ToJson())
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// UpdateChannelNotifyProps will update the notification properties on a channel for a user.
func (c *Client4) UpdateChannelNotifyProps(channelId, userId string, props map[string]string) (bool, *Response) {
r, err := c.DoApiPut(c.GetChannelMemberRoute(channelId, userId)+"/notify_props", MapToJson(props))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// AddChannelMember adds user to channel and return a channel member.
func (c *Client4) AddChannelMember(channelId, userId string) (*ChannelMember, *Response) {
requestBody := map[string]string{"user_id": userId}
r, err := c.DoApiPost(c.GetChannelMembersRoute(channelId)+"", MapToJson(requestBody))
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelMemberFromJson(r.Body), BuildResponse(r)
}
// AddChannelMemberWithRootId adds user to channel and return a channel member. Post add to channel message has the postRootId.
func (c *Client4) AddChannelMemberWithRootId(channelId, userId, postRootId string) (*ChannelMember, *Response) {
requestBody := map[string]string{"user_id": userId, "post_root_id": postRootId}
r, err := c.DoApiPost(c.GetChannelMembersRoute(channelId)+"", MapToJson(requestBody))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelMemberFromJson(r.Body), BuildResponse(r)
}
// RemoveUserFromChannel will delete the channel member object for a user, effectively removing the user from a channel.
func (c *Client4) RemoveUserFromChannel(channelId, userId string) (bool, *Response) {
r, err := c.DoApiDelete(c.GetChannelMemberRoute(channelId, userId))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// AutocompleteChannelsForTeam will return an ordered list of channels autocomplete suggestions.
func (c *Client4) AutocompleteChannelsForTeam(teamId, name string) (*ChannelList, *Response) {
query := fmt.Sprintf("?name=%v", name)
r, err := c.DoApiGet(c.GetChannelsForTeamRoute(teamId)+"/autocomplete"+query, "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelListFromJson(r.Body), BuildResponse(r)
}
// AutocompleteChannelsForTeamForSearch will return an ordered list of your channels autocomplete suggestions.
func (c *Client4) AutocompleteChannelsForTeamForSearch(teamId, name string) (*ChannelList, *Response) {
query := fmt.Sprintf("?name=%v", name)
r, err := c.DoApiGet(c.GetChannelsForTeamRoute(teamId)+"/search_autocomplete"+query, "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ChannelListFromJson(r.Body), BuildResponse(r)
}
// Post Section
// CreatePost creates a post based on the provided post struct.
func (c *Client4) CreatePost(post *Post) (*Post, *Response) {
r, err := c.DoApiPost(c.GetPostsRoute(), post.ToUnsanitizedJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return PostFromJson(r.Body), BuildResponse(r)
}
// CreatePostEphemeral creates a ephemeral post based on the provided post struct which is send to the given user id.
func (c *Client4) CreatePostEphemeral(post *PostEphemeral) (*Post, *Response) {
r, err := c.DoApiPost(c.GetPostsEphemeralRoute(), post.ToUnsanitizedJson())
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return PostFromJson(r.Body), BuildResponse(r)
}
2017-02-28 11:34:32 +02:00
// UpdatePost updates a post based on the provided post struct.
func (c *Client4) UpdatePost(postId string, post *Post) (*Post, *Response) {
r, err := c.DoApiPut(c.GetPostRoute(postId), post.ToUnsanitizedJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
2017-02-28 11:34:32 +02:00
}
defer closeBody(r)
return PostFromJson(r.Body), BuildResponse(r)
2017-02-28 11:34:32 +02:00
}
// PatchPost partially updates a post. Any missing fields are not updated.
func (c *Client4) PatchPost(postId string, patch *PostPatch) (*Post, *Response) {
r, err := c.DoApiPut(c.GetPostRoute(postId)+"/patch", patch.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return PostFromJson(r.Body), BuildResponse(r)
}
// SetPostUnread marks channel where post belongs as unread on the time of the provided post.
func (c *Client4) SetPostUnread(userId string, postId string) *Response {
r, err := c.DoApiPost(c.GetUserRoute(userId)+c.GetPostRoute(postId)+"/set_unread", "")
if err != nil {
return BuildErrorResponse(r, err)
}
defer closeBody(r)
return BuildResponse(r)
}
// PinPost pin a post based on provided post id string.
func (c *Client4) PinPost(postId string) (bool, *Response) {
r, err := c.DoApiPost(c.GetPostRoute(postId)+"/pin", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// UnpinPost unpin a post based on provided post id string.
func (c *Client4) UnpinPost(postId string) (bool, *Response) {
r, err := c.DoApiPost(c.GetPostRoute(postId)+"/unpin", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// GetPost gets a single post.
func (c *Client4) GetPost(postId string, etag string) (*Post, *Response) {
r, err := c.DoApiGet(c.GetPostRoute(postId), etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return PostFromJson(r.Body), BuildResponse(r)
}
// DeletePost deletes a post from the provided post id string.
func (c *Client4) DeletePost(postId string) (bool, *Response) {
r, err := c.DoApiDelete(c.GetPostRoute(postId))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// GetPostThread gets a post with all the other posts in the same thread.
func (c *Client4) GetPostThread(postId string, etag string) (*PostList, *Response) {
r, err := c.DoApiGet(c.GetPostRoute(postId)+"/thread", etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return PostListFromJson(r.Body), BuildResponse(r)
}
// GetPostsForChannel gets a page of posts with an array for ordering for a channel.
func (c *Client4) GetPostsForChannel(channelId string, page, perPage int, etag string) (*PostList, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/posts"+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return PostListFromJson(r.Body), BuildResponse(r)
}
// GetFlaggedPostsForUser returns flagged posts of a user based on user id string.
func (c *Client4) GetFlaggedPostsForUser(userId string, page int, perPage int) (*PostList, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetUserRoute(userId)+"/posts/flagged"+query, "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return PostListFromJson(r.Body), BuildResponse(r)
}
// GetFlaggedPostsForUserInTeam returns flagged posts in team of a user based on user id string.
func (c *Client4) GetFlaggedPostsForUserInTeam(userId string, teamId string, page int, perPage int) (*PostList, *Response) {
if len(teamId) == 0 || len(teamId) != 26 {
return nil, &Response{StatusCode: http.StatusBadRequest, Error: NewAppError("GetFlaggedPostsForUserInTeam", "model.client.get_flagged_posts_in_team.missing_parameter.app_error", nil, "", http.StatusBadRequest)}
}
query := fmt.Sprintf("?team_id=%v&page=%v&per_page=%v", teamId, page, perPage)
r, err := c.DoApiGet(c.GetUserRoute(userId)+"/posts/flagged"+query, "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return PostListFromJson(r.Body), BuildResponse(r)
}
// GetFlaggedPostsForUserInChannel returns flagged posts in channel of a user based on user id string.
func (c *Client4) GetFlaggedPostsForUserInChannel(userId string, channelId string, page int, perPage int) (*PostList, *Response) {
if len(channelId) == 0 || len(channelId) != 26 {
return nil, &Response{StatusCode: http.StatusBadRequest, Error: NewAppError("GetFlaggedPostsForUserInChannel", "model.client.get_flagged_posts_in_channel.missing_parameter.app_error", nil, "", http.StatusBadRequest)}
}
query := fmt.Sprintf("?channel_id=%v&page=%v&per_page=%v", channelId, page, perPage)
r, err := c.DoApiGet(c.GetUserRoute(userId)+"/posts/flagged"+query, "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return PostListFromJson(r.Body), BuildResponse(r)
}
// GetPostsSince gets posts created after a specified time as Unix time in milliseconds.
func (c *Client4) GetPostsSince(channelId string, time int64) (*PostList, *Response) {
query := fmt.Sprintf("?since=%v", time)
r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/posts"+query, "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return PostListFromJson(r.Body), BuildResponse(r)
}
// GetPostsAfter gets a page of posts that were posted after the post provided.
func (c *Client4) GetPostsAfter(channelId, postId string, page, perPage int, etag string) (*PostList, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v&after=%v", page, perPage, postId)
r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/posts"+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return PostListFromJson(r.Body), BuildResponse(r)
}
// GetPostsBefore gets a page of posts that were posted before the post provided.
func (c *Client4) GetPostsBefore(channelId, postId string, page, perPage int, etag string) (*PostList, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v&before=%v", page, perPage, postId)
r, err := c.DoApiGet(c.GetChannelRoute(channelId)+"/posts"+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return PostListFromJson(r.Body), BuildResponse(r)
}
MM-11210 Add "GET /posts/unread" API to support landing on the last unread post (#11486) * [MM-11210] Add API GET 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/posts/unread' for scrolling overhaul (#9108) * Add API GET 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/posts/unread' * add constants * refactor GetPostSince and added more tests * move constants to app package * [MM-11528 && MM-11583] Add userId to in the "posts/unread" path and update test with time delay to fix intermittent failure (#9229) * add userId to in the "posts/unread" path and update test with time delay to fix intermittent failure * add limit before and after to query * remove time delay on test and put pretermined value of Post.CreateAt * Fix conflict * [MM-11876] Add cursor to posts list such as next_post_id and previous_post_id (#9707) * add cursor to posts list such as next_post_id and previous_post_id add publish previous_post_id on WEBSOCKET_EVENT_POSTED and only get next or previous post IDs if necessary revert change on adding previous_post_id in WEBSOCKET_EVENT_POSTED add missing strings import fix merge conflicts * update per comment * update per feedback * corrected the logic in getting the next and previous post ID * fix logic to determine next and post IDs, and rename function to have suffix of "Time" * rearrange logics and add mote tests * fix merge conflict * fix missing message when using unread API (#10233) * MM-15569 Fixes failing test on TestGetPostsForChannelAroundLastUnread (#11039) * Fix missing posts when getting posts since * revert changes to GetPostsSince * migrate Post.GetPostAfterTime and Post.GetPostBeforeTime to sync by default * revert change to cacheItem * Fix post ID validation, build query on squirrel and only return post ID as necessary
2019-07-04 05:23:04 +08:00
// GetPostsAroundLastUnread gets a list of posts around last unread post by a user in a channel.
func (c *Client4) GetPostsAroundLastUnread(userId, channelId string, limitBefore, limitAfter int) (*PostList, *Response) {
query := fmt.Sprintf("?limit_before=%v&limit_after=%v", limitBefore, limitAfter)
if r, err := c.DoApiGet(c.GetUserRoute(userId)+c.GetChannelRoute(channelId)+"/posts/unread"+query, ""); err != nil {
return nil, BuildErrorResponse(r, err)
} else {
defer closeBody(r)
return PostListFromJson(r.Body), BuildResponse(r)
}
}
// SearchPosts returns any posts with matching terms string.
func (c *Client4) SearchPosts(teamId string, terms string, isOrSearch bool) (*PostList, *Response) {
params := SearchParameter{
Terms: &terms,
IsOrSearch: &isOrSearch,
}
return c.SearchPostsWithParams(teamId, &params)
}
// SearchPostsWithParams returns any posts with matching terms string.
func (c *Client4) SearchPostsWithParams(teamId string, params *SearchParameter) (*PostList, *Response) {
r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/posts/search", params.SearchParameterToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return PostListFromJson(r.Body), BuildResponse(r)
}
// SearchPostsWithMatches returns any posts with matching terms string, including.
func (c *Client4) SearchPostsWithMatches(teamId string, terms string, isOrSearch bool) (*PostSearchResults, *Response) {
requestBody := map[string]interface{}{"terms": terms, "is_or_search": isOrSearch}
r, err := c.DoApiPost(c.GetTeamRoute(teamId)+"/posts/search", StringInterfaceToJson(requestBody))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return PostSearchResultsFromJson(r.Body), BuildResponse(r)
}
// DoPostAction performs a post action.
func (c *Client4) DoPostAction(postId, actionId string) (bool, *Response) {
r, err := c.DoApiPost(c.GetPostRoute(postId)+"/actions/"+actionId, "")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
MM-10516: Added support for PostActions in ephemeral posts (#10258) * Added support for PostActions in ephemeral posts The general approach is that we take all the metadata that DoPostAction needs to process client DoPostActionRequests, and store it in a serialized, encrypted Cookie field, in the PostAction struct. The client then must send it back, and it is then used to process PostActions as a fallback top the metadata in the database. This PR adds a new config setting, `ServiceSettings.ActionCookieSecret`. In a cluster environment it must be the same for all instances. - Added type PostActionCookie, and a Cookie string to PostAction. - Added App.AddActionCookiesToPost. - Use App.AddActionCookiesToPost in api4.createEphemeralPost, App.SendEphemeralPost, App.UpdateEphemeralPost. - Added App.DoPostActionWithCookie to process incoming requests with cookies. For backward compatibility, it prefers the metadata in the database; falls back to cookie. - Added plugin.API.UpdateEphemeralPost and plugin.API.DeleteEphemeralPost. - Added App.encryptActionCookie/App.decryptActionCookie. * Style * Fixed an unfortunate typo, tested with matterpoll * minor PR feedback * Fixed uninitialized Context * Fixed another test failure * Fixed permission check * Added api test for DoPostActionWithCookie * Replaced config.ActionCookieSecret with Server.PostActionCookieSecret Modeled after AsymetricSigningKey * style * Set DeleteAt in DeleteEphemeralPost * PR feedback * Removed deadwood comment * Added EXPERIMENTAL comment to the 2 APIs in question
2019-03-01 10:15:31 -08:00
// DoPostActionWithCookie performs a post action with extra arguments
func (c *Client4) DoPostActionWithCookie(postId, actionId, selected, cookieStr string) (bool, *Response) {
var body []byte
if selected != "" || cookieStr != "" {
body, _ = json.Marshal(DoPostActionRequest{
SelectedOption: selected,
Cookie: cookieStr,
})
}
r, err := c.DoApiPost(c.GetPostRoute(postId)+"/actions/"+actionId, string(body))
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// OpenInteractiveDialog sends a WebSocket event to a user's clients to
// open interactive dialogs, based on the provided trigger ID and other
// provided data. Used with interactive message buttons, menus and
// slash commands.
func (c *Client4) OpenInteractiveDialog(request OpenDialogRequest) (bool, *Response) {
b, _ := json.Marshal(request)
r, err := c.DoApiPost("/actions/dialogs/open", string(b))
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// SubmitInteractiveDialog will submit the provided dialog data to the integration
// configured by the URL. Used with the interactive dialogs integration feature.
func (c *Client4) SubmitInteractiveDialog(request SubmitDialogRequest) (*SubmitDialogResponse, *Response) {
b, _ := json.Marshal(request)
r, err := c.DoApiPost("/actions/dialogs/submit", string(b))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
var resp SubmitDialogResponse
json.NewDecoder(r.Body).Decode(&resp)
return &resp, BuildResponse(r)
}
// UploadFile will upload a file to a channel using a multipart request, to be later attached to a post.
// This method is functionally equivalent to Client4.UploadFileAsRequestBody.
func (c *Client4) UploadFile(data []byte, channelId string, filename string) (*FileUploadResponse, *Response) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
MM-7633: Optimize memory utilization during file uploads (#9835) * MM-7633: Optimize memory utilization during file uploads Refactored the file upload code to reduce redundant buffering and stream directly to the file store. Added tests. Benchmark results: ``` levs-mbp:mattermost-server levb$ go test -v -run nothing -bench Upload -benchmem ./app ... BenchmarkUploadFile/random-5Mb-gif-raw-ish_DoUploadFile-4 10 122598031 ns/op 21211370 B/op 1008 allocs/op BenchmarkUploadFile/random-5Mb-gif-raw_UploadFileTask-4 100 20211926 ns/op 5678750 B/op 126 allocs/op BenchmarkUploadFile/random-5Mb-gif-UploadFiles-4 2 1037051184 ns/op 81806360 B/op 3705013 allocs/op BenchmarkUploadFile/random-5Mb-gif-UploadFileTask-4 2 933644431 ns/op 67015868 B/op 3704410 allocs/op BenchmarkUploadFile/random-2Mb-jpg-raw-ish_DoUploadFile-4 100 13110509 ns/op 6032614 B/op 8052 allocs/op BenchmarkUploadFile/random-2Mb-jpg-raw_UploadFileTask-4 100 10729867 ns/op 1738303 B/op 125 allocs/op BenchmarkUploadFile/random-2Mb-jpg-UploadFiles-4 2 925274912 ns/op 70326352 B/op 3718856 allocs/op BenchmarkUploadFile/random-2Mb-jpg-UploadFileTask-4 2 995033336 ns/op 58113796 B/op 3710943 allocs/op BenchmarkUploadFile/zero-10Mb-raw-ish_DoUploadFile-4 30 50777211 ns/op 54791929 B/op 2714 allocs/op BenchmarkUploadFile/zero-10Mb-raw_UploadFileTask-4 50 36387339 ns/op 10503920 B/op 126 allocs/op BenchmarkUploadFile/zero-10Mb-UploadFiles-4 30 48657678 ns/op 54791948 B/op 2719 allocs/op BenchmarkUploadFile/zero-10Mb-UploadFileTask-4 50 37506467 ns/op 31492060 B/op 131 allocs/op ... ``` https://mattermost.atlassian.net/browse/MM-7633 https://github.com/mattermost/mattermost-server/issues/7801 [Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fields.] - [x] Added or updated unit tests (required for all new features) - [ ] Added API documentation (required for all new APIs) - [ ] All new/modified APIs include changes to the drivers *N/A*??? - [x] Includes text changes and localization file ([.../i18n/en.json](https://github.com/mattermost/mattermost-server/blob/master/i18n/en.json)) updates Overview of changes: - api4 - Replaced `uploadFile` handler with `uploadFileStream` that reduces unnecessary buffering. - Added/refactored tests for the new API. - Refactored apitestlib/Check...Status functions. - app - Added App.UploadFileTask, a more efficient refactor of UploadFile. - Consistently set `FileInfo.HasPreviewImage` - Added benchmarks for the new and prior implementations - Replaced passing around `*image.Image` with `image.Image` in the existing code. - model - Added a more capable `client4.UploadFiles` API to match the new server API’s capabilities. - I18n - Replaced `api.file.upload_file.bad_parse.app_error` with a more generic `api.file.upload_file.read_request.app_error` - plugin - Removed type `plugin.multiPluginHookRunnerFunc` in favor of using `func(hooks Hooks) bool` explicitly, to help with testing - tests - Added test files for testing images Still remaining, but can be separate PRs - please let me know the preferred course of action - Investigate JS client API - how does it do multipart? - Performance loss from old code on (small) image processing? - Deprecate the old functions, change other API implementations to use UploadFileTask Definitely separate future PRs - should I file tickets foe these? - Only invoke t.readAll() if there are indeed applicable plugins to run - Find a way to leverage goexif buffer rather than re-reading Suggested long-term improvements - should I file separate tickets for these? - Actually allow uploading of large (GB-sized) files. This may require a change in how the file is passed to plugins. - (Many) api4 tests should probably be subtests and share a server setup - will be much faster - Performance improvements in image processing (goexif/thumbnail/preview) (maybe use https://mattermost.atlassian.net/browse/MM-10188 for this) Questions: 1. I am commiting MBs of test images, are there better alternatives? I can probably create much less dense images that would take up considerably less space, even at pretty large sizes 2. I18n: Do I need to do anything special for the string change? Or just wait until it gets picked up and translated/updated? 3. The image dimensions are flipped in resulting FileInfo to match the actual orientation. Is this by design? Should add a test for it, perhaps? 4. What to do in the case of partial success? S3 but not DB, some files but not others? For now, just doing what the old code did, I think. 5. Make maxUploadDrainBytes configurable? Also, should this be the systemic behavior of all APIs with non-empty body? Otherwise dropped altogether? Check all other ioutil.ReadAll() from sockets. Find a way to set a total byte limit on request Body? * WIP - Fixed for GetPluginsEnvironment() changes * WIP - PR feedback 1. Refactored checkHTTPStatus to improve failure messages 2. Use `var name []type` rather than `name := ([]type)(nil)` 3. Replaced single-letter `p` with a more intention-revealing `part` 4. Added tests for full image size, `HasPreviewImage` * WIP - rebased (c.Session->c.App.Session) * WIP - PR feedback: eliminated use of Request.MultipartReader Instead of hacking the request object to use r.MultipartReader now have own functions `parseMultipartRequestHeader` and `multipartReader` eliminating the need to hack the request object to use Request.MultipartReader limitations. * WIP - PR feedback: UploadFileX with functional options * WIP - PR feedback: style * WIP - PR feedback: errors cleanup * WIP - clarified File Upload benchmarks * WIP - PR feedback: display the value of erroneous formname * WIP - PR feedback: fixed handling of multiple channel_ids * WIP - rebased from master - fixed tests * PR Feedback * PR feedback - moved client4.UploadFiles to _test for now
2018-12-13 13:32:07 -08:00
part, err := writer.CreateFormField("channel_id")
if err != nil {
MM-7633: Optimize memory utilization during file uploads (#9835) * MM-7633: Optimize memory utilization during file uploads Refactored the file upload code to reduce redundant buffering and stream directly to the file store. Added tests. Benchmark results: ``` levs-mbp:mattermost-server levb$ go test -v -run nothing -bench Upload -benchmem ./app ... BenchmarkUploadFile/random-5Mb-gif-raw-ish_DoUploadFile-4 10 122598031 ns/op 21211370 B/op 1008 allocs/op BenchmarkUploadFile/random-5Mb-gif-raw_UploadFileTask-4 100 20211926 ns/op 5678750 B/op 126 allocs/op BenchmarkUploadFile/random-5Mb-gif-UploadFiles-4 2 1037051184 ns/op 81806360 B/op 3705013 allocs/op BenchmarkUploadFile/random-5Mb-gif-UploadFileTask-4 2 933644431 ns/op 67015868 B/op 3704410 allocs/op BenchmarkUploadFile/random-2Mb-jpg-raw-ish_DoUploadFile-4 100 13110509 ns/op 6032614 B/op 8052 allocs/op BenchmarkUploadFile/random-2Mb-jpg-raw_UploadFileTask-4 100 10729867 ns/op 1738303 B/op 125 allocs/op BenchmarkUploadFile/random-2Mb-jpg-UploadFiles-4 2 925274912 ns/op 70326352 B/op 3718856 allocs/op BenchmarkUploadFile/random-2Mb-jpg-UploadFileTask-4 2 995033336 ns/op 58113796 B/op 3710943 allocs/op BenchmarkUploadFile/zero-10Mb-raw-ish_DoUploadFile-4 30 50777211 ns/op 54791929 B/op 2714 allocs/op BenchmarkUploadFile/zero-10Mb-raw_UploadFileTask-4 50 36387339 ns/op 10503920 B/op 126 allocs/op BenchmarkUploadFile/zero-10Mb-UploadFiles-4 30 48657678 ns/op 54791948 B/op 2719 allocs/op BenchmarkUploadFile/zero-10Mb-UploadFileTask-4 50 37506467 ns/op 31492060 B/op 131 allocs/op ... ``` https://mattermost.atlassian.net/browse/MM-7633 https://github.com/mattermost/mattermost-server/issues/7801 [Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fields.] - [x] Added or updated unit tests (required for all new features) - [ ] Added API documentation (required for all new APIs) - [ ] All new/modified APIs include changes to the drivers *N/A*??? - [x] Includes text changes and localization file ([.../i18n/en.json](https://github.com/mattermost/mattermost-server/blob/master/i18n/en.json)) updates Overview of changes: - api4 - Replaced `uploadFile` handler with `uploadFileStream` that reduces unnecessary buffering. - Added/refactored tests for the new API. - Refactored apitestlib/Check...Status functions. - app - Added App.UploadFileTask, a more efficient refactor of UploadFile. - Consistently set `FileInfo.HasPreviewImage` - Added benchmarks for the new and prior implementations - Replaced passing around `*image.Image` with `image.Image` in the existing code. - model - Added a more capable `client4.UploadFiles` API to match the new server API’s capabilities. - I18n - Replaced `api.file.upload_file.bad_parse.app_error` with a more generic `api.file.upload_file.read_request.app_error` - plugin - Removed type `plugin.multiPluginHookRunnerFunc` in favor of using `func(hooks Hooks) bool` explicitly, to help with testing - tests - Added test files for testing images Still remaining, but can be separate PRs - please let me know the preferred course of action - Investigate JS client API - how does it do multipart? - Performance loss from old code on (small) image processing? - Deprecate the old functions, change other API implementations to use UploadFileTask Definitely separate future PRs - should I file tickets foe these? - Only invoke t.readAll() if there are indeed applicable plugins to run - Find a way to leverage goexif buffer rather than re-reading Suggested long-term improvements - should I file separate tickets for these? - Actually allow uploading of large (GB-sized) files. This may require a change in how the file is passed to plugins. - (Many) api4 tests should probably be subtests and share a server setup - will be much faster - Performance improvements in image processing (goexif/thumbnail/preview) (maybe use https://mattermost.atlassian.net/browse/MM-10188 for this) Questions: 1. I am commiting MBs of test images, are there better alternatives? I can probably create much less dense images that would take up considerably less space, even at pretty large sizes 2. I18n: Do I need to do anything special for the string change? Or just wait until it gets picked up and translated/updated? 3. The image dimensions are flipped in resulting FileInfo to match the actual orientation. Is this by design? Should add a test for it, perhaps? 4. What to do in the case of partial success? S3 but not DB, some files but not others? For now, just doing what the old code did, I think. 5. Make maxUploadDrainBytes configurable? Also, should this be the systemic behavior of all APIs with non-empty body? Otherwise dropped altogether? Check all other ioutil.ReadAll() from sockets. Find a way to set a total byte limit on request Body? * WIP - Fixed for GetPluginsEnvironment() changes * WIP - PR feedback 1. Refactored checkHTTPStatus to improve failure messages 2. Use `var name []type` rather than `name := ([]type)(nil)` 3. Replaced single-letter `p` with a more intention-revealing `part` 4. Added tests for full image size, `HasPreviewImage` * WIP - rebased (c.Session->c.App.Session) * WIP - PR feedback: eliminated use of Request.MultipartReader Instead of hacking the request object to use r.MultipartReader now have own functions `parseMultipartRequestHeader` and `multipartReader` eliminating the need to hack the request object to use Request.MultipartReader limitations. * WIP - PR feedback: UploadFileX with functional options * WIP - PR feedback: style * WIP - PR feedback: errors cleanup * WIP - clarified File Upload benchmarks * WIP - PR feedback: display the value of erroneous formname * WIP - PR feedback: fixed handling of multiple channel_ids * WIP - rebased from master - fixed tests * PR Feedback * PR feedback - moved client4.UploadFiles to _test for now
2018-12-13 13:32:07 -08:00
return nil, &Response{Error: NewAppError("UploadPostAttachment", "model.client.upload_post_attachment.channel_id.app_error", nil, err.Error(), http.StatusBadRequest)}
}
MM-7633: Optimize memory utilization during file uploads (#9835) * MM-7633: Optimize memory utilization during file uploads Refactored the file upload code to reduce redundant buffering and stream directly to the file store. Added tests. Benchmark results: ``` levs-mbp:mattermost-server levb$ go test -v -run nothing -bench Upload -benchmem ./app ... BenchmarkUploadFile/random-5Mb-gif-raw-ish_DoUploadFile-4 10 122598031 ns/op 21211370 B/op 1008 allocs/op BenchmarkUploadFile/random-5Mb-gif-raw_UploadFileTask-4 100 20211926 ns/op 5678750 B/op 126 allocs/op BenchmarkUploadFile/random-5Mb-gif-UploadFiles-4 2 1037051184 ns/op 81806360 B/op 3705013 allocs/op BenchmarkUploadFile/random-5Mb-gif-UploadFileTask-4 2 933644431 ns/op 67015868 B/op 3704410 allocs/op BenchmarkUploadFile/random-2Mb-jpg-raw-ish_DoUploadFile-4 100 13110509 ns/op 6032614 B/op 8052 allocs/op BenchmarkUploadFile/random-2Mb-jpg-raw_UploadFileTask-4 100 10729867 ns/op 1738303 B/op 125 allocs/op BenchmarkUploadFile/random-2Mb-jpg-UploadFiles-4 2 925274912 ns/op 70326352 B/op 3718856 allocs/op BenchmarkUploadFile/random-2Mb-jpg-UploadFileTask-4 2 995033336 ns/op 58113796 B/op 3710943 allocs/op BenchmarkUploadFile/zero-10Mb-raw-ish_DoUploadFile-4 30 50777211 ns/op 54791929 B/op 2714 allocs/op BenchmarkUploadFile/zero-10Mb-raw_UploadFileTask-4 50 36387339 ns/op 10503920 B/op 126 allocs/op BenchmarkUploadFile/zero-10Mb-UploadFiles-4 30 48657678 ns/op 54791948 B/op 2719 allocs/op BenchmarkUploadFile/zero-10Mb-UploadFileTask-4 50 37506467 ns/op 31492060 B/op 131 allocs/op ... ``` https://mattermost.atlassian.net/browse/MM-7633 https://github.com/mattermost/mattermost-server/issues/7801 [Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fields.] - [x] Added or updated unit tests (required for all new features) - [ ] Added API documentation (required for all new APIs) - [ ] All new/modified APIs include changes to the drivers *N/A*??? - [x] Includes text changes and localization file ([.../i18n/en.json](https://github.com/mattermost/mattermost-server/blob/master/i18n/en.json)) updates Overview of changes: - api4 - Replaced `uploadFile` handler with `uploadFileStream` that reduces unnecessary buffering. - Added/refactored tests for the new API. - Refactored apitestlib/Check...Status functions. - app - Added App.UploadFileTask, a more efficient refactor of UploadFile. - Consistently set `FileInfo.HasPreviewImage` - Added benchmarks for the new and prior implementations - Replaced passing around `*image.Image` with `image.Image` in the existing code. - model - Added a more capable `client4.UploadFiles` API to match the new server API’s capabilities. - I18n - Replaced `api.file.upload_file.bad_parse.app_error` with a more generic `api.file.upload_file.read_request.app_error` - plugin - Removed type `plugin.multiPluginHookRunnerFunc` in favor of using `func(hooks Hooks) bool` explicitly, to help with testing - tests - Added test files for testing images Still remaining, but can be separate PRs - please let me know the preferred course of action - Investigate JS client API - how does it do multipart? - Performance loss from old code on (small) image processing? - Deprecate the old functions, change other API implementations to use UploadFileTask Definitely separate future PRs - should I file tickets foe these? - Only invoke t.readAll() if there are indeed applicable plugins to run - Find a way to leverage goexif buffer rather than re-reading Suggested long-term improvements - should I file separate tickets for these? - Actually allow uploading of large (GB-sized) files. This may require a change in how the file is passed to plugins. - (Many) api4 tests should probably be subtests and share a server setup - will be much faster - Performance improvements in image processing (goexif/thumbnail/preview) (maybe use https://mattermost.atlassian.net/browse/MM-10188 for this) Questions: 1. I am commiting MBs of test images, are there better alternatives? I can probably create much less dense images that would take up considerably less space, even at pretty large sizes 2. I18n: Do I need to do anything special for the string change? Or just wait until it gets picked up and translated/updated? 3. The image dimensions are flipped in resulting FileInfo to match the actual orientation. Is this by design? Should add a test for it, perhaps? 4. What to do in the case of partial success? S3 but not DB, some files but not others? For now, just doing what the old code did, I think. 5. Make maxUploadDrainBytes configurable? Also, should this be the systemic behavior of all APIs with non-empty body? Otherwise dropped altogether? Check all other ioutil.ReadAll() from sockets. Find a way to set a total byte limit on request Body? * WIP - Fixed for GetPluginsEnvironment() changes * WIP - PR feedback 1. Refactored checkHTTPStatus to improve failure messages 2. Use `var name []type` rather than `name := ([]type)(nil)` 3. Replaced single-letter `p` with a more intention-revealing `part` 4. Added tests for full image size, `HasPreviewImage` * WIP - rebased (c.Session->c.App.Session) * WIP - PR feedback: eliminated use of Request.MultipartReader Instead of hacking the request object to use r.MultipartReader now have own functions `parseMultipartRequestHeader` and `multipartReader` eliminating the need to hack the request object to use Request.MultipartReader limitations. * WIP - PR feedback: UploadFileX with functional options * WIP - PR feedback: style * WIP - PR feedback: errors cleanup * WIP - clarified File Upload benchmarks * WIP - PR feedback: display the value of erroneous formname * WIP - PR feedback: fixed handling of multiple channel_ids * WIP - rebased from master - fixed tests * PR Feedback * PR feedback - moved client4.UploadFiles to _test for now
2018-12-13 13:32:07 -08:00
_, err = io.Copy(part, strings.NewReader(channelId))
if err != nil {
return nil, &Response{Error: NewAppError("UploadPostAttachment", "model.client.upload_post_attachment.channel_id.app_error", nil, err.Error(), http.StatusBadRequest)}
}
MM-7633: Optimize memory utilization during file uploads (#9835) * MM-7633: Optimize memory utilization during file uploads Refactored the file upload code to reduce redundant buffering and stream directly to the file store. Added tests. Benchmark results: ``` levs-mbp:mattermost-server levb$ go test -v -run nothing -bench Upload -benchmem ./app ... BenchmarkUploadFile/random-5Mb-gif-raw-ish_DoUploadFile-4 10 122598031 ns/op 21211370 B/op 1008 allocs/op BenchmarkUploadFile/random-5Mb-gif-raw_UploadFileTask-4 100 20211926 ns/op 5678750 B/op 126 allocs/op BenchmarkUploadFile/random-5Mb-gif-UploadFiles-4 2 1037051184 ns/op 81806360 B/op 3705013 allocs/op BenchmarkUploadFile/random-5Mb-gif-UploadFileTask-4 2 933644431 ns/op 67015868 B/op 3704410 allocs/op BenchmarkUploadFile/random-2Mb-jpg-raw-ish_DoUploadFile-4 100 13110509 ns/op 6032614 B/op 8052 allocs/op BenchmarkUploadFile/random-2Mb-jpg-raw_UploadFileTask-4 100 10729867 ns/op 1738303 B/op 125 allocs/op BenchmarkUploadFile/random-2Mb-jpg-UploadFiles-4 2 925274912 ns/op 70326352 B/op 3718856 allocs/op BenchmarkUploadFile/random-2Mb-jpg-UploadFileTask-4 2 995033336 ns/op 58113796 B/op 3710943 allocs/op BenchmarkUploadFile/zero-10Mb-raw-ish_DoUploadFile-4 30 50777211 ns/op 54791929 B/op 2714 allocs/op BenchmarkUploadFile/zero-10Mb-raw_UploadFileTask-4 50 36387339 ns/op 10503920 B/op 126 allocs/op BenchmarkUploadFile/zero-10Mb-UploadFiles-4 30 48657678 ns/op 54791948 B/op 2719 allocs/op BenchmarkUploadFile/zero-10Mb-UploadFileTask-4 50 37506467 ns/op 31492060 B/op 131 allocs/op ... ``` https://mattermost.atlassian.net/browse/MM-7633 https://github.com/mattermost/mattermost-server/issues/7801 [Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fields.] - [x] Added or updated unit tests (required for all new features) - [ ] Added API documentation (required for all new APIs) - [ ] All new/modified APIs include changes to the drivers *N/A*??? - [x] Includes text changes and localization file ([.../i18n/en.json](https://github.com/mattermost/mattermost-server/blob/master/i18n/en.json)) updates Overview of changes: - api4 - Replaced `uploadFile` handler with `uploadFileStream` that reduces unnecessary buffering. - Added/refactored tests for the new API. - Refactored apitestlib/Check...Status functions. - app - Added App.UploadFileTask, a more efficient refactor of UploadFile. - Consistently set `FileInfo.HasPreviewImage` - Added benchmarks for the new and prior implementations - Replaced passing around `*image.Image` with `image.Image` in the existing code. - model - Added a more capable `client4.UploadFiles` API to match the new server API’s capabilities. - I18n - Replaced `api.file.upload_file.bad_parse.app_error` with a more generic `api.file.upload_file.read_request.app_error` - plugin - Removed type `plugin.multiPluginHookRunnerFunc` in favor of using `func(hooks Hooks) bool` explicitly, to help with testing - tests - Added test files for testing images Still remaining, but can be separate PRs - please let me know the preferred course of action - Investigate JS client API - how does it do multipart? - Performance loss from old code on (small) image processing? - Deprecate the old functions, change other API implementations to use UploadFileTask Definitely separate future PRs - should I file tickets foe these? - Only invoke t.readAll() if there are indeed applicable plugins to run - Find a way to leverage goexif buffer rather than re-reading Suggested long-term improvements - should I file separate tickets for these? - Actually allow uploading of large (GB-sized) files. This may require a change in how the file is passed to plugins. - (Many) api4 tests should probably be subtests and share a server setup - will be much faster - Performance improvements in image processing (goexif/thumbnail/preview) (maybe use https://mattermost.atlassian.net/browse/MM-10188 for this) Questions: 1. I am commiting MBs of test images, are there better alternatives? I can probably create much less dense images that would take up considerably less space, even at pretty large sizes 2. I18n: Do I need to do anything special for the string change? Or just wait until it gets picked up and translated/updated? 3. The image dimensions are flipped in resulting FileInfo to match the actual orientation. Is this by design? Should add a test for it, perhaps? 4. What to do in the case of partial success? S3 but not DB, some files but not others? For now, just doing what the old code did, I think. 5. Make maxUploadDrainBytes configurable? Also, should this be the systemic behavior of all APIs with non-empty body? Otherwise dropped altogether? Check all other ioutil.ReadAll() from sockets. Find a way to set a total byte limit on request Body? * WIP - Fixed for GetPluginsEnvironment() changes * WIP - PR feedback 1. Refactored checkHTTPStatus to improve failure messages 2. Use `var name []type` rather than `name := ([]type)(nil)` 3. Replaced single-letter `p` with a more intention-revealing `part` 4. Added tests for full image size, `HasPreviewImage` * WIP - rebased (c.Session->c.App.Session) * WIP - PR feedback: eliminated use of Request.MultipartReader Instead of hacking the request object to use r.MultipartReader now have own functions `parseMultipartRequestHeader` and `multipartReader` eliminating the need to hack the request object to use Request.MultipartReader limitations. * WIP - PR feedback: UploadFileX with functional options * WIP - PR feedback: style * WIP - PR feedback: errors cleanup * WIP - clarified File Upload benchmarks * WIP - PR feedback: display the value of erroneous formname * WIP - PR feedback: fixed handling of multiple channel_ids * WIP - rebased from master - fixed tests * PR Feedback * PR feedback - moved client4.UploadFiles to _test for now
2018-12-13 13:32:07 -08:00
part, err = writer.CreateFormFile("files", filename)
if err != nil {
return nil, &Response{Error: NewAppError("UploadPostAttachment", "model.client.upload_post_attachment.file.app_error", nil, err.Error(), http.StatusBadRequest)}
}
_, err = io.Copy(part, bytes.NewBuffer(data))
if err != nil {
return nil, &Response{Error: NewAppError("UploadPostAttachment", "model.client.upload_post_attachment.file.app_error", nil, err.Error(), http.StatusBadRequest)}
}
MM-7633: Optimize memory utilization during file uploads (#9835) * MM-7633: Optimize memory utilization during file uploads Refactored the file upload code to reduce redundant buffering and stream directly to the file store. Added tests. Benchmark results: ``` levs-mbp:mattermost-server levb$ go test -v -run nothing -bench Upload -benchmem ./app ... BenchmarkUploadFile/random-5Mb-gif-raw-ish_DoUploadFile-4 10 122598031 ns/op 21211370 B/op 1008 allocs/op BenchmarkUploadFile/random-5Mb-gif-raw_UploadFileTask-4 100 20211926 ns/op 5678750 B/op 126 allocs/op BenchmarkUploadFile/random-5Mb-gif-UploadFiles-4 2 1037051184 ns/op 81806360 B/op 3705013 allocs/op BenchmarkUploadFile/random-5Mb-gif-UploadFileTask-4 2 933644431 ns/op 67015868 B/op 3704410 allocs/op BenchmarkUploadFile/random-2Mb-jpg-raw-ish_DoUploadFile-4 100 13110509 ns/op 6032614 B/op 8052 allocs/op BenchmarkUploadFile/random-2Mb-jpg-raw_UploadFileTask-4 100 10729867 ns/op 1738303 B/op 125 allocs/op BenchmarkUploadFile/random-2Mb-jpg-UploadFiles-4 2 925274912 ns/op 70326352 B/op 3718856 allocs/op BenchmarkUploadFile/random-2Mb-jpg-UploadFileTask-4 2 995033336 ns/op 58113796 B/op 3710943 allocs/op BenchmarkUploadFile/zero-10Mb-raw-ish_DoUploadFile-4 30 50777211 ns/op 54791929 B/op 2714 allocs/op BenchmarkUploadFile/zero-10Mb-raw_UploadFileTask-4 50 36387339 ns/op 10503920 B/op 126 allocs/op BenchmarkUploadFile/zero-10Mb-UploadFiles-4 30 48657678 ns/op 54791948 B/op 2719 allocs/op BenchmarkUploadFile/zero-10Mb-UploadFileTask-4 50 37506467 ns/op 31492060 B/op 131 allocs/op ... ``` https://mattermost.atlassian.net/browse/MM-7633 https://github.com/mattermost/mattermost-server/issues/7801 [Place an '[x]' (no spaces) in all applicable fields. Please remove unrelated fields.] - [x] Added or updated unit tests (required for all new features) - [ ] Added API documentation (required for all new APIs) - [ ] All new/modified APIs include changes to the drivers *N/A*??? - [x] Includes text changes and localization file ([.../i18n/en.json](https://github.com/mattermost/mattermost-server/blob/master/i18n/en.json)) updates Overview of changes: - api4 - Replaced `uploadFile` handler with `uploadFileStream` that reduces unnecessary buffering. - Added/refactored tests for the new API. - Refactored apitestlib/Check...Status functions. - app - Added App.UploadFileTask, a more efficient refactor of UploadFile. - Consistently set `FileInfo.HasPreviewImage` - Added benchmarks for the new and prior implementations - Replaced passing around `*image.Image` with `image.Image` in the existing code. - model - Added a more capable `client4.UploadFiles` API to match the new server API’s capabilities. - I18n - Replaced `api.file.upload_file.bad_parse.app_error` with a more generic `api.file.upload_file.read_request.app_error` - plugin - Removed type `plugin.multiPluginHookRunnerFunc` in favor of using `func(hooks Hooks) bool` explicitly, to help with testing - tests - Added test files for testing images Still remaining, but can be separate PRs - please let me know the preferred course of action - Investigate JS client API - how does it do multipart? - Performance loss from old code on (small) image processing? - Deprecate the old functions, change other API implementations to use UploadFileTask Definitely separate future PRs - should I file tickets foe these? - Only invoke t.readAll() if there are indeed applicable plugins to run - Find a way to leverage goexif buffer rather than re-reading Suggested long-term improvements - should I file separate tickets for these? - Actually allow uploading of large (GB-sized) files. This may require a change in how the file is passed to plugins. - (Many) api4 tests should probably be subtests and share a server setup - will be much faster - Performance improvements in image processing (goexif/thumbnail/preview) (maybe use https://mattermost.atlassian.net/browse/MM-10188 for this) Questions: 1. I am commiting MBs of test images, are there better alternatives? I can probably create much less dense images that would take up considerably less space, even at pretty large sizes 2. I18n: Do I need to do anything special for the string change? Or just wait until it gets picked up and translated/updated? 3. The image dimensions are flipped in resulting FileInfo to match the actual orientation. Is this by design? Should add a test for it, perhaps? 4. What to do in the case of partial success? S3 but not DB, some files but not others? For now, just doing what the old code did, I think. 5. Make maxUploadDrainBytes configurable? Also, should this be the systemic behavior of all APIs with non-empty body? Otherwise dropped altogether? Check all other ioutil.ReadAll() from sockets. Find a way to set a total byte limit on request Body? * WIP - Fixed for GetPluginsEnvironment() changes * WIP - PR feedback 1. Refactored checkHTTPStatus to improve failure messages 2. Use `var name []type` rather than `name := ([]type)(nil)` 3. Replaced single-letter `p` with a more intention-revealing `part` 4. Added tests for full image size, `HasPreviewImage` * WIP - rebased (c.Session->c.App.Session) * WIP - PR feedback: eliminated use of Request.MultipartReader Instead of hacking the request object to use r.MultipartReader now have own functions `parseMultipartRequestHeader` and `multipartReader` eliminating the need to hack the request object to use Request.MultipartReader limitations. * WIP - PR feedback: UploadFileX with functional options * WIP - PR feedback: style * WIP - PR feedback: errors cleanup * WIP - clarified File Upload benchmarks * WIP - PR feedback: display the value of erroneous formname * WIP - PR feedback: fixed handling of multiple channel_ids * WIP - rebased from master - fixed tests * PR Feedback * PR feedback - moved client4.UploadFiles to _test for now
2018-12-13 13:32:07 -08:00
err = writer.Close()
if err != nil {
return nil, &Response{Error: NewAppError("UploadPostAttachment", "model.client.upload_post_attachment.writer.app_error", nil, err.Error(), http.StatusBadRequest)}
}
return c.DoUploadFile(c.GetFilesRoute(), body.Bytes(), writer.FormDataContentType())
}
// UploadFileAsRequestBody will upload a file to a channel as the body of a request, to be later attached
// to a post. This method is functionally equivalent to Client4.UploadFile.
func (c *Client4) UploadFileAsRequestBody(data []byte, channelId string, filename string) (*FileUploadResponse, *Response) {
return c.DoUploadFile(c.GetFilesRoute()+fmt.Sprintf("?channel_id=%v&filename=%v", url.QueryEscape(channelId), url.QueryEscape(filename)), data, http.DetectContentType(data))
}
// GetFile gets the bytes for a file by id.
func (c *Client4) GetFile(fileId string) ([]byte, *Response) {
r, appErr := c.DoApiGet(c.GetFileRoute(fileId), "")
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, BuildErrorResponse(r, NewAppError("GetFile", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode))
}
return data, BuildResponse(r)
}
// DownloadFile gets the bytes for a file by id, optionally adding headers to force the browser to download it.
func (c *Client4) DownloadFile(fileId string, download bool) ([]byte, *Response) {
r, appErr := c.DoApiGet(c.GetFileRoute(fileId)+fmt.Sprintf("?download=%v", download), "")
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, BuildErrorResponse(r, NewAppError("DownloadFile", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode))
}
return data, BuildResponse(r)
}
// GetFileThumbnail gets the bytes for a file by id.
func (c *Client4) GetFileThumbnail(fileId string) ([]byte, *Response) {
r, appErr := c.DoApiGet(c.GetFileRoute(fileId)+"/thumbnail", "")
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, BuildErrorResponse(r, NewAppError("GetFileThumbnail", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode))
}
return data, BuildResponse(r)
}
// DownloadFileThumbnail gets the bytes for a file by id, optionally adding headers to force the browser to download it.
func (c *Client4) DownloadFileThumbnail(fileId string, download bool) ([]byte, *Response) {
r, appErr := c.DoApiGet(c.GetFileRoute(fileId)+fmt.Sprintf("/thumbnail?download=%v", download), "")
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, BuildErrorResponse(r, NewAppError("DownloadFileThumbnail", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode))
}
return data, BuildResponse(r)
}
// GetFileLink gets the public link of a file by id.
func (c *Client4) GetFileLink(fileId string) (string, *Response) {
r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/link", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return "", BuildErrorResponse(r, err)
}
defer closeBody(r)
return MapFromJson(r.Body)["link"], BuildResponse(r)
}
// GetFilePreview gets the bytes for a file by id.
func (c *Client4) GetFilePreview(fileId string) ([]byte, *Response) {
r, appErr := c.DoApiGet(c.GetFileRoute(fileId)+"/preview", "")
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, BuildErrorResponse(r, NewAppError("GetFilePreview", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode))
}
return data, BuildResponse(r)
}
// DownloadFilePreview gets the bytes for a file by id.
func (c *Client4) DownloadFilePreview(fileId string, download bool) ([]byte, *Response) {
r, appErr := c.DoApiGet(c.GetFileRoute(fileId)+fmt.Sprintf("/preview?download=%v", download), "")
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, BuildErrorResponse(r, NewAppError("DownloadFilePreview", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode))
}
return data, BuildResponse(r)
}
// GetFileInfo gets all the file info objects.
func (c *Client4) GetFileInfo(fileId string) (*FileInfo, *Response) {
r, err := c.DoApiGet(c.GetFileRoute(fileId)+"/info", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return FileInfoFromJson(r.Body), BuildResponse(r)
}
// GetFileInfosForPost gets all the file info objects attached to a post.
func (c *Client4) GetFileInfosForPost(postId string, etag string) ([]*FileInfo, *Response) {
r, err := c.DoApiGet(c.GetPostRoute(postId)+"/files/info", etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return FileInfosFromJson(r.Body), BuildResponse(r)
}
// General/System Section
// GetPing will return ok if the running goRoutines are below the threshold and unhealthy for above.
func (c *Client4) GetPing() (string, *Response) {
r, err := c.DoApiGet(c.GetSystemRoute()+"/ping", "")
if r != nil && r.StatusCode == 500 {
defer r.Body.Close()
return STATUS_UNHEALTHY, BuildErrorResponse(r, err)
}
if err != nil {
return "", BuildErrorResponse(r, err)
}
defer closeBody(r)
return MapFromJson(r.Body)["status"], BuildResponse(r)
}
// GetPingWithServerStatus will return ok if several basic server health checks
// all pass successfully.
func (c *Client4) GetPingWithServerStatus() (string, *Response) {
r, err := c.DoApiGet(c.GetSystemRoute()+"/ping?get_server_status=true", "")
if r != nil && r.StatusCode == 500 {
defer r.Body.Close()
return STATUS_UNHEALTHY, BuildErrorResponse(r, err)
}
if err != nil {
2017-06-21 15:50:51 -04:00
return "", BuildErrorResponse(r, err)
}
defer closeBody(r)
return MapFromJson(r.Body)["status"], BuildResponse(r)
}
// TestEmail will attempt to connect to the configured SMTP server.
func (c *Client4) TestEmail(config *Config) (bool, *Response) {
r, err := c.DoApiPost(c.GetTestEmailRoute(), config.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// TestSiteURL will test the validity of a site URL.
func (c *Client4) TestSiteURL(siteURL string) (bool, *Response) {
requestBody := make(map[string]string)
requestBody["site_url"] = siteURL
r, err := c.DoApiPost(c.GetTestSiteURLRoute(), MapToJson(requestBody))
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// TestS3Connection will attempt to connect to the AWS S3.
func (c *Client4) TestS3Connection(config *Config) (bool, *Response) {
r, err := c.DoApiPost(c.GetTestS3Route(), config.ToJson())
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// GetConfig will retrieve the server config with some sanitized items.
func (c *Client4) GetConfig() (*Config, *Response) {
r, err := c.DoApiGet(c.GetConfigRoute(), "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ConfigFromJson(r.Body), BuildResponse(r)
}
// ReloadConfig will reload the server configuration.
func (c *Client4) ReloadConfig() (bool, *Response) {
r, err := c.DoApiPost(c.GetConfigRoute()+"/reload", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// GetOldClientConfig will retrieve the parts of the server configuration needed by the
// client, formatted in the old format.
func (c *Client4) GetOldClientConfig(etag string) (map[string]string, *Response) {
r, err := c.DoApiGet(c.GetConfigRoute()+"/client?format=old", etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return MapFromJson(r.Body), BuildResponse(r)
}
// GetEnvironmentConfig will retrieve a map mirroring the server configuration where fields
// are set to true if the corresponding config setting is set through an environment variable.
// Settings that haven't been set through environment variables will be missing from the map.
func (c *Client4) GetEnvironmentConfig() (map[string]interface{}, *Response) {
r, err := c.DoApiGet(c.GetConfigRoute()+"/environment", "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return StringInterfaceFromJson(r.Body), BuildResponse(r)
}
// GetOldClientLicense will retrieve the parts of the server license needed by the
// client, formatted in the old format.
func (c *Client4) GetOldClientLicense(etag string) (map[string]string, *Response) {
r, err := c.DoApiGet(c.GetLicenseRoute()+"/client?format=old", etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return MapFromJson(r.Body), BuildResponse(r)
}
// DatabaseRecycle will recycle the connections. Discard current connection and get new one.
func (c *Client4) DatabaseRecycle() (bool, *Response) {
r, err := c.DoApiPost(c.GetDatabaseRoute()+"/recycle", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// InvalidateCaches will purge the cache and can affect the performance while is cleaning.
func (c *Client4) InvalidateCaches() (bool, *Response) {
r, err := c.DoApiPost(c.GetCacheRoute()+"/invalidate", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// UpdateConfig will update the server configuration.
func (c *Client4) UpdateConfig(config *Config) (*Config, *Response) {
r, err := c.DoApiPut(c.GetConfigRoute(), config.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ConfigFromJson(r.Body), BuildResponse(r)
}
// UploadLicenseFile will add a license file to the system.
func (c *Client4) UploadLicenseFile(data []byte) (bool, *Response) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("license", "test-license.mattermost-license")
if err != nil {
return false, &Response{Error: NewAppError("UploadLicenseFile", "model.client.set_profile_user.no_file.app_error", nil, err.Error(), http.StatusBadRequest)}
}
if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil {
return false, &Response{Error: NewAppError("UploadLicenseFile", "model.client.set_profile_user.no_file.app_error", nil, err.Error(), http.StatusBadRequest)}
}
if err = writer.Close(); err != nil {
return false, &Response{Error: NewAppError("UploadLicenseFile", "model.client.set_profile_user.writer.app_error", nil, err.Error(), http.StatusBadRequest)}
}
rq, err := http.NewRequest("POST", c.ApiUrl+c.GetLicenseRoute(), bytes.NewReader(body.Bytes()))
if err != nil {
return false, &Response{Error: NewAppError("UploadLicenseFile", "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)}
}
rq.Header.Set("Content-Type", writer.FormDataContentType())
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
}
rp, err := c.HttpClient.Do(rq)
if err != nil || rp == nil {
return false, &Response{StatusCode: http.StatusForbidden, Error: NewAppError(c.GetLicenseRoute(), "model.client.connecting.app_error", nil, err.Error(), http.StatusForbidden)}
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return false, BuildErrorResponse(rp, AppErrorFromJson(rp.Body))
}
return CheckStatusOK(rp), BuildResponse(rp)
}
// RemoveLicenseFile will remove the server license it exists. Note that this will
// disable all enterprise features.
func (c *Client4) RemoveLicenseFile() (bool, *Response) {
r, err := c.DoApiDelete(c.GetLicenseRoute())
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// GetAnalyticsOld will retrieve analytics using the old format. New format is not
// available but the "/analytics" endpoint is reserved for it. The "name" argument is optional
// and defaults to "standard". The "teamId" argument is optional and will limit results
// to a specific team.
func (c *Client4) GetAnalyticsOld(name, teamId string) (AnalyticsRows, *Response) {
query := fmt.Sprintf("?name=%v&team_id=%v", name, teamId)
r, err := c.DoApiGet(c.GetAnalyticsRoute()+"/old"+query, "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return AnalyticsRowsFromJson(r.Body), BuildResponse(r)
}
// Webhooks Section
// CreateIncomingWebhook creates an incoming webhook for a channel.
func (c *Client4) CreateIncomingWebhook(hook *IncomingWebhook) (*IncomingWebhook, *Response) {
r, err := c.DoApiPost(c.GetIncomingWebhooksRoute(), hook.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return IncomingWebhookFromJson(r.Body), BuildResponse(r)
}
// UpdateIncomingWebhook updates an incoming webhook for a channel.
func (c *Client4) UpdateIncomingWebhook(hook *IncomingWebhook) (*IncomingWebhook, *Response) {
r, err := c.DoApiPut(c.GetIncomingWebhookRoute(hook.Id), hook.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return IncomingWebhookFromJson(r.Body), BuildResponse(r)
}
// GetIncomingWebhooks returns a page of incoming webhooks on the system. Page counting starts at 0.
func (c *Client4) GetIncomingWebhooks(page int, perPage int, etag string) ([]*IncomingWebhook, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetIncomingWebhooksRoute()+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return IncomingWebhookListFromJson(r.Body), BuildResponse(r)
}
// GetIncomingWebhooksForTeam returns a page of incoming webhooks for a team. Page counting starts at 0.
func (c *Client4) GetIncomingWebhooksForTeam(teamId string, page int, perPage int, etag string) ([]*IncomingWebhook, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v&team_id=%v", page, perPage, teamId)
r, err := c.DoApiGet(c.GetIncomingWebhooksRoute()+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return IncomingWebhookListFromJson(r.Body), BuildResponse(r)
}
// GetIncomingWebhook returns an Incoming webhook given the hook ID.
func (c *Client4) GetIncomingWebhook(hookID string, etag string) (*IncomingWebhook, *Response) {
r, err := c.DoApiGet(c.GetIncomingWebhookRoute(hookID), etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return IncomingWebhookFromJson(r.Body), BuildResponse(r)
}
// DeleteIncomingWebhook deletes and Incoming Webhook given the hook ID.
func (c *Client4) DeleteIncomingWebhook(hookID string) (bool, *Response) {
r, err := c.DoApiDelete(c.GetIncomingWebhookRoute(hookID))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// CreateOutgoingWebhook creates an outgoing webhook for a team or channel.
func (c *Client4) CreateOutgoingWebhook(hook *OutgoingWebhook) (*OutgoingWebhook, *Response) {
r, err := c.DoApiPost(c.GetOutgoingWebhooksRoute(), hook.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return OutgoingWebhookFromJson(r.Body), BuildResponse(r)
}
// UpdateOutgoingWebhook creates an outgoing webhook for a team or channel.
func (c *Client4) UpdateOutgoingWebhook(hook *OutgoingWebhook) (*OutgoingWebhook, *Response) {
r, err := c.DoApiPut(c.GetOutgoingWebhookRoute(hook.Id), hook.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return OutgoingWebhookFromJson(r.Body), BuildResponse(r)
}
// GetOutgoingWebhooks returns a page of outgoing webhooks on the system. Page counting starts at 0.
func (c *Client4) GetOutgoingWebhooks(page int, perPage int, etag string) ([]*OutgoingWebhook, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetOutgoingWebhooksRoute()+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return OutgoingWebhookListFromJson(r.Body), BuildResponse(r)
}
// GetOutgoingWebhook outgoing webhooks on the system requested by Hook Id.
func (c *Client4) GetOutgoingWebhook(hookId string) (*OutgoingWebhook, *Response) {
r, err := c.DoApiGet(c.GetOutgoingWebhookRoute(hookId), "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return OutgoingWebhookFromJson(r.Body), BuildResponse(r)
}
// GetOutgoingWebhooksForChannel returns a page of outgoing webhooks for a channel. Page counting starts at 0.
func (c *Client4) GetOutgoingWebhooksForChannel(channelId string, page int, perPage int, etag string) ([]*OutgoingWebhook, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v&channel_id=%v", page, perPage, channelId)
r, err := c.DoApiGet(c.GetOutgoingWebhooksRoute()+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return OutgoingWebhookListFromJson(r.Body), BuildResponse(r)
}
// GetOutgoingWebhooksForTeam returns a page of outgoing webhooks for a team. Page counting starts at 0.
func (c *Client4) GetOutgoingWebhooksForTeam(teamId string, page int, perPage int, etag string) ([]*OutgoingWebhook, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v&team_id=%v", page, perPage, teamId)
r, err := c.DoApiGet(c.GetOutgoingWebhooksRoute()+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return OutgoingWebhookListFromJson(r.Body), BuildResponse(r)
}
// RegenOutgoingHookToken regenerate the outgoing webhook token.
func (c *Client4) RegenOutgoingHookToken(hookId string) (*OutgoingWebhook, *Response) {
r, err := c.DoApiPost(c.GetOutgoingWebhookRoute(hookId)+"/regen_token", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return OutgoingWebhookFromJson(r.Body), BuildResponse(r)
}
// DeleteOutgoingWebhook delete the outgoing webhook on the system requested by Hook Id.
func (c *Client4) DeleteOutgoingWebhook(hookId string) (bool, *Response) {
r, err := c.DoApiDelete(c.GetOutgoingWebhookRoute(hookId))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// Preferences Section
// GetPreferences returns the user's preferences.
func (c *Client4) GetPreferences(userId string) (Preferences, *Response) {
r, err := c.DoApiGet(c.GetPreferencesRoute(userId), "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
preferences, _ := PreferencesFromJson(r.Body)
return preferences, BuildResponse(r)
}
// UpdatePreferences saves the user's preferences.
func (c *Client4) UpdatePreferences(userId string, preferences *Preferences) (bool, *Response) {
r, err := c.DoApiPut(c.GetPreferencesRoute(userId), preferences.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return true, BuildResponse(r)
}
// DeletePreferences deletes the user's preferences.
func (c *Client4) DeletePreferences(userId string, preferences *Preferences) (bool, *Response) {
r, err := c.DoApiPost(c.GetPreferencesRoute(userId)+"/delete", preferences.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return true, BuildResponse(r)
}
// GetPreferencesByCategory returns the user's preferences from the provided category string.
func (c *Client4) GetPreferencesByCategory(userId string, category string) (Preferences, *Response) {
url := fmt.Sprintf(c.GetPreferencesRoute(userId)+"/%s", category)
r, err := c.DoApiGet(url, "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
preferences, _ := PreferencesFromJson(r.Body)
return preferences, BuildResponse(r)
}
// GetPreferenceByCategoryAndName returns the user's preferences from the provided category and preference name string.
func (c *Client4) GetPreferenceByCategoryAndName(userId string, category string, preferenceName string) (*Preference, *Response) {
url := fmt.Sprintf(c.GetPreferencesRoute(userId)+"/%s/name/%v", category, preferenceName)
r, err := c.DoApiGet(url, "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return PreferenceFromJson(r.Body), BuildResponse(r)
}
// SAML Section
// GetSamlMetadata returns metadata for the SAML configuration.
func (c *Client4) GetSamlMetadata() (string, *Response) {
r, err := c.DoApiGet(c.GetSamlRoute()+"/metadata", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return "", BuildErrorResponse(r, err)
}
defer closeBody(r)
buf := new(bytes.Buffer)
_, _ = buf.ReadFrom(r.Body)
return buf.String(), BuildResponse(r)
}
func samlFileToMultipart(data []byte, filename string) ([]byte, *multipart.Writer, error) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("certificate", filename)
if err != nil {
return nil, nil, err
}
if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil {
return nil, nil, err
}
if err := writer.Close(); err != nil {
return nil, nil, err
}
return body.Bytes(), writer, nil
}
// UploadSamlIdpCertificate will upload an IDP certificate for SAML and set the config to use it.
// The filename parameter is deprecated and ignored: the server will pick a hard-coded filename when writing to disk.
func (c *Client4) UploadSamlIdpCertificate(data []byte, filename string) (bool, *Response) {
body, writer, err := samlFileToMultipart(data, filename)
if err != nil {
return false, &Response{Error: NewAppError("UploadSamlIdpCertificate", "model.client.upload_saml_cert.app_error", nil, err.Error(), http.StatusBadRequest)}
}
_, resp := c.DoUploadFile(c.GetSamlRoute()+"/certificate/idp", body, writer.FormDataContentType())
return resp.Error == nil, resp
}
// UploadSamlPublicCertificate will upload a public certificate for SAML and set the config to use it.
// The filename parameter is deprecated and ignored: the server will pick a hard-coded filename when writing to disk.
func (c *Client4) UploadSamlPublicCertificate(data []byte, filename string) (bool, *Response) {
body, writer, err := samlFileToMultipart(data, filename)
if err != nil {
return false, &Response{Error: NewAppError("UploadSamlPublicCertificate", "model.client.upload_saml_cert.app_error", nil, err.Error(), http.StatusBadRequest)}
}
_, resp := c.DoUploadFile(c.GetSamlRoute()+"/certificate/public", body, writer.FormDataContentType())
return resp.Error == nil, resp
}
// UploadSamlPrivateCertificate will upload a private key for SAML and set the config to use it.
// The filename parameter is deprecated and ignored: the server will pick a hard-coded filename when writing to disk.
func (c *Client4) UploadSamlPrivateCertificate(data []byte, filename string) (bool, *Response) {
body, writer, err := samlFileToMultipart(data, filename)
if err != nil {
return false, &Response{Error: NewAppError("UploadSamlPrivateCertificate", "model.client.upload_saml_cert.app_error", nil, err.Error(), http.StatusBadRequest)}
}
_, resp := c.DoUploadFile(c.GetSamlRoute()+"/certificate/private", body, writer.FormDataContentType())
return resp.Error == nil, resp
}
// DeleteSamlIdpCertificate deletes the SAML IDP certificate from the server and updates the config to not use it and disable SAML.
func (c *Client4) DeleteSamlIdpCertificate() (bool, *Response) {
r, err := c.DoApiDelete(c.GetSamlRoute() + "/certificate/idp")
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// DeleteSamlPublicCertificate deletes the SAML IDP certificate from the server and updates the config to not use it and disable SAML.
func (c *Client4) DeleteSamlPublicCertificate() (bool, *Response) {
r, err := c.DoApiDelete(c.GetSamlRoute() + "/certificate/public")
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// DeleteSamlPrivateCertificate deletes the SAML IDP certificate from the server and updates the config to not use it and disable SAML.
func (c *Client4) DeleteSamlPrivateCertificate() (bool, *Response) {
r, err := c.DoApiDelete(c.GetSamlRoute() + "/certificate/private")
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// GetSamlCertificateStatus returns metadata for the SAML configuration.
func (c *Client4) GetSamlCertificateStatus() (*SamlCertificateStatus, *Response) {
r, err := c.DoApiGet(c.GetSamlRoute()+"/certificate/status", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return SamlCertificateStatusFromJson(r.Body), BuildResponse(r)
}
// Compliance Section
// CreateComplianceReport creates an incoming webhook for a channel.
func (c *Client4) CreateComplianceReport(report *Compliance) (*Compliance, *Response) {
r, err := c.DoApiPost(c.GetComplianceReportsRoute(), report.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ComplianceFromJson(r.Body), BuildResponse(r)
}
// GetComplianceReports returns list of compliance reports.
func (c *Client4) GetComplianceReports(page, perPage int) (Compliances, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetComplianceReportsRoute()+query, "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CompliancesFromJson(r.Body), BuildResponse(r)
}
// GetComplianceReport returns a compliance report.
func (c *Client4) GetComplianceReport(reportId string) (*Compliance, *Response) {
r, err := c.DoApiGet(c.GetComplianceReportRoute(reportId), "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ComplianceFromJson(r.Body), BuildResponse(r)
}
// DownloadComplianceReport returns a full compliance report as a file.
func (c *Client4) DownloadComplianceReport(reportId string) ([]byte, *Response) {
rq, err := http.NewRequest("GET", c.ApiUrl+c.GetComplianceReportRoute(reportId), nil)
if err != nil {
return nil, &Response{Error: NewAppError("DownloadComplianceReport", "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)}
}
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
}
rp, err := c.HttpClient.Do(rq)
if err != nil || rp == nil {
return nil, &Response{Error: NewAppError("DownloadComplianceReport", "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)}
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return nil, BuildErrorResponse(rp, AppErrorFromJson(rp.Body))
}
data, err := ioutil.ReadAll(rp.Body)
if err != nil {
return nil, BuildErrorResponse(rp, NewAppError("DownloadComplianceReport", "model.client.read_file.app_error", nil, err.Error(), rp.StatusCode))
}
return data, BuildResponse(rp)
}
// Cluster Section
// GetClusterStatus returns the status of all the configured cluster nodes.
func (c *Client4) GetClusterStatus() ([]*ClusterInfo, *Response) {
r, err := c.DoApiGet(c.GetClusterRoute()+"/status", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ClusterInfosFromJson(r.Body), BuildResponse(r)
}
// LDAP Section
// SyncLdap will force a sync with the configured LDAP server.
func (c *Client4) SyncLdap() (bool, *Response) {
r, err := c.DoApiPost(c.GetLdapRoute()+"/sync", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// TestLdap will attempt to connect to the configured LDAP server and return OK if configured
// correctly.
func (c *Client4) TestLdap() (bool, *Response) {
r, err := c.DoApiPost(c.GetLdapRoute()+"/test", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
Ldap groups phase1 (#9752) * Initial models, API, app, and persistence of groups and group syncing. * Consistent letter casing in ldif. * Moves group-specific migrations into func. * Adds API endpoint to retrieve LDAP groups (and associated MM groups) one tree level at a time. * Adds mattermost group id to SCIMGroup (if available). * Splits user and group creation so that memberOf works. Returns users from ldap interface. * Updates method name. * Returns users IDs instead of User. * Removes non-essential group data. * MM-11807: Add GroupFilter to LDAP config. (#9513) * MM-11807: Add GroupFilter to LDAP config. * Add diagnostic. * Adds new config option for using 'memberOf' overlay. * Adds API endpoint to link a group. * Removes debug statements. * Adds unlink group API endpoint. * Fix to LDAP API. Adds API method to client4 and app. * Adds some missing app methods. Renames API unexported func. * Fixes link/unlink API path to accept valid DNs. * Allow any character for DN portion of path. * Switches from DN to objectGUID or entryUUID as the remote identifier linking LDAP groups to MM groups. * Formatting. * Formatting. * Setting group name field to an ID for phase 1. * Adds an LDAP config field to Setting up configuration for local LDAP. * Changes to LDAP and GroupStore interfaces. * Draft of nesting groups in API response. * Removes unnecessary tree models. * Updates group membershipt create store method to also restore. * Adds new config to test config. * Accept AD format length. * Switches to SetUniqueTogether method. * Updates revert. * Tweaks to syncing queries . * Updates query for pending team and channel memberships. * Removes old GroupSyncableScanner usage. Some formatting and renaming. * Fixes bug setting syncable type in selecting paged. * Adds tests for syncables populator. * Only add users to teams and channels that are not deleted. * Renames method. * Updates test LDAP setup. * Removes memberof config stuff. * Renames. * Updates test data. * Fix for gofmt. * Adds missing license. * Adds missing teardowns. * Test fix. * Adds a cycle to the groups test data. * Changes API to return flat list. * Removes some unused interface and app methods. * Returns empty braces if results are empty. * Adds more LDAP test data. * Fix for test data error. * Adds error. * Moves test groups. * Adds OU for load test data. * Moves load test ou creation to load data. * Adds a new bool flag to SCIMGroups. * Removes SCIMGroup completely. * Removes FULL JOIN because it is not supported in MySQL. * Adds tests for sync queries; renames constant. * Bad merge fix. * Vet fix. * Returning OK on delete ldap group link * Removes foreign key constraints. * Adding total to the ldap getAllGroups api endpoint * Adds get group members page. * Removes pagination from groups syncables list API. * Adding syncable check now that foreign key constraint is removes. * Joins teams and channels to group syncables. * Adds group member count. * Adding GetAllChannels and SearchAllChannels for system admins only * Fix. * Test fix from pagination removal. * Orders groupmembers by createat. * Fixing search of all channels * Test fix after removing pagination. * JSON syntax error fix. * Changing tests (for now) pending investigation. * Adding GetAllChannels and SearchAllChannels tests for the store * Adding GetAllChannels and SearchAllChannels API tests * Omit empty JSON values of group syncables. * Fixing GetAllChannels and SearchAllChannels tests * Fixing GetAllChannels and SearchAllChannels store tests * Fixing GetAllChannels api tests * Adds 'LDAP groups' feature flag. (#9861) * Migrate new client functions to idiomatic error handling * Test fixes. * Simplification of groups api (#9860) * Simplification of groups api * Fixing RequireSyncableType * Test fix. * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Fix copy/paste error. * Fix copy/paste error. * Adds missing return, changes to correct HTTP status code. * Adds missing return, changes status codes. * Check for license. * Renames variable for new signature. * Adds client method to get a group. * Adds client method and tests for PatchGroup. * Adds more API tests. * Adds groups API tests. * Adds client method and tests for getting group syncables. * Adds tests for patching group teams and channels. * Update to translations. * Removes test. * Fix incorrect conditional. * Removes unnecessary nil check. * Removes unnecessary return. * Updates comment, removes unused variable. * Uses consistent JSON unmarshal pattern. * Uses consistent JSON unmarshal pattern. * Moves const block. * Switches 'already linked' from error to success response. * Removes commented-out code. * Switched to status ok. * Add parens for readability. * Fix copy/paste error. * Unexport some structs. * Removes repeated validity check. * Return without attempting commit if there's a rollback. * Fix incorrect HTTP status code. * Update store/sqlstore/group_supplier.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Adds utility methods for going from groupsyncable to groupteam and groupchannel. * Fixing george suggestions (#9911) * Test fix. * Adds QA data to VC with visualization. * Fixes typo in graph image. * Update display name when re-linking in case it has changed in LDAP. * Adds ability to configure group display name and unique identifier. (#9923) * Adds ability to configure group display name and unique identifier. * Adds some configs to confi-ldap make command. * Fix for move of session. * Exposes method for use by SAML package. * Switches GroupSyncableType from int to string. * Update Jenkins build files. * Removes unused variable assignment. * Removes old unnecessary early return. * Removes unnecessary variable. * Moves param parsing before license and permissions checks. * Removes old code. * Compares agains underlying error rather than error id. * Switches tests to assertions. * Adds more assertions. * Adds missing return. * Adds space after comma for added legibility. * Moves a view model to the api package. * Unexports method. * Uses id validator function. * Fix docker-compose flag. * Typo fix. * Moves index creation to supplier. * Removes bad merge. * Renames parameter. * Re-adds space. * Removes unnecessary transaction. * Escapes the Groups table name with backticks because it is a reserved keyword. * Fix roles cache bug * Removing unnecesiary deserializing function * Switches table name rather than custom SQL everywhere for Postgres without backticks. * Removes redundant check for sql.ErrNoRows. * Removes redundant check for sql.ErrNoRows. * Removes data integrity check and redundant nil conditional. * Removes redundant check for sql.ErrNoRows. * Removes unnecessary query. * Removes ID length validation from persistence tier. * Makes some supplier methods idempotent. * Removes some empty switch defaults. * Renames Group Type field to Source. * Fix for mistaken field name change. * Uses IsValidId function. * Removes comment. * Changes json key name. * Removes test because no longer validating user. * Moves model state validation to app layer. * Don't create Groups.CanLeave column until phase 2. * Removes state validation until properties are used in phase 2. * Removes duplicated check. * Removes state validation until properties are used in phase 2. * Removes some tests until phase 2. * Comment-out a bunch of test related to CanLeave. * Extra unmarshal validation check. Removes more code for CanLeave. * Removes tests for CanLeave. * Explict error msg. * Rewrite queries. * Changes index name. Adds index. * Removes assertion. * Adds experimental feature flag.
2019-01-10 15:17:31 -05:00
// GetLdapGroups retrieves the immediate child groups of the given parent group.
func (c *Client4) GetLdapGroups() ([]*Group, *Response) {
path := fmt.Sprintf("%s/groups", c.GetLdapRoute())
r, appErr := c.DoApiGet(path, "")
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
return GroupsFromJson(r.Body), BuildResponse(r)
}
// LinkLdapGroup creates or undeletes a Mattermost group and associates it to the given LDAP group DN.
func (c *Client4) LinkLdapGroup(dn string) (*Group, *Response) {
path := fmt.Sprintf("%s/groups/%s/link", c.GetLdapRoute(), dn)
r, appErr := c.DoApiPost(path, "")
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
return GroupFromJson(r.Body), BuildResponse(r)
}
// UnlinkLdapGroup deletes the Mattermost group associated with the given LDAP group DN.
func (c *Client4) UnlinkLdapGroup(dn string) (*Group, *Response) {
path := fmt.Sprintf("%s/groups/%s/link", c.GetLdapRoute(), dn)
r, appErr := c.DoApiDelete(path)
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
return GroupFromJson(r.Body), BuildResponse(r)
}
// GetGroupsByChannel retrieves the Mattermost Groups associated with a given channel
func (c *Client4) GetGroupsByChannel(channelId string, opts GroupSearchOpts) ([]*Group, int, *Response) {
path := fmt.Sprintf("%s/groups?q=%v&include_member_count=%v", c.GetChannelRoute(channelId), opts.Q, opts.IncludeMemberCount)
if opts.PageOpts != nil {
path = fmt.Sprintf("%s&page=%v&per_page=%v", path, opts.PageOpts.Page, opts.PageOpts.PerPage)
}
r, appErr := c.DoApiGet(path, "")
if appErr != nil {
return nil, 0, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
responseData := struct {
Groups []*Group `json:"groups"`
Count int `json:"total_group_count"`
}{}
if err := json.NewDecoder(r.Body).Decode(&responseData); err != nil {
appErr := NewAppError("Api4.GetGroupsByChannel", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return nil, 0, BuildErrorResponse(r, appErr)
}
return responseData.Groups, responseData.Count, BuildResponse(r)
}
// GetGroupsByTeam retrieves the Mattermost Groups associated with a given team
func (c *Client4) GetGroupsByTeam(teamId string, opts GroupSearchOpts) ([]*Group, int, *Response) {
path := fmt.Sprintf("%s/groups?q=%v&include_member_count=%v", c.GetTeamRoute(teamId), opts.Q, opts.IncludeMemberCount)
if opts.PageOpts != nil {
path = fmt.Sprintf("%s&page=%v&per_page=%v", path, opts.PageOpts.Page, opts.PageOpts.PerPage)
}
r, appErr := c.DoApiGet(path, "")
if appErr != nil {
return nil, 0, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
responseData := struct {
Groups []*Group `json:"groups"`
Count int `json:"total_group_count"`
}{}
if err := json.NewDecoder(r.Body).Decode(&responseData); err != nil {
appErr := NewAppError("Api4.GetGroupsByTeam", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
return nil, 0, BuildErrorResponse(r, appErr)
}
return responseData.Groups, responseData.Count, BuildResponse(r)
}
// GetGroups retrieves Mattermost Groups
func (c *Client4) GetGroups(opts GroupSearchOpts) ([]*Group, *Response) {
path := fmt.Sprintf(
"%s?include_member_count=%v&not_associated_to_team=%v&not_associated_to_channel=%v&q=%v",
c.GetGroupsRoute(),
opts.IncludeMemberCount,
opts.NotAssociatedToTeam,
opts.NotAssociatedToChannel,
opts.Q,
)
if opts.PageOpts != nil {
path = fmt.Sprintf("%s&page=%v&per_page=%v", path, opts.PageOpts.Page, opts.PageOpts.PerPage)
}
r, appErr := c.DoApiGet(path, "")
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
return GroupsFromJson(r.Body), BuildResponse(r)
}
// Audits Section
// GetAudits returns a list of audits for the whole system.
func (c *Client4) GetAudits(page int, perPage int, etag string) (Audits, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet("/audits"+query, etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return AuditsFromJson(r.Body), BuildResponse(r)
}
// Brand Section
// GetBrandImage retrieves the previously uploaded brand image.
func (c *Client4) GetBrandImage() ([]byte, *Response) {
r, appErr := c.DoApiGet(c.GetBrandRoute()+"/image", "")
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
if r.StatusCode >= 300 {
return nil, BuildErrorResponse(r, AppErrorFromJson(r.Body))
}
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, BuildErrorResponse(r, NewAppError("GetBrandImage", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode))
}
return data, BuildResponse(r)
}
// DeleteBrandImage deletes the brand image for the system.
func (c *Client4) DeleteBrandImage() *Response {
r, err := c.DoApiDelete(c.GetBrandRoute() + "/image")
if err != nil {
return BuildErrorResponse(r, err)
}
return BuildResponse(r)
}
// UploadBrandImage sets the brand image for the system.
func (c *Client4) UploadBrandImage(data []byte) (bool, *Response) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("image", "brand.png")
if err != nil {
return false, &Response{Error: NewAppError("UploadBrandImage", "model.client.set_profile_user.no_file.app_error", nil, err.Error(), http.StatusBadRequest)}
}
if _, err = io.Copy(part, bytes.NewBuffer(data)); err != nil {
return false, &Response{Error: NewAppError("UploadBrandImage", "model.client.set_profile_user.no_file.app_error", nil, err.Error(), http.StatusBadRequest)}
}
if err = writer.Close(); err != nil {
return false, &Response{Error: NewAppError("UploadBrandImage", "model.client.set_profile_user.writer.app_error", nil, err.Error(), http.StatusBadRequest)}
}
rq, err := http.NewRequest("POST", c.ApiUrl+c.GetBrandRoute()+"/image", bytes.NewReader(body.Bytes()))
if err != nil {
return false, &Response{Error: NewAppError("UploadBrandImage", "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)}
}
rq.Header.Set("Content-Type", writer.FormDataContentType())
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
}
rp, err := c.HttpClient.Do(rq)
if err != nil || rp == nil {
return false, &Response{StatusCode: http.StatusForbidden, Error: NewAppError(c.GetBrandRoute()+"/image", "model.client.connecting.app_error", nil, err.Error(), http.StatusForbidden)}
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return false, BuildErrorResponse(rp, AppErrorFromJson(rp.Body))
}
return CheckStatusOK(rp), BuildResponse(rp)
}
// Logs Section
// GetLogs page of logs as a string array.
func (c *Client4) GetLogs(page, perPage int) ([]string, *Response) {
query := fmt.Sprintf("?page=%v&logs_per_page=%v", page, perPage)
r, err := c.DoApiGet("/logs"+query, "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ArrayFromJson(r.Body), BuildResponse(r)
}
2017-04-21 11:16:35 +02:00
// PostLog is a convenience Web Service call so clients can log messages into
// the server-side logs. For example we typically log javascript error messages
// into the server-side. It returns the log message if the logging was successful.
2017-04-21 11:16:35 +02:00
func (c *Client4) PostLog(message map[string]string) (map[string]string, *Response) {
r, err := c.DoApiPost("/logs", MapToJson(message))
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
2017-04-21 11:16:35 +02:00
}
defer closeBody(r)
return MapFromJson(r.Body), BuildResponse(r)
2017-04-21 11:16:35 +02:00
}
// OAuth Section
// CreateOAuthApp will register a new OAuth 2.0 client application with Mattermost acting as an OAuth 2.0 service provider.
func (c *Client4) CreateOAuthApp(app *OAuthApp) (*OAuthApp, *Response) {
r, err := c.DoApiPost(c.GetOAuthAppsRoute(), app.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return OAuthAppFromJson(r.Body), BuildResponse(r)
}
// UpdateOAuthApp updates a page of registered OAuth 2.0 client applications with Mattermost acting as an OAuth 2.0 service provider.
func (c *Client4) UpdateOAuthApp(app *OAuthApp) (*OAuthApp, *Response) {
r, err := c.DoApiPut(c.GetOAuthAppRoute(app.Id), app.ToJson())
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return OAuthAppFromJson(r.Body), BuildResponse(r)
}
// GetOAuthApps gets a page of registered OAuth 2.0 client applications with Mattermost acting as an OAuth 2.0 service provider.
func (c *Client4) GetOAuthApps(page, perPage int) ([]*OAuthApp, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetOAuthAppsRoute()+query, "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return OAuthAppListFromJson(r.Body), BuildResponse(r)
}
// GetOAuthApp gets a registered OAuth 2.0 client application with Mattermost acting as an OAuth 2.0 service provider.
func (c *Client4) GetOAuthApp(appId string) (*OAuthApp, *Response) {
r, err := c.DoApiGet(c.GetOAuthAppRoute(appId), "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return OAuthAppFromJson(r.Body), BuildResponse(r)
}
// GetOAuthAppInfo gets a sanitized version of a registered OAuth 2.0 client application with Mattermost acting as an OAuth 2.0 service provider.
func (c *Client4) GetOAuthAppInfo(appId string) (*OAuthApp, *Response) {
r, err := c.DoApiGet(c.GetOAuthAppRoute(appId)+"/info", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return OAuthAppFromJson(r.Body), BuildResponse(r)
}
// DeleteOAuthApp deletes a registered OAuth 2.0 client application.
func (c *Client4) DeleteOAuthApp(appId string) (bool, *Response) {
r, err := c.DoApiDelete(c.GetOAuthAppRoute(appId))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// RegenerateOAuthAppSecret regenerates the client secret for a registered OAuth 2.0 client application.
func (c *Client4) RegenerateOAuthAppSecret(appId string) (*OAuthApp, *Response) {
r, err := c.DoApiPost(c.GetOAuthAppRoute(appId)+"/regen_secret", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return OAuthAppFromJson(r.Body), BuildResponse(r)
}
// GetAuthorizedOAuthAppsForUser gets a page of OAuth 2.0 client applications the user has authorized to use access their account.
func (c *Client4) GetAuthorizedOAuthAppsForUser(userId string, page, perPage int) ([]*OAuthApp, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetUserRoute(userId)+"/oauth/apps/authorized"+query, "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return OAuthAppListFromJson(r.Body), BuildResponse(r)
}
// AuthorizeOAuthApp will authorize an OAuth 2.0 client application to access a user's account and provide a redirect link to follow.
func (c *Client4) AuthorizeOAuthApp(authRequest *AuthorizeRequest) (string, *Response) {
r, err := c.DoApiRequest(http.MethodPost, c.Url+"/oauth/authorize", authRequest.ToJson(), "")
if err != nil {
2017-06-21 15:50:51 -04:00
return "", BuildErrorResponse(r, err)
}
defer closeBody(r)
return MapFromJson(r.Body)["redirect"], BuildResponse(r)
}
// DeauthorizeOAuthApp will deauthorize an OAuth 2.0 client application from accessing a user's account.
func (c *Client4) DeauthorizeOAuthApp(appId string) (bool, *Response) {
requestData := map[string]string{"client_id": appId}
r, err := c.DoApiRequest(http.MethodPost, c.Url+"/oauth/deauthorize", MapToJson(requestData), "")
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// GetOAuthAccessToken is a test helper function for the OAuth access token endpoint.
func (c *Client4) GetOAuthAccessToken(data url.Values) (*AccessResponse, *Response) {
rq, err := http.NewRequest(http.MethodPost, c.Url+"/oauth/access_token", strings.NewReader(data.Encode()))
if err != nil {
return nil, &Response{Error: NewAppError(c.Url+"/oauth/access_token", "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)}
}
rq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
}
rp, err := c.HttpClient.Do(rq)
if err != nil || rp == nil {
return nil, &Response{StatusCode: http.StatusForbidden, Error: NewAppError(c.Url+"/oauth/access_token", "model.client.connecting.app_error", nil, err.Error(), 403)}
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return nil, BuildErrorResponse(rp, AppErrorFromJson(rp.Body))
}
return AccessResponseFromJson(rp.Body), BuildResponse(rp)
}
// Elasticsearch Section
// TestElasticsearch will attempt to connect to the configured Elasticsearch server and return OK if configured.
// correctly.
func (c *Client4) TestElasticsearch() (bool, *Response) {
r, err := c.DoApiPost(c.GetElasticsearchRoute()+"/test", "")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// PurgeElasticsearchIndexes immediately deletes all Elasticsearch indexes.
func (c *Client4) PurgeElasticsearchIndexes() (bool, *Response) {
r, err := c.DoApiPost(c.GetElasticsearchRoute()+"/purge_indexes", "")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// Data Retention Section
// GetDataRetentionPolicy will get the current server data retention policy details.
func (c *Client4) GetDataRetentionPolicy() (*DataRetentionPolicy, *Response) {
r, err := c.DoApiGet(c.GetDataRetentionRoute()+"/policy", "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return DataRetentionPolicyFromJson(r.Body), BuildResponse(r)
}
// Commands Section
// CreateCommand will create a new command if the user have the right permissions.
func (c *Client4) CreateCommand(cmd *Command) (*Command, *Response) {
r, err := c.DoApiPost(c.GetCommandsRoute(), cmd.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CommandFromJson(r.Body), BuildResponse(r)
}
// UpdateCommand updates a command based on the provided Command struct.
func (c *Client4) UpdateCommand(cmd *Command) (*Command, *Response) {
r, err := c.DoApiPut(c.GetCommandRoute(cmd.Id), cmd.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CommandFromJson(r.Body), BuildResponse(r)
}
// DeleteCommand deletes a command based on the provided command id string.
func (c *Client4) DeleteCommand(commandId string) (bool, *Response) {
r, err := c.DoApiDelete(c.GetCommandRoute(commandId))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
2017-04-03 14:12:50 +02:00
// ListCommands will retrieve a list of commands available in the team.
func (c *Client4) ListCommands(teamId string, customOnly bool) ([]*Command, *Response) {
query := fmt.Sprintf("?team_id=%v&custom_only=%v", teamId, customOnly)
r, err := c.DoApiGet(c.GetCommandsRoute()+query, "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
2017-04-03 14:12:50 +02:00
}
defer closeBody(r)
return CommandListFromJson(r.Body), BuildResponse(r)
2017-04-03 14:12:50 +02:00
}
// ExecuteCommand executes a given slash command.
2017-04-27 22:41:52 +09:00
func (c *Client4) ExecuteCommand(channelId, command string) (*CommandResponse, *Response) {
commandArgs := &CommandArgs{
ChannelId: channelId,
Command: command,
}
r, err := c.DoApiPost(c.GetCommandsRoute()+"/execute", commandArgs.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
2017-04-27 22:41:52 +09:00
}
defer closeBody(r)
response, _ := CommandResponseFromJson(r.Body)
return response, BuildResponse(r)
2017-04-27 22:41:52 +09:00
}
// ExecuteCommandWithTeam executes a given slash command against the specified team.
// Use this when executing slash commands in a DM/GM, since the team id cannot be inferred in that case.
func (c *Client4) ExecuteCommandWithTeam(channelId, teamId, command string) (*CommandResponse, *Response) {
commandArgs := &CommandArgs{
ChannelId: channelId,
TeamId: teamId,
Command: command,
}
r, err := c.DoApiPost(c.GetCommandsRoute()+"/execute", commandArgs.ToJson())
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
response, _ := CommandResponseFromJson(r.Body)
return response, BuildResponse(r)
}
// ListAutocompleteCommands will retrieve a list of commands available in the team.
func (c *Client4) ListAutocompleteCommands(teamId string) ([]*Command, *Response) {
r, err := c.DoApiGet(c.GetTeamAutoCompleteCommandsRoute(teamId), "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CommandListFromJson(r.Body), BuildResponse(r)
}
// RegenCommandToken will create a new token if the user have the right permissions.
func (c *Client4) RegenCommandToken(commandId string) (string, *Response) {
r, err := c.DoApiPut(c.GetCommandRoute(commandId)+"/regen_token", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return "", BuildErrorResponse(r, err)
}
defer closeBody(r)
return MapFromJson(r.Body)["token"], BuildResponse(r)
}
// Status Section
// GetUserStatus returns a user based on the provided user id string.
func (c *Client4) GetUserStatus(userId, etag string) (*Status, *Response) {
r, err := c.DoApiGet(c.GetUserStatusRoute(userId), etag)
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return StatusFromJson(r.Body), BuildResponse(r)
}
// GetUsersStatusesByIds returns a list of users status based on the provided user ids.
func (c *Client4) GetUsersStatusesByIds(userIds []string) ([]*Status, *Response) {
r, err := c.DoApiPost(c.GetUserStatusesRoute()+"/ids", ArrayToJson(userIds))
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return StatusListFromJson(r.Body), BuildResponse(r)
}
// UpdateUserStatus sets a user's status based on the provided user id string.
func (c *Client4) UpdateUserStatus(userId string, userStatus *Status) (*Status, *Response) {
r, err := c.DoApiPut(c.GetUserStatusRoute(userId), userStatus.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return StatusFromJson(r.Body), BuildResponse(r)
}
// Emoji Section
// CreateEmoji will save an emoji to the server if the current user has permission
// to do so. If successful, the provided emoji will be returned with its Id field
// filled in. Otherwise, an error will be returned.
func (c *Client4) CreateEmoji(emoji *Emoji, image []byte, filename string) (*Emoji, *Response) {
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
part, err := writer.CreateFormFile("image", filename)
if err != nil {
return nil, &Response{StatusCode: http.StatusForbidden, Error: NewAppError("CreateEmoji", "model.client.create_emoji.image.app_error", nil, err.Error(), 0)}
}
if _, err := io.Copy(part, bytes.NewBuffer(image)); err != nil {
return nil, &Response{StatusCode: http.StatusForbidden, Error: NewAppError("CreateEmoji", "model.client.create_emoji.image.app_error", nil, err.Error(), 0)}
}
if err := writer.WriteField("emoji", emoji.ToJson()); err != nil {
return nil, &Response{StatusCode: http.StatusForbidden, Error: NewAppError("CreateEmoji", "model.client.create_emoji.emoji.app_error", nil, err.Error(), 0)}
}
if err := writer.Close(); err != nil {
return nil, &Response{StatusCode: http.StatusForbidden, Error: NewAppError("CreateEmoji", "model.client.create_emoji.writer.app_error", nil, err.Error(), 0)}
}
return c.DoEmojiUploadFile(c.GetEmojisRoute(), body.Bytes(), writer.FormDataContentType())
}
2017-04-07 19:05:14 +02:00
// GetEmojiList returns a page of custom emoji on the system.
func (c *Client4) GetEmojiList(page, perPage int) ([]*Emoji, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
r, err := c.DoApiGet(c.GetEmojisRoute()+query, "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
2017-04-07 19:05:14 +02:00
}
defer closeBody(r)
return EmojiListFromJson(r.Body), BuildResponse(r)
}
// GetSortedEmojiList returns a page of custom emoji on the system sorted based on the sort
// parameter, blank for no sorting and "name" to sort by emoji names.
func (c *Client4) GetSortedEmojiList(page, perPage int, sort string) ([]*Emoji, *Response) {
query := fmt.Sprintf("?page=%v&per_page=%v&sort=%v", page, perPage, sort)
r, err := c.DoApiGet(c.GetEmojisRoute()+query, "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return EmojiListFromJson(r.Body), BuildResponse(r)
2017-04-07 19:05:14 +02:00
}
// DeleteEmoji delete an custom emoji on the provided emoji id string.
func (c *Client4) DeleteEmoji(emojiId string) (bool, *Response) {
r, err := c.DoApiDelete(c.GetEmojiRoute(emojiId))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// GetEmoji returns a custom emoji based on the emojiId string.
func (c *Client4) GetEmoji(emojiId string) (*Emoji, *Response) {
r, err := c.DoApiGet(c.GetEmojiRoute(emojiId), "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return EmojiFromJson(r.Body), BuildResponse(r)
}
// GetEmojiByName returns a custom emoji based on the name string.
func (c *Client4) GetEmojiByName(name string) (*Emoji, *Response) {
r, err := c.DoApiGet(c.GetEmojiByNameRoute(name), "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return EmojiFromJson(r.Body), BuildResponse(r)
}
// GetEmojiImage returns the emoji image.
func (c *Client4) GetEmojiImage(emojiId string) ([]byte, *Response) {
r, apErr := c.DoApiGet(c.GetEmojiRoute(emojiId)+"/image", "")
if apErr != nil {
return nil, BuildErrorResponse(r, apErr)
}
defer closeBody(r)
data, err := ioutil.ReadAll(r.Body)
if err != nil {
return nil, BuildErrorResponse(r, NewAppError("GetEmojiImage", "model.client.read_file.app_error", nil, err.Error(), r.StatusCode))
}
return data, BuildResponse(r)
}
// SearchEmoji returns a list of emoji matching some search criteria.
func (c *Client4) SearchEmoji(search *EmojiSearch) ([]*Emoji, *Response) {
r, err := c.DoApiPost(c.GetEmojisRoute()+"/search", search.ToJson())
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return EmojiListFromJson(r.Body), BuildResponse(r)
}
// AutocompleteEmoji returns a list of emoji starting with or matching name.
func (c *Client4) AutocompleteEmoji(name string, etag string) ([]*Emoji, *Response) {
query := fmt.Sprintf("?name=%v", name)
r, err := c.DoApiGet(c.GetEmojisRoute()+"/autocomplete"+query, "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return EmojiListFromJson(r.Body), BuildResponse(r)
}
// Reaction Section
// SaveReaction saves an emoji reaction for a post. Returns the saved reaction if successful, otherwise an error will be returned.
func (c *Client4) SaveReaction(reaction *Reaction) (*Reaction, *Response) {
r, err := c.DoApiPost(c.GetReactionsRoute(), reaction.ToJson())
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ReactionFromJson(r.Body), BuildResponse(r)
}
// GetReactions returns a list of reactions to a post.
func (c *Client4) GetReactions(postId string) ([]*Reaction, *Response) {
r, err := c.DoApiGet(c.GetPostRoute(postId)+"/reactions", "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ReactionsFromJson(r.Body), BuildResponse(r)
}
// DeleteReaction deletes reaction of a user in a post.
func (c *Client4) DeleteReaction(reaction *Reaction) (bool, *Response) {
r, err := c.DoApiDelete(c.GetUserRoute(reaction.UserId) + c.GetPostRoute(reaction.PostId) + fmt.Sprintf("/reactions/%v", reaction.EmojiName))
if err != nil {
2017-06-21 15:50:51 -04:00
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// FetchBulkReactions returns a map of postIds and corresponding reactions
func (c *Client4) GetBulkReactions(postIds []string) (map[string][]*Reaction, *Response) {
r, err := c.DoApiPost(c.GetPostsRoute()+"/ids/reactions", ArrayToJson(postIds))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return MapPostIdToReactionsFromJson(r.Body), BuildResponse(r)
}
// Timezone Section
// GetSupportedTimezone returns a page of supported timezones on the system.
func (c *Client4) GetSupportedTimezone() ([]string, *Response) {
r, err := c.DoApiGet(c.GetTimezonesRoute(), "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
var timezones []string
json.NewDecoder(r.Body).Decode(&timezones)
return timezones, BuildResponse(r)
}
// Open Graph Metadata Section
// OpenGraph return the open graph metadata for a particular url if the site have the metadata.
func (c *Client4) OpenGraph(url string) (map[string]string, *Response) {
requestBody := make(map[string]string)
requestBody["url"] = url
r, err := c.DoApiPost(c.GetOpenGraphRoute(), MapToJson(requestBody))
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return MapFromJson(r.Body), BuildResponse(r)
}
// Jobs Section
// GetJob gets a single job.
func (c *Client4) GetJob(id string) (*Job, *Response) {
r, err := c.DoApiGet(c.GetJobsRoute()+fmt.Sprintf("/%v", id), "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return JobFromJson(r.Body), BuildResponse(r)
}
// GetJobs gets all jobs, sorted with the job that was created most recently first.
func (c *Client4) GetJobs(page int, perPage int) ([]*Job, *Response) {
r, err := c.DoApiGet(c.GetJobsRoute()+fmt.Sprintf("?page=%v&per_page=%v", page, perPage), "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return JobsFromJson(r.Body), BuildResponse(r)
}
// GetJobsByType gets all jobs of a given type, sorted with the job that was created most recently first.
func (c *Client4) GetJobsByType(jobType string, page int, perPage int) ([]*Job, *Response) {
r, err := c.DoApiGet(c.GetJobsRoute()+fmt.Sprintf("/type/%v?page=%v&per_page=%v", jobType, page, perPage), "")
if err != nil {
2017-06-21 15:50:51 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return JobsFromJson(r.Body), BuildResponse(r)
}
// CreateJob creates a job based on the provided job struct.
func (c *Client4) CreateJob(job *Job) (*Job, *Response) {
r, err := c.DoApiPost(c.GetJobsRoute(), job.ToJson())
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return JobFromJson(r.Body), BuildResponse(r)
}
// CancelJob requests the cancellation of the job with the provided Id.
func (c *Client4) CancelJob(jobId string) (bool, *Response) {
r, err := c.DoApiPost(c.GetJobsRoute()+fmt.Sprintf("/%v/cancel", jobId), "")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// Roles Section
// GetRole gets a single role by ID.
func (c *Client4) GetRole(id string) (*Role, *Response) {
r, err := c.DoApiGet(c.GetRolesRoute()+fmt.Sprintf("/%v", id), "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return RoleFromJson(r.Body), BuildResponse(r)
}
// GetRoleByName gets a single role by Name.
func (c *Client4) GetRoleByName(name string) (*Role, *Response) {
r, err := c.DoApiGet(c.GetRolesRoute()+fmt.Sprintf("/name/%v", name), "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return RoleFromJson(r.Body), BuildResponse(r)
}
// GetRolesByNames returns a list of roles based on the provided role names.
func (c *Client4) GetRolesByNames(roleNames []string) ([]*Role, *Response) {
r, err := c.DoApiPost(c.GetRolesRoute()+"/names", ArrayToJson(roleNames))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return RoleListFromJson(r.Body), BuildResponse(r)
}
// PatchRole partially updates a role in the system. Any missing fields are not updated.
func (c *Client4) PatchRole(roleId string, patch *RolePatch) (*Role, *Response) {
r, err := c.DoApiPut(c.GetRolesRoute()+fmt.Sprintf("/%v/patch", roleId), patch.ToJson())
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return RoleFromJson(r.Body), BuildResponse(r)
}
// Schemes Section
// CreateScheme creates a new Scheme.
func (c *Client4) CreateScheme(scheme *Scheme) (*Scheme, *Response) {
r, err := c.DoApiPost(c.GetSchemesRoute(), scheme.ToJson())
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return SchemeFromJson(r.Body), BuildResponse(r)
}
// GetScheme gets a single scheme by ID.
func (c *Client4) GetScheme(id string) (*Scheme, *Response) {
r, err := c.DoApiGet(c.GetSchemeRoute(id), "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return SchemeFromJson(r.Body), BuildResponse(r)
}
// GetSchemes gets all schemes, sorted with the most recently created first, optionally filtered by scope.
func (c *Client4) GetSchemes(scope string, page int, perPage int) ([]*Scheme, *Response) {
r, err := c.DoApiGet(c.GetSchemesRoute()+fmt.Sprintf("?scope=%v&page=%v&per_page=%v", scope, page, perPage), "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return SchemesFromJson(r.Body), BuildResponse(r)
}
// DeleteScheme deletes a single scheme by ID.
func (c *Client4) DeleteScheme(id string) (bool, *Response) {
r, err := c.DoApiDelete(c.GetSchemeRoute(id))
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// PatchScheme partially updates a scheme in the system. Any missing fields are not updated.
func (c *Client4) PatchScheme(id string, patch *SchemePatch) (*Scheme, *Response) {
r, err := c.DoApiPut(c.GetSchemeRoute(id)+"/patch", patch.ToJson())
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return SchemeFromJson(r.Body), BuildResponse(r)
}
// GetTeamsForScheme gets the teams using this scheme, sorted alphabetically by display name.
func (c *Client4) GetTeamsForScheme(schemeId string, page int, perPage int) ([]*Team, *Response) {
r, err := c.DoApiGet(c.GetSchemeRoute(schemeId)+fmt.Sprintf("/teams?page=%v&per_page=%v", page, perPage), "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TeamListFromJson(r.Body), BuildResponse(r)
}
// GetChannelsForScheme gets the channels using this scheme, sorted alphabetically by display name.
func (c *Client4) GetChannelsForScheme(schemeId string, page int, perPage int) (ChannelList, *Response) {
r, err := c.DoApiGet(c.GetSchemeRoute(schemeId)+fmt.Sprintf("/channels?page=%v&per_page=%v", page, perPage), "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return *ChannelListFromJson(r.Body), BuildResponse(r)
}
// Plugin Section
// UploadPlugin takes an io.Reader stream pointing to the contents of a .tar.gz plugin.
// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE.
func (c *Client4) UploadPlugin(file io.Reader) (*Manifest, *Response) {
return c.uploadPlugin(file, false)
}
func (c *Client4) UploadPluginForced(file io.Reader) (*Manifest, *Response) {
return c.uploadPlugin(file, true)
}
func (c *Client4) uploadPlugin(file io.Reader, force bool) (*Manifest, *Response) {
body := new(bytes.Buffer)
writer := multipart.NewWriter(body)
if force {
err := writer.WriteField("force", "true")
if err != nil {
return nil, &Response{Error: NewAppError("UploadPlugin", "model.client.writer.app_error", nil, err.Error(), 0)}
}
}
part, err := writer.CreateFormFile("plugin", "plugin.tar.gz")
if err != nil {
return nil, &Response{Error: NewAppError("UploadPlugin", "model.client.writer.app_error", nil, err.Error(), 0)}
}
if _, err = io.Copy(part, file); err != nil {
return nil, &Response{Error: NewAppError("UploadPlugin", "model.client.writer.app_error", nil, err.Error(), 0)}
}
if err = writer.Close(); err != nil {
return nil, &Response{Error: NewAppError("UploadPlugin", "model.client.writer.app_error", nil, err.Error(), 0)}
}
rq, err := http.NewRequest("POST", c.ApiUrl+c.GetPluginsRoute(), body)
if err != nil {
return nil, &Response{Error: NewAppError("UploadPlugin", "model.client.connecting.app_error", nil, err.Error(), http.StatusBadRequest)}
}
rq.Header.Set("Content-Type", writer.FormDataContentType())
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
}
rp, err := c.HttpClient.Do(rq)
if err != nil || rp == nil {
return nil, BuildErrorResponse(rp, NewAppError("UploadPlugin", "model.client.connecting.app_error", nil, err.Error(), 0))
}
defer closeBody(rp)
if rp.StatusCode >= 300 {
return nil, BuildErrorResponse(rp, AppErrorFromJson(rp.Body))
}
return ManifestFromJson(rp.Body), BuildResponse(rp)
}
func (c *Client4) InstallPluginFromUrl(downloadUrl string, force bool) (*Manifest, *Response) {
forceStr := "false"
if force {
forceStr = "true"
}
url := fmt.Sprintf("%s?plugin_download_url=%s&force=%s", c.GetPluginsRoute()+"/install_from_url", url.QueryEscape(downloadUrl), forceStr)
r, err := c.DoApiPost(url, "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ManifestFromJson(r.Body), BuildResponse(r)
}
MM-16368 - Plugin Signing (#13017) * [MM-18757] POST handler for `/plugins/marketplace` (#12372) * Implement installMarketplacePlugin * Add InstallMarketplacePlugin endpoint * Fix go.mod * merge with master * Fix go.mod * Fix plugin tests * Move get plugin to marketplace client * Fix stylistic concerns * Add trailing newline to the go.mod * [MM-16586] Add plugin signature settings (#12390) * MM-17149 - Extend config.json for marketplace settings (#11933) * MM-17149 - Extend config.json for marketplace settings * Renamed MarketplaceUrl, tracking default marketplace url * Added EnableMarketplace to the client config * Revert "Added EnableMarketplace to the client config" This reverts commit 0f982c4c661c2cd9bb96264e9a01a2363c40d9c5. * MM-17149 - Added EnableMarketplace to the client config (#11958) * Added EnableMarketplace to the client config * Moved EnableMarketplace setting out of limited client configuration * Add public key settings to the config.json * Rename PublicKeys to SignaturePublicKeyFiles * Change filepath.Split to Base * Remove additional prints * Force extention of a public key file * Remove config validation * Remove error on delete * Remove config cloning * Add error messages * Add plugin public key tests * Rename extension to PluginSignaturePublicKeyFileExtention * Remove EnforceVerification * Change []*PublicKeyDescription to []string * Change .asc extension to .plugin.asc * Change ordering of public methods * Change plugin key commands * Update examples in the plugin key commands * Remove forcing extention * Add verify signature in settings * Fix tabbing * Fix naming * Remove unused text * Remove unused text * Update command examples * Fix unit tests * Change errors.New to errors.Wrap * Fix verbose flag * Change .asc to .gpg * Fix } * Change AddPublicKey signature * Change public.key extension * Add plugin public key command tests * Update en.json * Bootstrap the public keys * Update en.json * Fix en.json * Fix en.json * Bootstrap hard-coded public key * Remove unused texts in en.json * Change file to name * Add license header * Update development public key * Remove writeFile method * Remove .plugin.asc extension * Rename publiKey to mattermostPublicKey * Remove init_public_keys string * GolangCI * Closing file handlers * Fixed test that was installing nps plugin * [MM-19798] Implement plugin signature verification (#12768) * MM-17149 - Extend config.json for marketplace settings (#11933) * MM-17149 - Extend config.json for marketplace settings * Renamed MarketplaceUrl, tracking default marketplace url * Added EnableMarketplace to the client config * Revert "Added EnableMarketplace to the client config" This reverts commit 0f982c4c661c2cd9bb96264e9a01a2363c40d9c5. * MM-17149 - Added EnableMarketplace to the client config (#11958) * Added EnableMarketplace to the client config * Moved EnableMarketplace setting out of limited client configuration * Add public key settings to the config.json * Rename PublicKeys to SignaturePublicKeyFiles * Change filepath.Split to Base * Remove additional prints * Force extention of a public key file * Remove config validation * Remove error on delete * Remove config cloning * Add error messages * Add plugin public key tests * Rename extension to PluginSignaturePublicKeyFileExtention * Remove EnforceVerification * Change []*PublicKeyDescription to []string * Change .asc extension to .plugin.asc * Change ordering of public methods * Change plugin key commands * Update examples in the plugin key commands * Remove forcing extention * Add verify signature in settings * Fix tabbing * Fix naming * Remove unused text * Remove unused text * Update command examples * Fix unit tests * Change errors.New to errors.Wrap * Fix verbose flag * Change .asc to .gpg * Fix } * Change AddPublicKey signature * Change public.key extension * Add plugin public key command tests * Update en.json * Bootstrap the public keys * Update en.json * Fix en.json * Fix en.json * Bootstrap hard-coded public key * Remove unused texts in en.json * Change file to name * Add license header * Implement plugin signature verification * Remove benburker openpgp * Update en.json * Update development public key * Add support of multiple signatures in filestore * Update en.json * Run go mod vendor * Fix style * Remove writeFile method * Remove .plugin.asc extension * Rename publiKey to mattermostPublicKey * Verify plugin with mattermost public key * Remove init_public_keys string * Add InstallPluginWithSignature method and Refactor * Add signature verification on claster notification * Remove armored signature headers * Add error strings * Fix en.json * Change signatureStorePath * Implement minor fixes * Refactor plugin install methods * Add installPlugin method to uploadPlugin * Update en.json * Refactor installPlugin * Limit number of signatures * Close signatures * Fix helper function * Fix fromReadCloseSeekerToReadSeeker * Cleaned up ReadCloseSeeker for signatures * Remove signature truncation on FS * GolangCI * Add tests for armored signatures and plugin uploads * Fix nil slice issue * Fix TestPluginSync * Fixed tests * Return io.ReadSeeker from downloadFromUrl * Add log for the found plugins in the file store * Remove logging plugin detection info * [MM-20134] Consume and store single-signature for each plugin (#13081) * Consume and store single-signature for each plugin * Fix en.json * Remove saveSignature method * Remove public key hash * PR Feedback * refactored config * PR feedback
2019-11-18 19:02:41 -05:00
// InstallMarketplacePlugin will install marketplace plugin.
// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE.
func (c *Client4) InstallMarketplacePlugin(request *InstallMarketplacePluginRequest) (*Manifest, *Response) {
json, err := request.ToJson()
if err != nil {
return nil, &Response{Error: NewAppError("InstallMarketplacePlugin", "model.client.plugin_request_to_json.app_error", nil, err.Error(), http.StatusBadRequest)}
}
r, appErr := c.DoApiPost(c.GetPluginsRoute()+"/marketplace", json)
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
return ManifestFromJson(r.Body), BuildResponse(r)
}
// GetPlugins will return a list of plugin manifests for currently active plugins.
// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE.
func (c *Client4) GetPlugins() (*PluginsResponse, *Response) {
r, err := c.DoApiGet(c.GetPluginsRoute(), "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return PluginsResponseFromJson(r.Body), BuildResponse(r)
}
MM-8622: Improved plugin error reporting (#8737) * allow `Wait()`ing on the supervisor In the event the plugin supervisor shuts down a plugin for crashing too many times, the new `Wait()` interface allows the `ActivatePlugin` to accept a callback function to trigger when `supervisor.Wait()` returns. If the supervisor shuts down normally, this callback is invoked with a nil error, otherwise any error reported by the supervisor is passed along. * improve plugin activation/deactivation logic Avoid triggering activation of previously failed-to-start plugins just becase something in the configuration changed. Now, intelligently compare the global enable bit as well as the each individual plugin's enabled bit. * expose store to manipulate PluginStatuses * expose API to fetch plugin statuses * keep track of whether or not plugin sandboxing is supported * transition plugin statuses * restore error on plugin activation if already active * don't initialize test plugins until successfully loaded * emit websocket events when plugin statuses change * skip pruning if already initialized * MM-8622: maintain plugin statuses in memory Switch away from persisting plugin statuses to the database, and maintain in memory instead. This will be followed by a cluster interface to query the in-memory status of plugin statuses from all cluster nodes. At the same time, rename `cluster_discovery_id` on the `PluginStatus` model object to `cluster_id`. * MM-8622: aggregate plugin statuses across cluster * fetch cluster plugin statuses when emitting websocket notification * address unit test fixes after rebasing * relax (poor) racey unit test re: supervisor.Wait() * make store-mocks
2018-05-23 14:26:35 -04:00
// GetPluginStatuses will return the plugins installed on any server in the cluster, for reporting
// to the administrator via the system console.
// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE.
func (c *Client4) GetPluginStatuses() (PluginStatuses, *Response) {
r, err := c.DoApiGet(c.GetPluginsRoute(), "/statuses")
if err != nil {
MM-8622: Improved plugin error reporting (#8737) * allow `Wait()`ing on the supervisor In the event the plugin supervisor shuts down a plugin for crashing too many times, the new `Wait()` interface allows the `ActivatePlugin` to accept a callback function to trigger when `supervisor.Wait()` returns. If the supervisor shuts down normally, this callback is invoked with a nil error, otherwise any error reported by the supervisor is passed along. * improve plugin activation/deactivation logic Avoid triggering activation of previously failed-to-start plugins just becase something in the configuration changed. Now, intelligently compare the global enable bit as well as the each individual plugin's enabled bit. * expose store to manipulate PluginStatuses * expose API to fetch plugin statuses * keep track of whether or not plugin sandboxing is supported * transition plugin statuses * restore error on plugin activation if already active * don't initialize test plugins until successfully loaded * emit websocket events when plugin statuses change * skip pruning if already initialized * MM-8622: maintain plugin statuses in memory Switch away from persisting plugin statuses to the database, and maintain in memory instead. This will be followed by a cluster interface to query the in-memory status of plugin statuses from all cluster nodes. At the same time, rename `cluster_discovery_id` on the `PluginStatus` model object to `cluster_id`. * MM-8622: aggregate plugin statuses across cluster * fetch cluster plugin statuses when emitting websocket notification * address unit test fixes after rebasing * relax (poor) racey unit test re: supervisor.Wait() * make store-mocks
2018-05-23 14:26:35 -04:00
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return PluginStatusesFromJson(r.Body), BuildResponse(r)
MM-8622: Improved plugin error reporting (#8737) * allow `Wait()`ing on the supervisor In the event the plugin supervisor shuts down a plugin for crashing too many times, the new `Wait()` interface allows the `ActivatePlugin` to accept a callback function to trigger when `supervisor.Wait()` returns. If the supervisor shuts down normally, this callback is invoked with a nil error, otherwise any error reported by the supervisor is passed along. * improve plugin activation/deactivation logic Avoid triggering activation of previously failed-to-start plugins just becase something in the configuration changed. Now, intelligently compare the global enable bit as well as the each individual plugin's enabled bit. * expose store to manipulate PluginStatuses * expose API to fetch plugin statuses * keep track of whether or not plugin sandboxing is supported * transition plugin statuses * restore error on plugin activation if already active * don't initialize test plugins until successfully loaded * emit websocket events when plugin statuses change * skip pruning if already initialized * MM-8622: maintain plugin statuses in memory Switch away from persisting plugin statuses to the database, and maintain in memory instead. This will be followed by a cluster interface to query the in-memory status of plugin statuses from all cluster nodes. At the same time, rename `cluster_discovery_id` on the `PluginStatus` model object to `cluster_id`. * MM-8622: aggregate plugin statuses across cluster * fetch cluster plugin statuses when emitting websocket notification * address unit test fixes after rebasing * relax (poor) racey unit test re: supervisor.Wait() * make store-mocks
2018-05-23 14:26:35 -04:00
}
// RemovePlugin will disable and delete a plugin.
// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE.
func (c *Client4) RemovePlugin(id string) (bool, *Response) {
r, err := c.DoApiDelete(c.GetPluginRoute(id))
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// GetWebappPlugins will return a list of plugins that the webapp should download.
// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE.
func (c *Client4) GetWebappPlugins() ([]*Manifest, *Response) {
r, err := c.DoApiGet(c.GetPluginsRoute()+"/webapp", "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return ManifestListFromJson(r.Body), BuildResponse(r)
}
// EnablePlugin will enable an plugin installed.
// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE.
func (c *Client4) EnablePlugin(id string) (bool, *Response) {
r, err := c.DoApiPost(c.GetPluginRoute(id)+"/enable", "")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// DisablePlugin will disable an enabled plugin.
// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE.
func (c *Client4) DisablePlugin(id string) (bool, *Response) {
r, err := c.DoApiPost(c.GetPluginRoute(id)+"/disable", "")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
MM-17023: Plugin Marketplace (#12183) * MM-17149 - Extend config.json for marketplace settings (#11933) * MM-17149 - Extend config.json for marketplace settings * Renamed MarketplaceUrl, tracking default marketplace url * Added EnableMarketplace to the client config * Revert "Added EnableMarketplace to the client config" This reverts commit 0f982c4c661c2cd9bb96264e9a01a2363c40d9c5. * MM-17149 - Added EnableMarketplace to the client config (#11958) * Added EnableMarketplace to the client config * Moved EnableMarketplace setting out of limited client configuration * MM-17150, MM-17545, MM-18100 - Implement GET /api/v4/plugins/m… (#11977) * MM-17150 - Implement GET /api/v4/plugins/marketplace proxying upstream MM-17545 - Merge locally installed plugins into GET /api/v4/plugins/marketplace * Replaced MarketplacePluginState with Installed * Setting InstalledVersion instead of Installed * marketplace client setting per_page if non zero * Creating insecure client for marketplace url * Fixed trailing slash for default marketplace url * Adding filtering * Fixed function names * Renamed Manifest() to GetManifest(), added godoc for BaseMarketplacePlugin * Handling plugin.ErrNotFound correctly * Checking err == nil instead when a plugin is installed * MM-18450 - Local-only plugin search (#12152) * MM-17846: plugin icons (#12157) * MM-17846: add support for plugin icons Extend the model definitions to support plugin icons from the marketplace. * s/IconURL/IconData * MM-18475 - Converge on snake_case responses from the marketplace (#12179) * MM-18520 - MM-Server should forward server version to marketplace server (#12181) * Renamed request to filter client4.GetMarketplacePlugins * Renamed request to filter * Guarding against bad marketplace server response
2019-09-17 16:02:26 -03:00
// GetMarketplacePlugins will return a list of plugins that an admin can install.
// WARNING: PLUGINS ARE STILL EXPERIMENTAL. THIS FUNCTION IS SUBJECT TO CHANGE.
func (c *Client4) GetMarketplacePlugins(filter *MarketplacePluginFilter) ([]*MarketplacePlugin, *Response) {
route := c.GetPluginsRoute() + "/marketplace"
u, parseErr := url.Parse(route)
if parseErr != nil {
return nil, &Response{Error: NewAppError("GetMarketplacePlugins", "model.client.parse_plugins.app_error", nil, parseErr.Error(), http.StatusBadRequest)}
}
filter.ApplyToURL(u)
r, err := c.DoApiGet(u.String(), "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
plugins, readerErr := MarketplacePluginsFromReader(r.Body)
if readerErr != nil {
return nil, BuildErrorResponse(r, NewAppError(route, "model.client.parse_plugins.app_error", nil, err.Error(), http.StatusBadRequest))
}
return plugins, BuildResponse(r)
}
// UpdateChannelScheme will update a channel's scheme.
func (c *Client4) UpdateChannelScheme(channelId, schemeId string) (bool, *Response) {
sip := &SchemeIDPatch{SchemeID: &schemeId}
r, err := c.DoApiPut(c.GetChannelSchemeRoute(channelId), sip.ToJson())
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// UpdateTeamScheme will update a team's scheme.
func (c *Client4) UpdateTeamScheme(teamId, schemeId string) (bool, *Response) {
sip := &SchemeIDPatch{SchemeID: &schemeId}
r, err := c.DoApiPut(c.GetTeamSchemeRoute(teamId), sip.ToJson())
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// GetRedirectLocation retrieves the value of the 'Location' header of an HTTP response for a given URL.
func (c *Client4) GetRedirectLocation(urlParam, etag string) (string, *Response) {
url := fmt.Sprintf("%s?url=%s", c.GetRedirectLocationRoute(), url.QueryEscape(urlParam))
r, err := c.DoApiGet(url, etag)
if err != nil {
return "", BuildErrorResponse(r, err)
}
defer closeBody(r)
return MapFromJson(r.Body)["location"], BuildResponse(r)
}
// SetServerBusy will mark the server as busy, which disables non-critical services for `secs` seconds.
func (c *Client4) SetServerBusy(secs int) (bool, *Response) {
url := fmt.Sprintf("%s?seconds=%d", c.GetServerBusyRoute(), secs)
r, err := c.DoApiPost(url, "")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// ClearServerBusy will mark the server as not busy.
func (c *Client4) ClearServerBusy() (bool, *Response) {
r, err := c.DoApiPost(c.GetServerBusyRoute()+"/clear", "")
if err != nil {
return false, BuildErrorResponse(r, err)
}
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
// GetServerBusyExpires returns the time when a server marked busy
// will automatically have the flag cleared.
func (c *Client4) GetServerBusyExpires() (*time.Time, *Response) {
r, err := c.DoApiGet(c.GetServerBusyRoute(), "")
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
sbs := ServerBusyStateFromJson(r.Body)
expires := time.Unix(sbs.Expires, 0)
return &expires, BuildResponse(r)
}
// RegisterTermsOfServiceAction saves action performed by a user against a specific terms of service.
func (c *Client4) RegisterTermsOfServiceAction(userId, termsOfServiceId string, accepted bool) (*bool, *Response) {
url := c.GetUserTermsOfServiceRoute(userId)
data := map[string]interface{}{"termsOfServiceId": termsOfServiceId, "accepted": accepted}
r, err := c.DoApiPost(url, StringInterfaceToJson(data))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return NewBool(CheckStatusOK(r)), BuildResponse(r)
}
// GetTermsOfService fetches the latest terms of service
func (c *Client4) GetTermsOfService(etag string) (*TermsOfService, *Response) {
url := c.GetTermsOfServiceRoute()
r, err := c.DoApiGet(url, etag)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TermsOfServiceFromJson(r.Body), BuildResponse(r)
}
// GetUserTermsOfService fetches user's latest terms of service action if the latest action was for acceptance.
func (c *Client4) GetUserTermsOfService(userId, etag string) (*UserTermsOfService, *Response) {
url := c.GetUserTermsOfServiceRoute(userId)
r, err := c.DoApiGet(url, etag)
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return UserTermsOfServiceFromJson(r.Body), BuildResponse(r)
}
// CreateTermsOfService creates new terms of service.
func (c *Client4) CreateTermsOfService(text, userId string) (*TermsOfService, *Response) {
url := c.GetTermsOfServiceRoute()
data := map[string]interface{}{"text": text}
r, err := c.DoApiPost(url, StringInterfaceToJson(data))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return TermsOfServiceFromJson(r.Body), BuildResponse(r)
}
Ldap groups phase1 (#9752) * Initial models, API, app, and persistence of groups and group syncing. * Consistent letter casing in ldif. * Moves group-specific migrations into func. * Adds API endpoint to retrieve LDAP groups (and associated MM groups) one tree level at a time. * Adds mattermost group id to SCIMGroup (if available). * Splits user and group creation so that memberOf works. Returns users from ldap interface. * Updates method name. * Returns users IDs instead of User. * Removes non-essential group data. * MM-11807: Add GroupFilter to LDAP config. (#9513) * MM-11807: Add GroupFilter to LDAP config. * Add diagnostic. * Adds new config option for using 'memberOf' overlay. * Adds API endpoint to link a group. * Removes debug statements. * Adds unlink group API endpoint. * Fix to LDAP API. Adds API method to client4 and app. * Adds some missing app methods. Renames API unexported func. * Fixes link/unlink API path to accept valid DNs. * Allow any character for DN portion of path. * Switches from DN to objectGUID or entryUUID as the remote identifier linking LDAP groups to MM groups. * Formatting. * Formatting. * Setting group name field to an ID for phase 1. * Adds an LDAP config field to Setting up configuration for local LDAP. * Changes to LDAP and GroupStore interfaces. * Draft of nesting groups in API response. * Removes unnecessary tree models. * Updates group membershipt create store method to also restore. * Adds new config to test config. * Accept AD format length. * Switches to SetUniqueTogether method. * Updates revert. * Tweaks to syncing queries . * Updates query for pending team and channel memberships. * Removes old GroupSyncableScanner usage. Some formatting and renaming. * Fixes bug setting syncable type in selecting paged. * Adds tests for syncables populator. * Only add users to teams and channels that are not deleted. * Renames method. * Updates test LDAP setup. * Removes memberof config stuff. * Renames. * Updates test data. * Fix for gofmt. * Adds missing license. * Adds missing teardowns. * Test fix. * Adds a cycle to the groups test data. * Changes API to return flat list. * Removes some unused interface and app methods. * Returns empty braces if results are empty. * Adds more LDAP test data. * Fix for test data error. * Adds error. * Moves test groups. * Adds OU for load test data. * Moves load test ou creation to load data. * Adds a new bool flag to SCIMGroups. * Removes SCIMGroup completely. * Removes FULL JOIN because it is not supported in MySQL. * Adds tests for sync queries; renames constant. * Bad merge fix. * Vet fix. * Returning OK on delete ldap group link * Removes foreign key constraints. * Adding total to the ldap getAllGroups api endpoint * Adds get group members page. * Removes pagination from groups syncables list API. * Adding syncable check now that foreign key constraint is removes. * Joins teams and channels to group syncables. * Adds group member count. * Adding GetAllChannels and SearchAllChannels for system admins only * Fix. * Test fix from pagination removal. * Orders groupmembers by createat. * Fixing search of all channels * Test fix after removing pagination. * JSON syntax error fix. * Changing tests (for now) pending investigation. * Adding GetAllChannels and SearchAllChannels tests for the store * Adding GetAllChannels and SearchAllChannels API tests * Omit empty JSON values of group syncables. * Fixing GetAllChannels and SearchAllChannels tests * Fixing GetAllChannels and SearchAllChannels store tests * Fixing GetAllChannels api tests * Adds 'LDAP groups' feature flag. (#9861) * Migrate new client functions to idiomatic error handling * Test fixes. * Simplification of groups api (#9860) * Simplification of groups api * Fixing RequireSyncableType * Test fix. * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Update api4/group.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Fix copy/paste error. * Fix copy/paste error. * Adds missing return, changes to correct HTTP status code. * Adds missing return, changes status codes. * Check for license. * Renames variable for new signature. * Adds client method to get a group. * Adds client method and tests for PatchGroup. * Adds more API tests. * Adds groups API tests. * Adds client method and tests for getting group syncables. * Adds tests for patching group teams and channels. * Update to translations. * Removes test. * Fix incorrect conditional. * Removes unnecessary nil check. * Removes unnecessary return. * Updates comment, removes unused variable. * Uses consistent JSON unmarshal pattern. * Uses consistent JSON unmarshal pattern. * Moves const block. * Switches 'already linked' from error to success response. * Removes commented-out code. * Switched to status ok. * Add parens for readability. * Fix copy/paste error. * Unexport some structs. * Removes repeated validity check. * Return without attempting commit if there's a rollback. * Fix incorrect HTTP status code. * Update store/sqlstore/group_supplier.go Co-Authored-By: mkraft <martinkraft@gmail.com> * Adds utility methods for going from groupsyncable to groupteam and groupchannel. * Fixing george suggestions (#9911) * Test fix. * Adds QA data to VC with visualization. * Fixes typo in graph image. * Update display name when re-linking in case it has changed in LDAP. * Adds ability to configure group display name and unique identifier. (#9923) * Adds ability to configure group display name and unique identifier. * Adds some configs to confi-ldap make command. * Fix for move of session. * Exposes method for use by SAML package. * Switches GroupSyncableType from int to string. * Update Jenkins build files. * Removes unused variable assignment. * Removes old unnecessary early return. * Removes unnecessary variable. * Moves param parsing before license and permissions checks. * Removes old code. * Compares agains underlying error rather than error id. * Switches tests to assertions. * Adds more assertions. * Adds missing return. * Adds space after comma for added legibility. * Moves a view model to the api package. * Unexports method. * Uses id validator function. * Fix docker-compose flag. * Typo fix. * Moves index creation to supplier. * Removes bad merge. * Renames parameter. * Re-adds space. * Removes unnecessary transaction. * Escapes the Groups table name with backticks because it is a reserved keyword. * Fix roles cache bug * Removing unnecesiary deserializing function * Switches table name rather than custom SQL everywhere for Postgres without backticks. * Removes redundant check for sql.ErrNoRows. * Removes redundant check for sql.ErrNoRows. * Removes data integrity check and redundant nil conditional. * Removes redundant check for sql.ErrNoRows. * Removes unnecessary query. * Removes ID length validation from persistence tier. * Makes some supplier methods idempotent. * Removes some empty switch defaults. * Renames Group Type field to Source. * Fix for mistaken field name change. * Uses IsValidId function. * Removes comment. * Changes json key name. * Removes test because no longer validating user. * Moves model state validation to app layer. * Don't create Groups.CanLeave column until phase 2. * Removes state validation until properties are used in phase 2. * Removes duplicated check. * Removes state validation until properties are used in phase 2. * Removes some tests until phase 2. * Comment-out a bunch of test related to CanLeave. * Extra unmarshal validation check. Removes more code for CanLeave. * Removes tests for CanLeave. * Explict error msg. * Rewrite queries. * Changes index name. Adds index. * Removes assertion. * Adds experimental feature flag.
2019-01-10 15:17:31 -05:00
func (c *Client4) GetGroup(groupID, etag string) (*Group, *Response) {
r, appErr := c.DoApiGet(c.GetGroupRoute(groupID), etag)
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
return GroupFromJson(r.Body), BuildResponse(r)
}
func (c *Client4) PatchGroup(groupID string, patch *GroupPatch) (*Group, *Response) {
payload, _ := json.Marshal(patch)
r, appErr := c.DoApiPut(c.GetGroupRoute(groupID)+"/patch", string(payload))
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
return GroupFromJson(r.Body), BuildResponse(r)
}
func (c *Client4) LinkGroupSyncable(groupID, syncableID string, syncableType GroupSyncableType, patch *GroupSyncablePatch) (*GroupSyncable, *Response) {
payload, _ := json.Marshal(patch)
url := fmt.Sprintf("%s/link", c.GetGroupSyncableRoute(groupID, syncableID, syncableType))
r, appErr := c.DoApiPost(url, string(payload))
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
return GroupSyncableFromJson(r.Body), BuildResponse(r)
}
func (c *Client4) UnlinkGroupSyncable(groupID, syncableID string, syncableType GroupSyncableType) *Response {
url := fmt.Sprintf("%s/link", c.GetGroupSyncableRoute(groupID, syncableID, syncableType))
r, appErr := c.DoApiDelete(url)
if appErr != nil {
return BuildErrorResponse(r, appErr)
}
defer closeBody(r)
return BuildResponse(r)
}
func (c *Client4) GetGroupSyncable(groupID, syncableID string, syncableType GroupSyncableType, etag string) (*GroupSyncable, *Response) {
r, appErr := c.DoApiGet(c.GetGroupSyncableRoute(groupID, syncableID, syncableType), etag)
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
return GroupSyncableFromJson(r.Body), BuildResponse(r)
}
func (c *Client4) GetGroupSyncables(groupID string, syncableType GroupSyncableType, etag string) ([]*GroupSyncable, *Response) {
r, appErr := c.DoApiGet(c.GetGroupSyncablesRoute(groupID, syncableType), etag)
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
return GroupSyncablesFromJson(r.Body), BuildResponse(r)
}
func (c *Client4) PatchGroupSyncable(groupID, syncableID string, syncableType GroupSyncableType, patch *GroupSyncablePatch) (*GroupSyncable, *Response) {
payload, _ := json.Marshal(patch)
r, appErr := c.DoApiPut(c.GetGroupSyncableRoute(groupID, syncableID, syncableType)+"/patch", string(payload))
if appErr != nil {
return nil, BuildErrorResponse(r, appErr)
}
defer closeBody(r)
return GroupSyncableFromJson(r.Body), BuildResponse(r)
}
func (c *Client4) TeamMembersMinusGroupMembers(teamID string, groupIDs []string, page, perPage int, etag string) ([]*UserWithGroups, int64, *Response) {
groupIDStr := strings.Join(groupIDs, ",")
query := fmt.Sprintf("?group_ids=%s&page=%d&per_page=%d", groupIDStr, page, perPage)
r, err := c.DoApiGet(c.GetTeamRoute(teamID)+"/members_minus_group_members"+query, etag)
if err != nil {
return nil, 0, BuildErrorResponse(r, err)
}
defer closeBody(r)
ugc := UsersWithGroupsAndCountFromJson(r.Body)
return ugc.Users, ugc.Count, BuildResponse(r)
}
func (c *Client4) ChannelMembersMinusGroupMembers(channelID string, groupIDs []string, page, perPage int, etag string) ([]*UserWithGroups, int64, *Response) {
groupIDStr := strings.Join(groupIDs, ",")
query := fmt.Sprintf("?group_ids=%s&page=%d&per_page=%d", groupIDStr, page, perPage)
r, err := c.DoApiGet(c.GetChannelRoute(channelID)+"/members_minus_group_members"+query, etag)
if err != nil {
return nil, 0, BuildErrorResponse(r, err)
}
defer closeBody(r)
ugc := UsersWithGroupsAndCountFromJson(r.Body)
return ugc.Users, ugc.Count, BuildResponse(r)
}