Merge branch 'master' of github.com:mattermost/mattermost-server into top-dms-clean
Этот коммит содержится в:
@@ -18,30 +18,30 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
HeaderRequestId = "X-Request-ID"
|
||||
HeaderVersionId = "X-Version-ID"
|
||||
HeaderClusterId = "X-Cluster-ID"
|
||||
HeaderEtagServer = "ETag"
|
||||
HeaderEtagClient = "If-None-Match"
|
||||
HeaderForwarded = "X-Forwarded-For"
|
||||
HeaderRealIP = "X-Real-IP"
|
||||
HeaderForwardedProto = "X-Forwarded-Proto"
|
||||
HeaderToken = "token"
|
||||
HeaderCsrfToken = "X-CSRF-Token"
|
||||
HeaderBearer = "BEARER"
|
||||
HeaderAuth = "Authorization"
|
||||
HeaderCloudToken = "X-Cloud-Token"
|
||||
HeaderRemoteclusterToken = "X-RemoteCluster-Token"
|
||||
HeaderRemoteclusterId = "X-RemoteCluster-Id"
|
||||
HeaderRequestedWith = "X-Requested-With"
|
||||
HeaderRequestedWithXML = "XMLHttpRequest"
|
||||
HeaderHasInaccessiblePosts = "Has-Inaccessible-Posts"
|
||||
HeaderRange = "Range"
|
||||
STATUS = "status"
|
||||
StatusOk = "OK"
|
||||
StatusFail = "FAIL"
|
||||
StatusUnhealthy = "UNHEALTHY"
|
||||
StatusRemove = "REMOVE"
|
||||
HeaderRequestId = "X-Request-ID"
|
||||
HeaderVersionId = "X-Version-ID"
|
||||
HeaderClusterId = "X-Cluster-ID"
|
||||
HeaderEtagServer = "ETag"
|
||||
HeaderEtagClient = "If-None-Match"
|
||||
HeaderForwarded = "X-Forwarded-For"
|
||||
HeaderRealIP = "X-Real-IP"
|
||||
HeaderForwardedProto = "X-Forwarded-Proto"
|
||||
HeaderToken = "token"
|
||||
HeaderCsrfToken = "X-CSRF-Token"
|
||||
HeaderBearer = "BEARER"
|
||||
HeaderAuth = "Authorization"
|
||||
HeaderCloudToken = "X-Cloud-Token"
|
||||
HeaderRemoteclusterToken = "X-RemoteCluster-Token"
|
||||
HeaderRemoteclusterId = "X-RemoteCluster-Id"
|
||||
HeaderRequestedWith = "X-Requested-With"
|
||||
HeaderRequestedWithXML = "XMLHttpRequest"
|
||||
HeaderFirstInaccessiblePostTime = "First-Inaccessible-Post-Time"
|
||||
HeaderRange = "Range"
|
||||
STATUS = "status"
|
||||
StatusOk = "OK"
|
||||
StatusFail = "FAIL"
|
||||
StatusUnhealthy = "UNHEALTHY"
|
||||
StatusRemove = "REMOVE"
|
||||
|
||||
ClientDir = "client"
|
||||
|
||||
@@ -103,7 +103,7 @@ func (c *Client4) boolString(value bool) string {
|
||||
|
||||
func closeBody(r *http.Response) {
|
||||
if r.Body != nil {
|
||||
_, _ = io.Copy(ioutil.Discard, r.Body)
|
||||
_, _ = io.Copy(io.Discard, r.Body)
|
||||
_ = r.Body.Close()
|
||||
}
|
||||
}
|
||||
@@ -3743,6 +3743,23 @@ func (c *Client4) SetPostUnread(userId string, postId string, collapsedThreadsSu
|
||||
return BuildResponse(r), nil
|
||||
}
|
||||
|
||||
// SetPostReminder creates a post reminder for a given post at a specified time.
|
||||
// The time needs to be in UTC epoch in seconds. It is always truncated to a
|
||||
// 5 minute resolution minimum.
|
||||
func (c *Client4) SetPostReminder(reminder *PostReminder) (*Response, error) {
|
||||
b, err := json.Marshal(reminder)
|
||||
if err != nil {
|
||||
return nil, NewAppError("SetPostReminder", "api.marshal_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
r, err := c.DoAPIPostBytes(c.userRoute(reminder.UserId)+c.postRoute(reminder.PostId)+"/reminder", b)
|
||||
if err != nil {
|
||||
return BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
return BuildResponse(r), nil
|
||||
}
|
||||
|
||||
// PinPost pin a post based on provided post id string.
|
||||
func (c *Client4) PinPost(postId string) (*Response, error) {
|
||||
r, err := c.DoAPIPost(c.postRoute(postId)+"/pin", "")
|
||||
@@ -4419,6 +4436,24 @@ func (c *Client4) GetFileInfosForPost(postId string, etag string) ([]*FileInfo,
|
||||
return list, BuildResponse(r), nil
|
||||
}
|
||||
|
||||
// GetFileInfosForPost gets all the file info objects attached to a post, including deleted
|
||||
func (c *Client4) GetFileInfosForPostIncludeDeleted(postId string, etag string) ([]*FileInfo, *Response, error) {
|
||||
r, err := c.DoAPIGet(c.postRoute(postId)+"/files/info"+"?include_deleted="+c.boolString(true), etag)
|
||||
if err != nil {
|
||||
return nil, BuildResponse(r), err
|
||||
}
|
||||
defer closeBody(r)
|
||||
|
||||
var list []*FileInfo
|
||||
if r.StatusCode == http.StatusNotModified {
|
||||
return list, BuildResponse(r), nil
|
||||
}
|
||||
if jsonErr := json.NewDecoder(r.Body).Decode(&list); jsonErr != nil {
|
||||
return nil, nil, NewAppError("GetFileInfosForPostIncludeDeleted", "api.unmarshal_error", nil, jsonErr.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return list, BuildResponse(r), nil
|
||||
}
|
||||
|
||||
// General/System Section
|
||||
|
||||
// GenerateSupportPacket downloads the generated support packet
|
||||
|
||||
@@ -67,10 +67,14 @@ type FeatureFlags struct {
|
||||
|
||||
CommandPalette bool
|
||||
|
||||
PostForwarding bool
|
||||
|
||||
AdvancedTextEditor bool
|
||||
|
||||
// Enable Boards as a product (multi-product architecture)
|
||||
BoardsProduct bool
|
||||
|
||||
PlanUpgradeButtonText string
|
||||
}
|
||||
|
||||
func (f *FeatureFlags) SetDefaults() {
|
||||
@@ -94,9 +98,11 @@ func (f *FeatureFlags) SetDefaults() {
|
||||
f.GraphQL = false
|
||||
f.InsightsEnabled = true
|
||||
f.CommandPalette = false
|
||||
f.PostForwarding = false
|
||||
f.AdvancedTextEditor = true
|
||||
f.CallsEnabled = true
|
||||
f.BoardsProduct = false
|
||||
f.PlanUpgradeButtonText = "Upgrade"
|
||||
}
|
||||
|
||||
func (f *FeatureFlags) Plugins() map[string]string {
|
||||
|
||||
@@ -141,7 +141,7 @@ func GetInfoForBytes(name string, data io.ReadSeeker, size int) (*FileInfo, *App
|
||||
extension := strings.ToLower(filepath.Ext(name))
|
||||
info.MimeType = mime.TypeByExtension(extension)
|
||||
|
||||
if extension != "" && extension[0] == '.' {
|
||||
if extension != "" {
|
||||
// The client expects a file extension without the leading period
|
||||
info.Extension = extension[1:]
|
||||
} else {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/dyatlov/go-opengraph/opengraph"
|
||||
"github.com/dyatlov/go-opengraph/opengraph/types/image"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -50,7 +51,7 @@ func truncateText(original string) string {
|
||||
return original
|
||||
}
|
||||
|
||||
func firstNImages(images []*opengraph.Image, maxImages int) []*opengraph.Image {
|
||||
func firstNImages(images []*image.Image, maxImages int) []*image.Image {
|
||||
if maxImages < 0 { // don't break stuff, if it's weird, go for sane defaults
|
||||
maxImages = LinkMetadataMaxImages
|
||||
}
|
||||
|
||||
@@ -11,14 +11,20 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/dyatlov/go-opengraph/opengraph"
|
||||
"github.com/dyatlov/go-opengraph/opengraph/types/article"
|
||||
"github.com/dyatlov/go-opengraph/opengraph/types/audio"
|
||||
"github.com/dyatlov/go-opengraph/opengraph/types/book"
|
||||
"github.com/dyatlov/go-opengraph/opengraph/types/image"
|
||||
"github.com/dyatlov/go-opengraph/opengraph/types/profile"
|
||||
"github.com/dyatlov/go-opengraph/opengraph/types/video"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const BigText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus maximus faucibus ex, vitae placerat neque feugiat ac. Nam tempus libero quis pellentesque feugiat. Cras tristique diam vel condimentum viverra. Proin molestie posuere leo. Nam pulvinar, ex quis tristique cursus, turpis ante commodo elit, a dapibus est ipsum id eros. Mauris tortor dolor, posuere ac velit vitae, faucibus viverra fusce."
|
||||
|
||||
func sampleImage(imageName string) *opengraph.Image {
|
||||
return &opengraph.Image{
|
||||
func sampleImage(imageName string) *image.Image {
|
||||
return &image.Image{
|
||||
URL: fmt.Sprintf("http://example.com/%s", imageName),
|
||||
SecureURL: fmt.Sprintf("https://example.com/%s", imageName),
|
||||
Type: "png",
|
||||
@@ -180,7 +186,7 @@ func TestLinkMetadataDeserializeDataToConcreteType(t *testing.T) {
|
||||
og := &opengraph.OpenGraph{
|
||||
URL: "http://example.com",
|
||||
Description: "Hello, world!",
|
||||
Images: []*opengraph.Image{
|
||||
Images: []*image.Image{
|
||||
{
|
||||
URL: "http://example.com/image.png",
|
||||
},
|
||||
@@ -260,24 +266,24 @@ func TestTruncateText(t *testing.T) {
|
||||
|
||||
func TestFirstNImages(t *testing.T) {
|
||||
t.Run("when empty, return an empty one", func(t *testing.T) {
|
||||
empty := make([]*opengraph.Image, 0)
|
||||
empty := make([]*image.Image, 0)
|
||||
assert.Exactly(t, firstNImages(empty, 1), empty, "Should be the same element")
|
||||
})
|
||||
t.Run("when it contains one element, return the same array", func(t *testing.T) {
|
||||
one := []*opengraph.Image{sampleImage("image.png")}
|
||||
one := []*image.Image{sampleImage("image.png")}
|
||||
assert.Exactly(t, firstNImages(one, 1), one, "Should be the same element")
|
||||
})
|
||||
t.Run("when it contains more than one element and asking for only one, return the first one", func(t *testing.T) {
|
||||
two := []*opengraph.Image{sampleImage("image.png"), sampleImage("notme.png")}
|
||||
two := []*image.Image{sampleImage("image.png"), sampleImage("notme.png")}
|
||||
assert.True(t, strings.HasSuffix(firstNImages(two, 1)[0].URL, "image.png"), "Should be the image element")
|
||||
})
|
||||
t.Run("when it contains less than asked, return the original", func(t *testing.T) {
|
||||
two := []*opengraph.Image{sampleImage("image.png"), sampleImage("notme.png")}
|
||||
two := []*image.Image{sampleImage("image.png"), sampleImage("notme.png")}
|
||||
assert.Equal(t, two, firstNImages(two, 10), "should be the same pointer")
|
||||
})
|
||||
|
||||
t.Run("asking for negative images", func(t *testing.T) {
|
||||
six := []*opengraph.Image{
|
||||
six := []*image.Image{
|
||||
sampleImage("image.png"),
|
||||
sampleImage("another.png"),
|
||||
sampleImage("yetanother.jpg"),
|
||||
@@ -300,18 +306,18 @@ func TestTruncateOpenGraph(t *testing.T) {
|
||||
SiteName: BigText,
|
||||
Locale: "[EN-en]",
|
||||
LocalesAlternate: []string{"[EN-ca]", "[ES-es]"},
|
||||
Images: []*opengraph.Image{
|
||||
Images: []*image.Image{
|
||||
sampleImage("image.png"),
|
||||
sampleImage("another.png"),
|
||||
sampleImage("yetanother.jpg"),
|
||||
sampleImage("metoo.gif"),
|
||||
sampleImage("fifth.ico"),
|
||||
sampleImage("notme.tiff")},
|
||||
Audios: []*opengraph.Audio{{}},
|
||||
Videos: []*opengraph.Video{{}},
|
||||
Article: &opengraph.Article{},
|
||||
Book: &opengraph.Book{},
|
||||
Profile: &opengraph.Profile{},
|
||||
Audios: []*audio.Audio{{}},
|
||||
Videos: []*video.Video{{}},
|
||||
Article: &article.Article{},
|
||||
Book: &book.Book{},
|
||||
Profile: &profile.Profile{},
|
||||
}
|
||||
result := TruncateOpenGraph(&og)
|
||||
assert.Nil(t, result.Article, "No article stored")
|
||||
|
||||
@@ -84,6 +84,11 @@ type PluginSetting struct {
|
||||
// For "radio" or "dropdown" settings, this is the list of pre-defined options that the user can choose
|
||||
// from.
|
||||
Options []*PluginOption `json:"options,omitempty" yaml:"options,omitempty"`
|
||||
|
||||
// The intended hosting environment for this plugin setting. Can be "cloud" or "on-prem". When this field is set,
|
||||
// and the opposite environment is running the plugin, the setting will be hidden in the admin console UI.
|
||||
// Note that this functionality is entirely client-side, so the plugin needs to handle the case of invalid submissions.
|
||||
Hosting string `json:"hosting"`
|
||||
}
|
||||
|
||||
type PluginSettingsSchema struct {
|
||||
|
||||
@@ -18,6 +18,7 @@ type PluginStatus struct {
|
||||
ClusterId string `json:"cluster_id"`
|
||||
PluginPath string `json:"plugin_path"`
|
||||
State int `json:"state"`
|
||||
Error string `json:"error"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Version string `json:"version"`
|
||||
|
||||
@@ -48,6 +48,7 @@ const (
|
||||
PostTypeSystemWarnMetricStatus = "warn_metric_status"
|
||||
PostTypeMe = "me"
|
||||
PostCustomTypePrefix = "custom_"
|
||||
PostTypeReminder = "reminder"
|
||||
|
||||
PostFileidsMaxRunes = 300
|
||||
PostFilenamesMaxRunes = 4000
|
||||
@@ -149,6 +150,13 @@ type PostPatch struct {
|
||||
HasReactions *bool `json:"has_reactions"`
|
||||
}
|
||||
|
||||
type PostReminder struct {
|
||||
TargetTime int64 `json:"target_time"`
|
||||
// These fields are only used internally for interacting with DB.
|
||||
PostId string `json:",omitempty"`
|
||||
UserId string `json:",omitempty"`
|
||||
}
|
||||
|
||||
type SearchParameter struct {
|
||||
Terms *string `json:"terms"`
|
||||
IsOrSearch *bool `json:"is_or_search"`
|
||||
@@ -736,18 +744,10 @@ func (o *Post) ToNilIfInvalid() *Post {
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *Post) RemovePreviewPost() {
|
||||
if o.Metadata == nil || o.Metadata.Embeds == nil {
|
||||
return
|
||||
}
|
||||
n := 0
|
||||
for _, embed := range o.Metadata.Embeds {
|
||||
if embed.Type != PostEmbedPermalink {
|
||||
o.Metadata.Embeds[n] = embed
|
||||
n++
|
||||
}
|
||||
}
|
||||
o.Metadata.Embeds = o.Metadata.Embeds[:n]
|
||||
func (o *Post) ForPlugin() *Post {
|
||||
p := o.Clone()
|
||||
p.Metadata = nil
|
||||
return p
|
||||
}
|
||||
|
||||
func (o *Post) GetPreviewPost() *PreviewPost {
|
||||
|
||||
@@ -16,8 +16,8 @@ type PostList struct {
|
||||
PrevPostId string `json:"prev_post_id"`
|
||||
// HasNext indicates whether there are more items to be fetched or not.
|
||||
HasNext bool `json:"has_next"`
|
||||
// HasInaccessiblePosts tells if there are inaccessible posts, past the cloud limit.
|
||||
HasInaccessiblePosts bool `json:"has_inaccessible_posts"`
|
||||
// If there are inaccessible posts, FirstInaccessiblePostTime is the time of the latest inaccessible post
|
||||
FirstInaccessiblePostTime int64 `json:"first_inaccessible_post_time"`
|
||||
}
|
||||
|
||||
func NewPostList() *PostList {
|
||||
@@ -37,15 +37,23 @@ func (o *PostList) Clone() *PostList {
|
||||
postsCopy[k] = v.Clone()
|
||||
}
|
||||
return &PostList{
|
||||
Order: orderCopy,
|
||||
Posts: postsCopy,
|
||||
NextPostId: o.NextPostId,
|
||||
PrevPostId: o.PrevPostId,
|
||||
HasNext: o.HasNext,
|
||||
HasInaccessiblePosts: o.HasInaccessiblePosts,
|
||||
Order: orderCopy,
|
||||
Posts: postsCopy,
|
||||
NextPostId: o.NextPostId,
|
||||
PrevPostId: o.PrevPostId,
|
||||
HasNext: o.HasNext,
|
||||
FirstInaccessiblePostTime: o.FirstInaccessiblePostTime,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *PostList) ForPlugin() *PostList {
|
||||
copy := o.Clone()
|
||||
for k, p := range copy.Posts {
|
||||
copy.Posts[k] = p.ForPlugin()
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
func (o *PostList) ToSlice() []*Post {
|
||||
var posts []*Post
|
||||
|
||||
|
||||
@@ -33,3 +33,9 @@ func (o *PostSearchResults) EncodeJSON(w io.Writer) error {
|
||||
o.PostList.StripActionIntegrations()
|
||||
return json.NewEncoder(w).Encode(o)
|
||||
}
|
||||
|
||||
func (o *PostSearchResults) ForPlugin() *PostSearchResults {
|
||||
copy := *o
|
||||
copy.PostList = copy.PostList.ForPlugin()
|
||||
return ©
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user