[MM-15811] Adds fallback mechanism for elasticsearch queries (#11065)
* [MM-15811] Adds fallback mechanism for elasticsearch queries * Rename config helpers
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
4c075e45f4
Коммит
406dcb1311
@@ -107,8 +107,7 @@ func (a *App) JoinDefaultChannels(teamId string, user *model.User, shouldBeAdmin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
if a.IsESIndexingEnabled() {
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing {
|
|
||||||
a.Srv.Go(func() {
|
a.Srv.Go(func() {
|
||||||
if err = a.indexUser(user); err != nil {
|
if err = a.indexUser(user); err != nil {
|
||||||
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err))
|
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err))
|
||||||
@@ -185,8 +184,7 @@ func (a *App) CreateChannelWithUser(channel *model.Channel, userId string) (*mod
|
|||||||
message.Add("team_id", channel.TeamId)
|
message.Add("team_id", channel.TeamId)
|
||||||
a.Publish(message)
|
a.Publish(message)
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
if a.IsESIndexingEnabled() {
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing {
|
|
||||||
a.Srv.Go(func() {
|
a.Srv.Go(func() {
|
||||||
if err := a.indexUser(user); err != nil {
|
if err := a.indexUser(user); err != nil {
|
||||||
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err))
|
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err))
|
||||||
@@ -261,11 +259,10 @@ func (a *App) CreateChannel(channel *model.Channel, addMember bool) (*model.Chan
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
if a.IsESIndexingEnabled() {
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing {
|
if sc.Type == model.CHANNEL_OPEN {
|
||||||
if sc.Type == "O" {
|
|
||||||
a.Srv.Go(func() {
|
a.Srv.Go(func() {
|
||||||
if err := esInterface.IndexChannel(sc); err != nil {
|
if err := a.Elasticsearch.IndexChannel(sc); err != nil {
|
||||||
mlog.Error("Encountered error indexing channel", mlog.String("channel_id", sc.Id), mlog.Err(err))
|
mlog.Error("Encountered error indexing channel", mlog.String("channel_id", sc.Id), mlog.Err(err))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -309,8 +306,7 @@ func (a *App) GetOrCreateDirectChannel(userId, otherUserId string) (*model.Chann
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
if a.IsESIndexingEnabled() {
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing {
|
|
||||||
a.Srv.Go(func() {
|
a.Srv.Go(func() {
|
||||||
for _, id := range []string{userId, otherUserId} {
|
for _, id := range []string{userId, otherUserId} {
|
||||||
if err := a.indexUserFromId(id); err != nil {
|
if err := a.indexUserFromId(id); err != nil {
|
||||||
@@ -419,8 +415,7 @@ func (a *App) CreateGroupChannel(userIds []string, creatorId string) (*model.Cha
|
|||||||
message.Add("teammate_ids", model.ArrayToJson(userIds))
|
message.Add("teammate_ids", model.ArrayToJson(userIds))
|
||||||
a.Publish(message)
|
a.Publish(message)
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
if a.IsESIndexingEnabled() {
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing {
|
|
||||||
a.Srv.Go(func() {
|
a.Srv.Go(func() {
|
||||||
for _, id := range userIds {
|
for _, id := range userIds {
|
||||||
if err := a.indexUserFromId(id); err != nil {
|
if err := a.indexUserFromId(id); err != nil {
|
||||||
@@ -517,10 +512,9 @@ func (a *App) UpdateChannel(channel *model.Channel) (*model.Channel, *model.AppE
|
|||||||
messageWs.Add("channel", channel.ToJson())
|
messageWs.Add("channel", channel.ToJson())
|
||||||
a.Publish(messageWs)
|
a.Publish(messageWs)
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
if a.IsESIndexingEnabled() && channel.Type == model.CHANNEL_OPEN {
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing && channel.Type == "O" {
|
|
||||||
a.Srv.Go(func() {
|
a.Srv.Go(func() {
|
||||||
if err := esInterface.IndexChannel(channel); err != nil {
|
if err := a.Elasticsearch.IndexChannel(channel); err != nil {
|
||||||
mlog.Error("Encountered error indexing channel", mlog.String("channel_id", channel.Id), mlog.Err(err))
|
mlog.Error("Encountered error indexing channel", mlog.String("channel_id", channel.Id), mlog.Err(err))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -1003,8 +997,7 @@ func (a *App) AddChannelMember(userId string, channel *model.Channel, userReques
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
if a.IsESIndexingEnabled() {
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing {
|
|
||||||
a.Srv.Go(func() {
|
a.Srv.Go(func() {
|
||||||
if err := a.indexUser(user); err != nil {
|
if err := a.indexUser(user); err != nil {
|
||||||
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err))
|
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err))
|
||||||
@@ -1413,8 +1406,7 @@ func (a *App) JoinChannel(channel *model.Channel, userId string) *model.AppError
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
if a.IsESIndexingEnabled() {
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing {
|
|
||||||
a.Srv.Go(func() {
|
a.Srv.Go(func() {
|
||||||
if err := a.indexUser(user); err != nil {
|
if err := a.indexUser(user); err != nil {
|
||||||
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err))
|
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err))
|
||||||
@@ -1649,8 +1641,7 @@ func (a *App) removeUserFromChannel(userIdToRemove string, removerUserId string,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
if a.IsESIndexingEnabled() {
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing {
|
|
||||||
a.Srv.Go(func() {
|
a.Srv.Go(func() {
|
||||||
if err := a.indexUserFromId(userIdToRemove); err != nil {
|
if err := a.indexUserFromId(userIdToRemove); err != nil {
|
||||||
mlog.Error("Encountered error indexing user", mlog.String("user_id", userIdToRemove), mlog.Err(err))
|
mlog.Error("Encountered error indexing user", mlog.String("user_id", userIdToRemove), mlog.Err(err))
|
||||||
@@ -1745,36 +1736,50 @@ func (a *App) UpdateChannelLastViewedAt(channelIds []string, userId string) *mod
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) AutocompleteChannels(teamId string, term string) (*model.ChannelList, *model.AppError) {
|
func (a *App) esAutocompleteChannels(teamId, term string, includeDeleted bool) (*model.ChannelList, *model.AppError) {
|
||||||
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
|
channelIds, err := a.Elasticsearch.SearchChannels(teamId, term)
|
||||||
term = strings.TrimSpace(term)
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
channelList := model.ChannelList{}
|
||||||
license := a.License()
|
if len(channelIds) > 0 {
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableAutocomplete && license != nil && *license.Features.Elasticsearch {
|
channels, err := a.Srv.Store.Channel().GetChannelsByIds(channelIds)
|
||||||
channelIds, err := a.Elasticsearch.SearchChannels(teamId, term)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
for _, c := range channels {
|
||||||
channelList := model.ChannelList{}
|
if c.DeleteAt > 0 && !includeDeleted {
|
||||||
if len(channelIds) > 0 {
|
continue
|
||||||
channels, err := a.Srv.Store.Channel().GetChannelsByIds(channelIds)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, c := range channels {
|
|
||||||
if c.DeleteAt > 0 && !includeDeleted {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
channelList = append(channelList, c)
|
|
||||||
}
|
}
|
||||||
|
channelList = append(channelList, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &channelList, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return a.Srv.Store.Channel().AutocompleteInTeam(teamId, term, includeDeleted)
|
return &channelList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) AutocompleteChannels(teamId string, term string) (*model.ChannelList, *model.AppError) {
|
||||||
|
includeDeleted := *a.Config().TeamSettings.ExperimentalViewArchivedChannels
|
||||||
|
var channelList *model.ChannelList
|
||||||
|
var err *model.AppError
|
||||||
|
term = strings.TrimSpace(term)
|
||||||
|
|
||||||
|
if a.IsESAutocompletionEnabled() {
|
||||||
|
channelList, err = a.esAutocompleteChannels(teamId, term, includeDeleted)
|
||||||
|
if err != nil {
|
||||||
|
mlog.Error("Encountered error on AutocompleteChannels through Elasticsearch. Falling back to default autocompletion.", mlog.Err(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !a.IsESAutocompletionEnabled() || err != nil {
|
||||||
|
channelList, err = a.Srv.Store.Channel().AutocompleteInTeam(teamId, term, includeDeleted)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return channelList, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) AutocompleteChannelsForSearch(teamId string, userId string, term string) (*model.ChannelList, *model.AppError) {
|
func (a *App) AutocompleteChannelsForSearch(teamId string, userId string, term string) (*model.ChannelList, *model.AppError) {
|
||||||
@@ -1927,8 +1932,7 @@ func (a *App) PermanentDeleteChannel(channel *model.Channel) *model.AppError {
|
|||||||
return result.Err
|
return result.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
if a.IsESIndexingEnabled() {
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing {
|
|
||||||
a.Srv.Go(func() {
|
a.Srv.Go(func() {
|
||||||
for _, user := range channelUsers.Data.(map[string]*model.User) {
|
for _, user := range channelUsers.Data.(map[string]*model.User) {
|
||||||
if err := a.indexUser(user); err != nil {
|
if err := a.indexUser(user); err != nil {
|
||||||
@@ -1936,9 +1940,9 @@ func (a *App) PermanentDeleteChannel(channel *model.Channel) *model.AppError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if channel.Type == "O" {
|
if channel.Type == model.CHANNEL_OPEN {
|
||||||
a.Srv.Go(func() {
|
a.Srv.Go(func() {
|
||||||
if err := esInterface.DeleteChannel(channel); err != nil {
|
if err := a.Elasticsearch.DeleteChannel(channel); err != nil {
|
||||||
mlog.Error("Encountered error deleting channel", mlog.String("channel_id", channel.Id), mlog.Err(err))
|
mlog.Error("Encountered error deleting channel", mlog.String("channel_id", channel.Id), mlog.Err(err))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -399,3 +399,19 @@ func (a *App) SaveConfig(newCfg *model.Config, sendConfigChangeClusterMessage bo
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) IsESIndexingEnabled() bool {
|
||||||
|
return a.Elasticsearch != nil && *a.Config().ElasticsearchSettings.EnableIndexing
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) IsESSearchEnabled() bool {
|
||||||
|
esInterface := a.Elasticsearch
|
||||||
|
license := a.License()
|
||||||
|
return esInterface != nil && *a.Config().ElasticsearchSettings.EnableSearching && license != nil && *license.Features.Elasticsearch
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) IsESAutocompletionEnabled() bool {
|
||||||
|
esInterface := a.Elasticsearch
|
||||||
|
license := a.License()
|
||||||
|
return esInterface != nil && *a.Config().ElasticsearchSettings.EnableAutocomplete && license != nil && *license.Features.Elasticsearch
|
||||||
|
}
|
||||||
|
|||||||
166
app/post.go
166
app/post.go
@@ -274,10 +274,9 @@ func (a *App) CreatePost(post *model.Post, channel *model.Channel, triggerWebhoo
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
if a.IsESIndexingEnabled() {
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing {
|
|
||||||
a.Srv.Go(func() {
|
a.Srv.Go(func() {
|
||||||
if err = esInterface.IndexPost(rpost, channel.TeamId); err != nil {
|
if err = a.Elasticsearch.IndexPost(rpost, channel.TeamId); err != nil {
|
||||||
mlog.Error("Encountered error indexing post", mlog.String("post_id", post.Id), mlog.Err(err))
|
mlog.Error("Encountered error indexing post", mlog.String("post_id", post.Id), mlog.Err(err))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -551,15 +550,14 @@ func (a *App) UpdatePost(post *model.Post, safeUpdate bool) (*model.Post, *model
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
if a.IsESIndexingEnabled() {
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing {
|
|
||||||
a.Srv.Go(func() {
|
a.Srv.Go(func() {
|
||||||
rchannel := <-a.Srv.Store.Channel().GetForPost(rpost.Id)
|
rchannel := <-a.Srv.Store.Channel().GetForPost(rpost.Id)
|
||||||
if rchannel.Err != nil {
|
if rchannel.Err != nil {
|
||||||
mlog.Error(fmt.Sprintf("Couldn't get channel %v for post %v for Elasticsearch indexing.", rpost.ChannelId, rpost.Id))
|
mlog.Error(fmt.Sprintf("Couldn't get channel %v for post %v for Elasticsearch indexing.", rpost.ChannelId, rpost.Id))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := esInterface.IndexPost(rpost, rchannel.Data.(*model.Channel).TeamId); err != nil {
|
if err := a.Elasticsearch.IndexPost(rpost, rchannel.Data.(*model.Channel).TeamId); err != nil {
|
||||||
mlog.Error("Encountered error indexing post", mlog.String("post_id", post.Id), mlog.Err(err))
|
mlog.Error("Encountered error indexing post", mlog.String("post_id", post.Id), mlog.Err(err))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -708,10 +706,9 @@ func (a *App) DeletePost(postId, deleteByID string) (*model.Post, *model.AppErro
|
|||||||
a.DeleteFlaggedPosts(post.Id)
|
a.DeleteFlaggedPosts(post.Id)
|
||||||
})
|
})
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
if a.IsESIndexingEnabled() {
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing {
|
|
||||||
a.Srv.Go(func() {
|
a.Srv.Go(func() {
|
||||||
if err := esInterface.DeletePost(post); err != nil {
|
if err := a.Elasticsearch.DeletePost(post); err != nil {
|
||||||
mlog.Error("Encountered error deleting post", mlog.String("post_id", post.Id), mlog.Err(err))
|
mlog.Error("Encountered error deleting post", mlog.String("post_id", post.Id), mlog.Err(err))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -811,75 +808,82 @@ func (a *App) SearchPostsInTeam(teamId string, paramsList []*model.SearchParams)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) SearchPostsInTeamForUser(terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int) (*model.PostSearchResults, *model.AppError) {
|
func (a *App) esSearchPostsInTeamForUser(paramsList []*model.SearchParams, userId, teamId string, isOrSearch, includeDeletedChannels bool, page, perPage int) (*model.PostSearchResults, *model.AppError) {
|
||||||
paramsList := model.ParseSearchParams(strings.TrimSpace(terms), timeZoneOffset)
|
finalParamsList := []*model.SearchParams{}
|
||||||
includeDeleted := includeDeletedChannels && *a.Config().TeamSettings.ExperimentalViewArchivedChannels
|
includeDeleted := includeDeletedChannels && *a.Config().TeamSettings.ExperimentalViewArchivedChannels
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
for _, params := range paramsList {
|
||||||
license := a.License()
|
params.OrTerms = isOrSearch
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableSearching && license != nil && *license.Features.Elasticsearch {
|
// Don't allow users to search for "*"
|
||||||
finalParamsList := []*model.SearchParams{}
|
if params.Terms != "*" {
|
||||||
|
// Convert channel names to channel IDs
|
||||||
for _, params := range paramsList {
|
for idx, channelName := range params.InChannels {
|
||||||
params.OrTerms = isOrSearch
|
channel, err := a.parseAndFetchChannelIdByNameFromInFilter(channelName, userId, teamId, includeDeletedChannels)
|
||||||
// Don't allow users to search for "*"
|
if err != nil {
|
||||||
if params.Terms != "*" {
|
mlog.Error(fmt.Sprint(err))
|
||||||
// Convert channel names to channel IDs
|
continue
|
||||||
for idx, channelName := range params.InChannels {
|
|
||||||
channel, err := a.parseAndFetchChannelIdByNameFromInFilter(channelName, userId, teamId, includeDeletedChannels)
|
|
||||||
if err != nil {
|
|
||||||
mlog.Error(fmt.Sprint(err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
params.InChannels[idx] = channel.Id
|
|
||||||
}
|
}
|
||||||
|
params.InChannels[idx] = channel.Id
|
||||||
// Convert usernames to user IDs
|
|
||||||
for idx, username := range params.FromUsers {
|
|
||||||
if user, err := a.GetUserByUsername(username); err != nil {
|
|
||||||
mlog.Error(fmt.Sprint(err))
|
|
||||||
} else {
|
|
||||||
params.FromUsers[idx] = user.Id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
finalParamsList = append(finalParamsList, params)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// If the processed search params are empty, return empty search results.
|
// Convert usernames to user IDs
|
||||||
if len(finalParamsList) == 0 {
|
for idx, username := range params.FromUsers {
|
||||||
return model.MakePostSearchResults(model.NewPostList(), nil), nil
|
if user, err := a.GetUserByUsername(username); err != nil {
|
||||||
}
|
mlog.Error(fmt.Sprint(err))
|
||||||
|
} else {
|
||||||
|
params.FromUsers[idx] = user.Id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// We only allow the user to search in channels they are a member of.
|
finalParamsList = append(finalParamsList, params)
|
||||||
userChannels, err := a.GetChannelsForUser(teamId, userId, includeDeleted)
|
|
||||||
if err != nil {
|
|
||||||
mlog.Error(fmt.Sprint(err))
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
postIds, matches, err := a.Elasticsearch.SearchPosts(userChannels, finalParamsList, page, perPage)
|
// If the processed search params are empty, return empty search results.
|
||||||
|
if len(finalParamsList) == 0 {
|
||||||
|
return model.MakePostSearchResults(model.NewPostList(), nil), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only allow the user to search in channels they are a member of.
|
||||||
|
userChannels, err := a.GetChannelsForUser(teamId, userId, includeDeleted)
|
||||||
|
if err != nil {
|
||||||
|
mlog.Error(fmt.Sprint(err))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
postIds, matches, err := a.Elasticsearch.SearchPosts(userChannels, finalParamsList, page, perPage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the posts
|
||||||
|
postList := model.NewPostList()
|
||||||
|
if len(postIds) > 0 {
|
||||||
|
posts, err := a.Srv.Store.Post().GetPostsByIds(postIds)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
for _, p := range posts {
|
||||||
// Get the posts
|
if p.DeleteAt == 0 {
|
||||||
postList := model.NewPostList()
|
postList.AddPost(p)
|
||||||
if len(postIds) > 0 {
|
postList.AddOrder(p.Id)
|
||||||
posts, err := a.Srv.Store.Post().GetPostsByIds(postIds)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, p := range posts {
|
|
||||||
if p.DeleteAt == 0 {
|
|
||||||
postList.AddPost(p)
|
|
||||||
postList.AddOrder(p.Id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return model.MakePostSearchResults(postList, matches), nil
|
return model.MakePostSearchResults(postList, matches), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) SearchPostsInTeamForUser(terms string, userId string, teamId string, isOrSearch bool, includeDeletedChannels bool, timeZoneOffset int, page, perPage int) (*model.PostSearchResults, *model.AppError) {
|
||||||
|
var postSearchResults *model.PostSearchResults
|
||||||
|
var err *model.AppError
|
||||||
|
paramsList := model.ParseSearchParams(strings.TrimSpace(terms), timeZoneOffset)
|
||||||
|
|
||||||
|
if a.IsESSearchEnabled() {
|
||||||
|
postSearchResults, err = a.esSearchPostsInTeamForUser(paramsList, userId, teamId, isOrSearch, includeDeletedChannels, page, perPage)
|
||||||
|
if err != nil {
|
||||||
|
mlog.Error("Encountered error on SearchPostsInTeamForUser through Elasticsearch. Falling back to default search.", mlog.Err(err))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !*a.Config().ServiceSettings.EnablePostSearch {
|
if !*a.Config().ServiceSettings.EnablePostSearch {
|
||||||
@@ -891,24 +895,30 @@ func (a *App) SearchPostsInTeamForUser(terms string, userId string, teamId strin
|
|||||||
return model.MakePostSearchResults(model.NewPostList(), nil), nil
|
return model.MakePostSearchResults(model.NewPostList(), nil), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
posts, err := a.searchPostsInTeam(teamId, userId, paramsList, func(params *model.SearchParams) {
|
if !a.IsESSearchEnabled() || err != nil {
|
||||||
params.IncludeDeletedChannels = includeDeleted
|
includeDeleted := includeDeletedChannels && *a.Config().TeamSettings.ExperimentalViewArchivedChannels
|
||||||
params.OrTerms = isOrSearch
|
posts, err := a.searchPostsInTeam(teamId, userId, paramsList, func(params *model.SearchParams) {
|
||||||
for idx, channelName := range params.InChannels {
|
params.IncludeDeletedChannels = includeDeleted
|
||||||
if strings.HasPrefix(channelName, "@") {
|
params.OrTerms = isOrSearch
|
||||||
channel, err := a.parseAndFetchChannelIdByNameFromInFilter(channelName, userId, teamId, includeDeletedChannels)
|
for idx, channelName := range params.InChannels {
|
||||||
if err != nil {
|
if strings.HasPrefix(channelName, "@") {
|
||||||
mlog.Error(fmt.Sprint(err))
|
channel, err := a.parseAndFetchChannelIdByNameFromInFilter(channelName, userId, teamId, includeDeletedChannels)
|
||||||
continue
|
if err != nil {
|
||||||
|
mlog.Error(fmt.Sprint(err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
params.InChannels[idx] = channel.Name
|
||||||
}
|
}
|
||||||
params.InChannels[idx] = channel.Name
|
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
})
|
|
||||||
if err != nil {
|
postSearchResults = model.MakePostSearchResults(posts, nil)
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
return model.MakePostSearchResults(posts, nil), nil
|
|
||||||
|
return postSearchResults, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetFileInfosForPostWithMigration(postId string) ([]*model.FileInfo, *model.AppError) {
|
func (a *App) GetFileInfosForPostWithMigration(postId string) ([]*model.FileInfo, *model.AppError) {
|
||||||
|
|||||||
237
app/user.go
237
app/user.go
@@ -280,8 +280,7 @@ func (a *App) createUserOrGuest(user *model.User, guest bool) (*model.User, *mod
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
if a.IsESIndexingEnabled() {
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing {
|
|
||||||
a.Srv.Go(func() {
|
a.Srv.Go(func() {
|
||||||
if err := a.indexUser(user); err != nil {
|
if err := a.indexUser(user); err != nil {
|
||||||
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err))
|
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err))
|
||||||
@@ -1160,8 +1159,7 @@ func (a *App) UpdateUser(user *model.User, sendNotifications bool) (*model.User,
|
|||||||
|
|
||||||
a.InvalidateCacheForUser(user.Id)
|
a.InvalidateCacheForUser(user.Id)
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
if a.IsESIndexingEnabled() {
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing {
|
|
||||||
a.Srv.Go(func() {
|
a.Srv.Go(func() {
|
||||||
if err := a.indexUser(user); err != nil {
|
if err := a.indexUser(user); err != nil {
|
||||||
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err))
|
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err))
|
||||||
@@ -1516,8 +1514,7 @@ func (a *App) PermanentDeleteUser(user *model.User) *model.AppError {
|
|||||||
|
|
||||||
mlog.Warn(fmt.Sprintf("Permanently deleted account %v id=%v", user.Email, user.Id), mlog.String("user_id", user.Id))
|
mlog.Warn(fmt.Sprintf("Permanently deleted account %v id=%v", user.Email, user.Id), mlog.String("user_id", user.Id))
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
if a.IsESIndexingEnabled() {
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing {
|
|
||||||
a.Srv.Go(func() {
|
a.Srv.Go(func() {
|
||||||
if err := a.Elasticsearch.DeleteUser(user); err != nil {
|
if err := a.Elasticsearch.DeleteUser(user); err != nil {
|
||||||
mlog.Error("Encountered error deleting user", mlog.String("user_id", user.Id), mlog.Err(err))
|
mlog.Error("Encountered error deleting user", mlog.String("user_id", user.Id), mlog.Err(err))
|
||||||
@@ -1709,35 +1706,25 @@ func (a *App) SearchUsersNotInChannel(teamId string, channelId string, term stri
|
|||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) SearchUsersInTeam(teamId string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
|
func (a *App) esSearchUsersInTeam(teamId, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
|
||||||
var result store.StoreResult
|
listOfAllowedChannels, err := a.GetViewUsersRestrictionsForTeam(a.Session.UserId, teamId)
|
||||||
|
if err != nil {
|
||||||
term = strings.TrimSpace(term)
|
return nil, err
|
||||||
|
}
|
||||||
esInterface := a.Elasticsearch
|
if listOfAllowedChannels != nil && len(listOfAllowedChannels) == 0 {
|
||||||
license := a.License()
|
return []*model.User{}, nil
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableAutocomplete && license != nil && *license.Features.Elasticsearch {
|
|
||||||
listOfAllowedChannels, err := a.GetViewUsersRestrictionsForTeam(a.Session.UserId, teamId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if listOfAllowedChannels != nil && len(listOfAllowedChannels) == 0 {
|
|
||||||
return []*model.User{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
usersIds, err := a.Elasticsearch.SearchUsersInTeam(teamId, listOfAllowedChannels, term, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result = <-a.Srv.Store.User().GetProfileByIds(usersIds, false, nil)
|
|
||||||
} else {
|
|
||||||
result = <-a.Srv.Store.User().Search(teamId, term, options)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
usersIds, err := a.Elasticsearch.SearchUsersInTeam(teamId, listOfAllowedChannels, term, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := <-a.Srv.Store.User().GetProfileByIds(usersIds, false, nil)
|
||||||
if result.Err != nil {
|
if result.Err != nil {
|
||||||
return nil, result.Err
|
return nil, result.Err
|
||||||
}
|
}
|
||||||
|
|
||||||
users := result.Data.([]*model.User)
|
users := result.Data.([]*model.User)
|
||||||
|
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
@@ -1747,6 +1734,34 @@ func (a *App) SearchUsersInTeam(teamId string, term string, options *model.UserS
|
|||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) SearchUsersInTeam(teamId, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
|
||||||
|
var users []*model.User
|
||||||
|
var err *model.AppError
|
||||||
|
term = strings.TrimSpace(term)
|
||||||
|
|
||||||
|
if a.IsESAutocompletionEnabled() {
|
||||||
|
users, err = a.esSearchUsersInTeam(teamId, term, options)
|
||||||
|
if err != nil {
|
||||||
|
mlog.Error("Encountered error on SearchUsersInTeam through Elasticsearch. Falling back to default search.", mlog.Err(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !a.IsESAutocompletionEnabled() || err != nil {
|
||||||
|
result := <-a.Srv.Store.User().Search(teamId, term, options)
|
||||||
|
if result.Err != nil {
|
||||||
|
return nil, result.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
users = result.Data.([]*model.User)
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
a.SanitizeProfile(user, options.IsAdmin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) SearchUsersNotInTeam(notInTeamId string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
|
func (a *App) SearchUsersNotInTeam(notInTeamId string, term string, options *model.UserSearchOptions) ([]*model.User, *model.AppError) {
|
||||||
term = strings.TrimSpace(term)
|
term = strings.TrimSpace(term)
|
||||||
result := <-a.Srv.Store.User().SearchNotInTeam(notInTeamId, term, options)
|
result := <-a.Srv.Store.User().SearchNotInTeam(notInTeamId, term, options)
|
||||||
@@ -1777,38 +1792,26 @@ func (a *App) SearchUsersWithoutTeam(term string, options *model.UserSearchOptio
|
|||||||
return users, nil
|
return users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) AutocompleteUsersInChannel(teamId string, channelId string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, *model.AppError) {
|
func (a *App) esAutocompleteUsersInChannel(teamId, channelId, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, *model.AppError) {
|
||||||
var uchan, nuchan store.StoreChannel
|
listOfAllowedChannels, err := a.getListOfAllowedChannelsForTeam(teamId, options.ViewRestrictions)
|
||||||
|
if err != nil {
|
||||||
term = strings.TrimSpace(term)
|
return nil, err
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
|
||||||
license := a.License()
|
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableAutocomplete && license != nil && *license.Features.Elasticsearch {
|
|
||||||
listOfAllowedChannels, err := a.getListOfAllowedChannelsForTeam(teamId, options.ViewRestrictions)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if len(listOfAllowedChannels) == 0 {
|
|
||||||
return &model.UserAutocompleteInChannel{}, nil
|
|
||||||
}
|
|
||||||
uchanIds := []string{}
|
|
||||||
nuchanIds := []string{}
|
|
||||||
if !strings.Contains(strings.Join(listOfAllowedChannels, "."), channelId) {
|
|
||||||
nuchanIds, err = a.Elasticsearch.SearchUsersInTeam(teamId, listOfAllowedChannels, term, options)
|
|
||||||
} else {
|
|
||||||
uchanIds, nuchanIds, err = a.Elasticsearch.SearchUsersInChannel(teamId, channelId, listOfAllowedChannels, term, options)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
uchan = a.Srv.Store.User().GetProfileByIds(uchanIds, false, nil)
|
|
||||||
nuchan = a.Srv.Store.User().GetProfileByIds(nuchanIds, false, nil)
|
|
||||||
} else {
|
|
||||||
uchan = a.Srv.Store.User().SearchInChannel(channelId, term, options)
|
|
||||||
nuchan = a.Srv.Store.User().SearchNotInChannel(teamId, channelId, term, options)
|
|
||||||
}
|
}
|
||||||
|
if len(listOfAllowedChannels) == 0 {
|
||||||
|
return &model.UserAutocompleteInChannel{}, nil
|
||||||
|
}
|
||||||
|
uchanIds := []string{}
|
||||||
|
nuchanIds := []string{}
|
||||||
|
if !strings.Contains(strings.Join(listOfAllowedChannels, "."), channelId) {
|
||||||
|
nuchanIds, err = a.Elasticsearch.SearchUsersInTeam(teamId, listOfAllowedChannels, term, options)
|
||||||
|
} else {
|
||||||
|
uchanIds, nuchanIds, err = a.Elasticsearch.SearchUsersInChannel(teamId, channelId, listOfAllowedChannels, term, options)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
uchan := a.Srv.Store.User().GetProfileByIds(uchanIds, false, nil)
|
||||||
|
nuchan := a.Srv.Store.User().GetProfileByIds(nuchanIds, false, nil)
|
||||||
autocomplete := &model.UserAutocompleteInChannel{}
|
autocomplete := &model.UserAutocompleteInChannel{}
|
||||||
|
|
||||||
result := <-uchan
|
result := <-uchan
|
||||||
@@ -1838,47 +1841,112 @@ func (a *App) AutocompleteUsersInChannel(teamId string, channelId string, term s
|
|||||||
return autocomplete, nil
|
return autocomplete, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) AutocompleteUsersInTeam(teamId string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInTeam, *model.AppError) {
|
func (a *App) AutocompleteUsersInChannel(teamId string, channelId string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInChannel, *model.AppError) {
|
||||||
autocomplete := &model.UserAutocompleteInTeam{}
|
var autocomplete *model.UserAutocompleteInChannel
|
||||||
var result store.StoreResult
|
var err *model.AppError
|
||||||
|
|
||||||
term = strings.TrimSpace(term)
|
term = strings.TrimSpace(term)
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
if a.IsESAutocompletionEnabled() {
|
||||||
license := a.License()
|
autocomplete, err = a.esAutocompleteUsersInChannel(teamId, channelId, term, options)
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableAutocomplete && license != nil && *license.Features.Elasticsearch {
|
|
||||||
listOfAllowedChannels, err := a.getListOfAllowedChannelsForTeam(teamId, options.ViewRestrictions)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
mlog.Error("Encountered error on AutocompleteUsersInChannel through Elasticsearch. Falling back to default autocompletion.", mlog.Err(err))
|
||||||
}
|
}
|
||||||
if len(listOfAllowedChannels) == 0 {
|
|
||||||
return &model.UserAutocompleteInTeam{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
usersIds, err := a.Elasticsearch.SearchUsersInTeam(teamId, listOfAllowedChannels, term, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
result = <-a.Srv.Store.User().GetProfileByIds(usersIds, false, nil)
|
|
||||||
} else {
|
|
||||||
result = <-a.Srv.Store.User().Search(teamId, term, options)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !a.IsESAutocompletionEnabled() || err != nil {
|
||||||
|
autocomplete = &model.UserAutocompleteInChannel{}
|
||||||
|
uchan := a.Srv.Store.User().SearchInChannel(channelId, term, options)
|
||||||
|
nuchan := a.Srv.Store.User().SearchNotInChannel(teamId, channelId, term, options)
|
||||||
|
|
||||||
|
result := <-uchan
|
||||||
|
if result.Err != nil {
|
||||||
|
return nil, result.Err
|
||||||
|
}
|
||||||
|
users := result.Data.([]*model.User)
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
a.SanitizeProfile(user, options.IsAdmin)
|
||||||
|
}
|
||||||
|
|
||||||
|
autocomplete.InChannel = users
|
||||||
|
|
||||||
|
result = <-nuchan
|
||||||
|
if result.Err != nil {
|
||||||
|
return nil, result.Err
|
||||||
|
}
|
||||||
|
users = result.Data.([]*model.User)
|
||||||
|
|
||||||
|
for _, user := range users {
|
||||||
|
a.SanitizeProfile(user, options.IsAdmin)
|
||||||
|
}
|
||||||
|
|
||||||
|
autocomplete.OutOfChannel = users
|
||||||
|
}
|
||||||
|
|
||||||
|
return autocomplete, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) esAutocompleteUsersInTeam(teamId, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInTeam, *model.AppError) {
|
||||||
|
listOfAllowedChannels, err := a.getListOfAllowedChannelsForTeam(teamId, options.ViewRestrictions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(listOfAllowedChannels) == 0 {
|
||||||
|
return &model.UserAutocompleteInTeam{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
usersIds, err := a.Elasticsearch.SearchUsersInTeam(teamId, listOfAllowedChannels, term, options)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := <-a.Srv.Store.User().GetProfileByIds(usersIds, false, nil)
|
||||||
if result.Err != nil {
|
if result.Err != nil {
|
||||||
return nil, result.Err
|
return nil, result.Err
|
||||||
}
|
}
|
||||||
users := result.Data.([]*model.User)
|
|
||||||
|
|
||||||
|
users := result.Data.([]*model.User)
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
a.SanitizeProfile(user, options.IsAdmin)
|
a.SanitizeProfile(user, options.IsAdmin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
autocomplete := &model.UserAutocompleteInTeam{}
|
||||||
autocomplete.InTeam = users
|
autocomplete.InTeam = users
|
||||||
|
|
||||||
return autocomplete, nil
|
return autocomplete, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) AutocompleteUsersInTeam(teamId string, term string, options *model.UserSearchOptions) (*model.UserAutocompleteInTeam, *model.AppError) {
|
||||||
|
var autocomplete *model.UserAutocompleteInTeam
|
||||||
|
var err *model.AppError
|
||||||
|
|
||||||
|
term = strings.TrimSpace(term)
|
||||||
|
|
||||||
|
if a.IsESAutocompletionEnabled() {
|
||||||
|
autocomplete, err = a.esAutocompleteUsersInTeam(teamId, term, options)
|
||||||
|
if err != nil {
|
||||||
|
mlog.Error("Encountered error on AutocompleteUsersInTeam through Elasticsearch. Falling back to default autocompletion.", mlog.Err(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !a.IsESAutocompletionEnabled() || err != nil {
|
||||||
|
autocomplete = &model.UserAutocompleteInTeam{}
|
||||||
|
result := <-a.Srv.Store.User().Search(teamId, term, options)
|
||||||
|
if result.Err != nil {
|
||||||
|
return nil, result.Err
|
||||||
|
}
|
||||||
|
|
||||||
|
users := result.Data.([]*model.User)
|
||||||
|
for _, user := range users {
|
||||||
|
a.SanitizeProfile(user, options.IsAdmin)
|
||||||
|
}
|
||||||
|
|
||||||
|
autocomplete.InTeam = users
|
||||||
|
}
|
||||||
|
|
||||||
|
return autocomplete, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *App) UpdateOAuthUserAttrs(userData io.Reader, user *model.User, provider einterfaces.OauthProvider, service string) *model.AppError {
|
func (a *App) UpdateOAuthUserAttrs(userData io.Reader, user *model.User, provider einterfaces.OauthProvider, service string) *model.AppError {
|
||||||
oauthUser := provider.GetUserFromJson(userData)
|
oauthUser := provider.GetUserFromJson(userData)
|
||||||
if oauthUser == nil {
|
if oauthUser == nil {
|
||||||
@@ -1922,8 +1990,7 @@ func (a *App) UpdateOAuthUserAttrs(userData io.Reader, user *model.User, provide
|
|||||||
user = users.New
|
user = users.New
|
||||||
a.InvalidateCacheForUser(user.Id)
|
a.InvalidateCacheForUser(user.Id)
|
||||||
|
|
||||||
esInterface := a.Elasticsearch
|
if a.IsESIndexingEnabled() {
|
||||||
if esInterface != nil && *a.Config().ElasticsearchSettings.EnableIndexing {
|
|
||||||
a.Srv.Go(func() {
|
a.Srv.Go(func() {
|
||||||
if err := a.indexUser(user); err != nil {
|
if err := a.indexUser(user); err != nil {
|
||||||
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err))
|
mlog.Error("Encountered error indexing user", mlog.String("user_id", user.Id), mlog.Err(err))
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user