Expand Plugin and REST APIs to trigger user typing event (#14331)
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
8c1164d86b
Коммит
66597d0fcb
32
api4/user.go
32
api4/user.go
@@ -80,6 +80,8 @@ func (api *API) InitUser() {
|
|||||||
api.BaseRoutes.Users.Handle("/tokens/revoke", api.ApiSessionRequired(revokeUserAccessToken)).Methods("POST")
|
api.BaseRoutes.Users.Handle("/tokens/revoke", api.ApiSessionRequired(revokeUserAccessToken)).Methods("POST")
|
||||||
api.BaseRoutes.Users.Handle("/tokens/disable", api.ApiSessionRequired(disableUserAccessToken)).Methods("POST")
|
api.BaseRoutes.Users.Handle("/tokens/disable", api.ApiSessionRequired(disableUserAccessToken)).Methods("POST")
|
||||||
api.BaseRoutes.Users.Handle("/tokens/enable", api.ApiSessionRequired(enableUserAccessToken)).Methods("POST")
|
api.BaseRoutes.Users.Handle("/tokens/enable", api.ApiSessionRequired(enableUserAccessToken)).Methods("POST")
|
||||||
|
|
||||||
|
api.BaseRoutes.User.Handle("/typing", api.ApiSessionRequiredDisableWhenBusy(publishUserTyping)).Methods("POST")
|
||||||
}
|
}
|
||||||
|
|
||||||
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -2270,3 +2272,33 @@ func demoteUserToGuest(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
auditRec.Success()
|
auditRec.Success()
|
||||||
ReturnStatusOK(w)
|
ReturnStatusOK(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func publishUserTyping(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
c.RequireUserId()
|
||||||
|
if c.Err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
typingRequest := model.TypingRequestFromJson(r.Body)
|
||||||
|
if typingRequest == nil {
|
||||||
|
c.SetInvalidParam("typing_request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Params.UserId != c.App.Session().UserId && !c.App.SessionHasPermissionTo(*c.App.Session(), model.PERMISSION_MANAGE_SYSTEM) {
|
||||||
|
c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.App.HasPermissionToChannel(c.Params.UserId, typingRequest.ChannelId, model.PERMISSION_CREATE_POST) {
|
||||||
|
c.SetPermissionError(model.PERMISSION_CREATE_POST)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.App.PublishUserTyping(c.Params.UserId, typingRequest.ChannelId, typingRequest.ParentId); err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ReturnStatusOK(w)
|
||||||
|
}
|
||||||
|
|||||||
@@ -4781,3 +4781,78 @@ func TestGetKnownUsers(t *testing.T) {
|
|||||||
assert.ElementsMatch(t, userIds, []string{u2.Id, u3.Id})
|
assert.ElementsMatch(t, userIds, []string{u2.Id, u3.Id})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPublishUserTyping(t *testing.T) {
|
||||||
|
th := Setup(t).InitBasic()
|
||||||
|
defer th.TearDown()
|
||||||
|
|
||||||
|
tr := model.TypingRequest{
|
||||||
|
ChannelId: th.BasicChannel.Id,
|
||||||
|
ParentId: "randomparentid",
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("should return ok for non-system admin when triggering typing event for own user", func(t *testing.T) {
|
||||||
|
_, resp := th.Client.PublishUserTyping(th.BasicUser.Id, tr)
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return ok for system admin when triggering typing event for own user", func(t *testing.T) {
|
||||||
|
th.LinkUserToTeam(th.SystemAdminUser, th.BasicTeam)
|
||||||
|
th.AddUserToChannel(th.SystemAdminUser, th.BasicChannel)
|
||||||
|
|
||||||
|
_, resp := th.SystemAdminClient.PublishUserTyping(th.SystemAdminUser.Id, tr)
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return forbidden for non-system admin when triggering a typing event for a different user", func(t *testing.T) {
|
||||||
|
_, resp := th.Client.PublishUserTyping(th.BasicUser2.Id, tr)
|
||||||
|
CheckForbiddenStatus(t, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return bad request when triggering a typing event for an invalid user id", func(t *testing.T) {
|
||||||
|
_, resp := th.Client.PublishUserTyping("invalid", tr)
|
||||||
|
CheckErrorMessage(t, resp, "api.context.invalid_url_param.app_error")
|
||||||
|
CheckBadRequestStatus(t, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should send typing event via websocket when triggering a typing event for a user with a common channel", func(t *testing.T) {
|
||||||
|
webSocketClient, err := th.CreateWebSocketClient()
|
||||||
|
assert.Nil(t, err)
|
||||||
|
defer webSocketClient.Close()
|
||||||
|
|
||||||
|
webSocketClient.Listen()
|
||||||
|
|
||||||
|
time.Sleep(300 * time.Millisecond)
|
||||||
|
wsResp := <-webSocketClient.ResponseChannel
|
||||||
|
require.Equal(t, model.STATUS_OK, wsResp.Status)
|
||||||
|
|
||||||
|
_, resp := th.SystemAdminClient.PublishUserTyping(th.BasicUser2.Id, tr)
|
||||||
|
CheckNoError(t, resp)
|
||||||
|
|
||||||
|
assertExpectedWebsocketEvent(t, webSocketClient, model.WEBSOCKET_EVENT_TYPING, func(resp *model.WebSocketEvent) {
|
||||||
|
assert.Equal(t, th.BasicChannel.Id, resp.GetBroadcast().ChannelId)
|
||||||
|
|
||||||
|
eventUserId, ok := resp.GetData()["user_id"].(string)
|
||||||
|
require.True(t, ok, "expected user_id")
|
||||||
|
assert.Equal(t, th.BasicUser2.Id, eventUserId)
|
||||||
|
|
||||||
|
eventParentId, ok := resp.GetData()["parent_id"].(string)
|
||||||
|
require.True(t, ok, "expected parent_id")
|
||||||
|
assert.Equal(t, "randomparentid", eventParentId)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
th.Server.Busy.Set(time.Second * 10)
|
||||||
|
|
||||||
|
t.Run("should return service unavailable for non-system admin user when triggering a typing event and server busy", func(t *testing.T) {
|
||||||
|
_, resp := th.Client.PublishUserTyping("invalid", tr)
|
||||||
|
CheckErrorMessage(t, resp, "api.context.server_busy.app_error")
|
||||||
|
CheckServiceUnavailableStatus(t, resp)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should return service unavailable for system admin user when triggering a typing event and server busy", func(t *testing.T) {
|
||||||
|
_, resp := th.SystemAdminClient.PublishUserTyping(th.SystemAdminUser.Id, tr)
|
||||||
|
CheckErrorMessage(t, resp, "api.context.server_busy.app_error")
|
||||||
|
CheckServiceUnavailableStatus(t, resp)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -774,6 +774,7 @@ type AppIface interface {
|
|||||||
ProcessSlackText(text string) string
|
ProcessSlackText(text string) string
|
||||||
Publish(message *model.WebSocketEvent)
|
Publish(message *model.WebSocketEvent)
|
||||||
PublishSkipClusterSend(message *model.WebSocketEvent)
|
PublishSkipClusterSend(message *model.WebSocketEvent)
|
||||||
|
PublishUserTyping(userId, channelId, parentId string) *model.AppError
|
||||||
PurgeBleveIndexes() *model.AppError
|
PurgeBleveIndexes() *model.AppError
|
||||||
PurgeElasticsearchIndexes() *model.AppError
|
PurgeElasticsearchIndexes() *model.AppError
|
||||||
ReadFile(path string) ([]byte, *model.AppError)
|
ReadFile(path string) ([]byte, *model.AppError)
|
||||||
|
|||||||
@@ -10750,6 +10750,28 @@ func (a *OpenTracingAppLayer) PublishSkipClusterSend(message *model.WebSocketEve
|
|||||||
a.app.PublishSkipClusterSend(message)
|
a.app.PublishSkipClusterSend(message)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *OpenTracingAppLayer) PublishUserTyping(userId string, channelId string, parentId string) *model.AppError {
|
||||||
|
origCtx := a.ctx
|
||||||
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PublishUserTyping")
|
||||||
|
|
||||||
|
a.ctx = newCtx
|
||||||
|
a.app.Srv().Store.SetContext(newCtx)
|
||||||
|
defer func() {
|
||||||
|
a.app.Srv().Store.SetContext(origCtx)
|
||||||
|
a.ctx = origCtx
|
||||||
|
}()
|
||||||
|
|
||||||
|
defer span.Finish()
|
||||||
|
resultVar0 := a.app.PublishUserTyping(userId, channelId, parentId)
|
||||||
|
|
||||||
|
if resultVar0 != nil {
|
||||||
|
span.LogFields(spanlog.Error(resultVar0))
|
||||||
|
ext.Error.Set(span, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultVar0
|
||||||
|
}
|
||||||
|
|
||||||
func (a *OpenTracingAppLayer) PurgeBleveIndexes() *model.AppError {
|
func (a *OpenTracingAppLayer) PurgeBleveIndexes() *model.AppError {
|
||||||
origCtx := a.ctx
|
origCtx := a.ctx
|
||||||
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PurgeBleveIndexes")
|
span, newCtx := tracing.StartSpanWithParentByContext(a.ctx, "app.PurgeBleveIndexes")
|
||||||
|
|||||||
@@ -850,6 +850,10 @@ func (api *PluginAPI) DeleteBotIconImage(userId string) *model.AppError {
|
|||||||
return api.app.DeleteBotIconImage(userId)
|
return api.app.DeleteBotIconImage(userId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *PluginAPI) PublishUserTyping(userId, channelId, parentId string) *model.AppError {
|
||||||
|
return api.app.PublishUserTyping(userId, channelId, parentId)
|
||||||
|
}
|
||||||
|
|
||||||
func (api *PluginAPI) PluginHTTP(request *http.Request) *http.Response {
|
func (api *PluginAPI) PluginHTTP(request *http.Request) *http.Response {
|
||||||
split := strings.SplitN(request.URL.Path, "/", 3)
|
split := strings.SplitN(request.URL.Path, "/", 3)
|
||||||
if len(split) != 3 {
|
if len(split) != 3 {
|
||||||
|
|||||||
12
app/user.go
12
app/user.go
@@ -2088,6 +2088,18 @@ func (a *App) DemoteUserToGuest(user *model.User) *model.AppError {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) PublishUserTyping(userId, channelId, parentId string) *model.AppError {
|
||||||
|
omitUsers := make(map[string]bool, 1)
|
||||||
|
omitUsers[userId] = true
|
||||||
|
|
||||||
|
event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_TYPING, "", channelId, "", omitUsers)
|
||||||
|
event.Add("parent_id", parentId)
|
||||||
|
event.Add("user_id", userId)
|
||||||
|
a.Publish(event)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// invalidateUserCacheAndPublish Invalidates cache for a user and publishes user updated event
|
// invalidateUserCacheAndPublish Invalidates cache for a user and publishes user updated event
|
||||||
func (a *App) invalidateUserCacheAndPublish(userId string) {
|
func (a *App) invalidateUserCacheAndPublish(userId string) {
|
||||||
a.InvalidateCacheForUser(userId)
|
a.InvalidateCacheForUser(userId)
|
||||||
|
|||||||
@@ -487,6 +487,10 @@ func (c *Client4) GetGroupsRoute() string {
|
|||||||
return "/groups"
|
return "/groups"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client4) GetPublishUserTypingRoute(userId string) string {
|
||||||
|
return c.GetUserRoute(userId) + "/typing"
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client4) GetGroupRoute(groupID string) string {
|
func (c *Client4) GetGroupRoute(groupID string) string {
|
||||||
return fmt.Sprintf("%s/%s", c.GetGroupsRoute(), groupID)
|
return fmt.Sprintf("%s/%s", c.GetGroupsRoute(), groupID)
|
||||||
}
|
}
|
||||||
@@ -5094,6 +5098,16 @@ func (c *Client4) GetKnownUsers() ([]string, *Response) {
|
|||||||
return userIds, BuildResponse(r)
|
return userIds, BuildResponse(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PublishUserTyping publishes a user is typing websocket event based on the provided TypingRequest.
|
||||||
|
func (c *Client4) PublishUserTyping(userID string, typingRequest TypingRequest) (bool, *Response) {
|
||||||
|
r, err := c.DoApiPost(c.GetPublishUserTypingRoute(userID), typingRequest.ToJson())
|
||||||
|
if err != nil {
|
||||||
|
return false, BuildErrorResponse(r, err)
|
||||||
|
}
|
||||||
|
defer closeBody(r)
|
||||||
|
return CheckStatusOK(r), BuildResponse(r)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client4) GetChannelMemberCountsByGroup(channelID string, includeTimezones bool, etag string) ([]*ChannelMemberCountByGroup, *Response) {
|
func (c *Client4) GetChannelMemberCountsByGroup(channelID string, includeTimezones bool, etag string) ([]*ChannelMemberCountByGroup, *Response) {
|
||||||
r, err := c.DoApiGet(c.GetChannelRoute(channelID)+"/member_counts_by_group?include_timezones="+strconv.FormatBool(includeTimezones), etag)
|
r, err := c.DoApiGet(c.GetChannelRoute(channelID)+"/member_counts_by_group?include_timezones="+strconv.FormatBool(includeTimezones), etag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
25
model/typing_request.go
Обычный файл
25
model/typing_request.go
Обычный файл
@@ -0,0 +1,25 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TypingRequest struct {
|
||||||
|
ChannelId string `json:"channel_id"`
|
||||||
|
ParentId string `json:"parent_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *TypingRequest) ToJson() string {
|
||||||
|
b, _ := json.Marshal(o)
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TypingRequestFromJson(data io.Reader) *TypingRequest {
|
||||||
|
var o *TypingRequest
|
||||||
|
json.NewDecoder(data).Decode(&o)
|
||||||
|
return o
|
||||||
|
}
|
||||||
20
model/typing_request_test.go
Обычный файл
20
model/typing_request_test.go
Обычный файл
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTypingRequestJson(t *testing.T) {
|
||||||
|
o := TypingRequest{ChannelId: NewId(), ParentId: NewId()}
|
||||||
|
json := o.ToJson()
|
||||||
|
ro := TypingRequestFromJson(strings.NewReader(json))
|
||||||
|
|
||||||
|
require.Equal(t, o.ChannelId, ro.ChannelId, "ChannelIds do not match")
|
||||||
|
require.Equal(t, o.ParentId, ro.ParentId, "ParentIds do not match")
|
||||||
|
}
|
||||||
@@ -953,6 +953,13 @@ type API interface {
|
|||||||
//
|
//
|
||||||
// Minimum server version: 5.18
|
// Minimum server version: 5.18
|
||||||
PluginHTTP(request *http.Request) *http.Response
|
PluginHTTP(request *http.Request) *http.Response
|
||||||
|
|
||||||
|
// PublishUserTyping publishes a user is typing WebSocket event.
|
||||||
|
// The parentId parameter may be an empty string, the other parameters are required.
|
||||||
|
//
|
||||||
|
// @tag User
|
||||||
|
// Minimum server version: 5.26
|
||||||
|
PublishUserTyping(userId, channelId, parentId string) *model.AppError
|
||||||
}
|
}
|
||||||
|
|
||||||
var handshake = plugin.HandshakeConfig{
|
var handshake = plugin.HandshakeConfig{
|
||||||
|
|||||||
@@ -1015,3 +1015,10 @@ func (api *apiTimerLayer) PluginHTTP(request *http.Request) *http.Response {
|
|||||||
api.recordTime(startTime, "PluginHTTP", true)
|
api.recordTime(startTime, "PluginHTTP", true)
|
||||||
return _returnsA
|
return _returnsA
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (api *apiTimerLayer) PublishUserTyping(userId, channelId, parentId string) *model.AppError {
|
||||||
|
startTime := timePkg.Now()
|
||||||
|
_returnsA := api.apiImpl.PublishUserTyping(userId, channelId, parentId)
|
||||||
|
api.recordTime(startTime, "PublishUserTyping", true)
|
||||||
|
return _returnsA
|
||||||
|
}
|
||||||
|
|||||||
@@ -4422,3 +4422,33 @@ func (s *apiRPCServer) DeleteBotIconImage(args *Z_DeleteBotIconImageArgs, return
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Z_PublishUserTypingArgs struct {
|
||||||
|
A string
|
||||||
|
B string
|
||||||
|
C string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Z_PublishUserTypingReturns struct {
|
||||||
|
A *model.AppError
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *apiRPCClient) PublishUserTyping(userId, channelId, parentId string) *model.AppError {
|
||||||
|
_args := &Z_PublishUserTypingArgs{userId, channelId, parentId}
|
||||||
|
_returns := &Z_PublishUserTypingReturns{}
|
||||||
|
if err := g.client.Call("Plugin.PublishUserTyping", _args, _returns); err != nil {
|
||||||
|
log.Printf("RPC call to PublishUserTyping API failed: %s", err.Error())
|
||||||
|
}
|
||||||
|
return _returns.A
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *apiRPCServer) PublishUserTyping(args *Z_PublishUserTypingArgs, returns *Z_PublishUserTypingReturns) error {
|
||||||
|
if hook, ok := s.impl.(interface {
|
||||||
|
PublishUserTyping(userId, channelId, parentId string) *model.AppError
|
||||||
|
}); ok {
|
||||||
|
returns.A = hook.PublishUserTyping(args.A, args.B, args.C)
|
||||||
|
} else {
|
||||||
|
return encodableError(fmt.Errorf("API PublishUserTyping called but not implemented."))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -2443,6 +2443,22 @@ func (_m *API) PluginHTTP(request *http.Request) *http.Response {
|
|||||||
return r0
|
return r0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PublishUserTyping provides a mock function with given fields: userId, channelId, parentId
|
||||||
|
func (_m *API) PublishUserTyping(userId string, channelId string, parentId string) *model.AppError {
|
||||||
|
ret := _m.Called(userId, channelId, parentId)
|
||||||
|
|
||||||
|
var r0 *model.AppError
|
||||||
|
if rf, ok := ret.Get(0).(func(string, string, string) *model.AppError); ok {
|
||||||
|
r0 = rf(userId, channelId, parentId)
|
||||||
|
} else {
|
||||||
|
if ret.Get(0) != nil {
|
||||||
|
r0 = ret.Get(0).(*model.AppError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r0
|
||||||
|
}
|
||||||
|
|
||||||
// PublishWebSocketEvent provides a mock function with given fields: event, payload, broadcast
|
// PublishWebSocketEvent provides a mock function with given fields: event, payload, broadcast
|
||||||
func (_m *API) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *model.WebsocketBroadcast) {
|
func (_m *API) PublishWebSocketEvent(event string, payload map[string]interface{}, broadcast *model.WebsocketBroadcast) {
|
||||||
_m.Called(event, payload, broadcast)
|
_m.Called(event, payload, broadcast)
|
||||||
|
|||||||
@@ -35,15 +35,9 @@ func (api *API) userTyping(req *model.WebSocketRequest) (map[string]interface{},
|
|||||||
parentId = ""
|
parentId = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
omitUsers := make(map[string]bool, 1)
|
appErr := api.App.PublishUserTyping(req.Session.UserId, channelId, parentId)
|
||||||
omitUsers[req.Session.UserId] = true
|
|
||||||
|
|
||||||
event := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_TYPING, "", channelId, "", omitUsers)
|
return nil, appErr
|
||||||
event.Add("parent_id", parentId)
|
|
||||||
event.Add("user_id", req.Session.UserId)
|
|
||||||
api.App.Publish(event)
|
|
||||||
|
|
||||||
return nil, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (api *API) userUpdateActiveStatus(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) {
|
func (api *API) userUpdateActiveStatus(req *model.WebSocketRequest) (map[string]interface{}, *model.AppError) {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user