mirror of
https://github.com/mattermost/mattermost.git
synced 2026-02-03 20:40:00 -05:00
Some checks are pending
API / build (push) Waiting to run
Server CI / Compute Go Version (push) Waiting to run
Server CI / Check mocks (push) Blocked by required conditions
Server CI / Check go mod tidy (push) Blocked by required conditions
Server CI / check-style (push) Blocked by required conditions
Server CI / Check serialization methods for hot structs (push) Blocked by required conditions
Server CI / Vet API (push) Blocked by required conditions
Server CI / Check migration files (push) Blocked by required conditions
Server CI / Generate email templates (push) Blocked by required conditions
Server CI / Check store layers (push) Blocked by required conditions
Server CI / Check mmctl docs (push) Blocked by required conditions
Server CI / Postgres with binary parameters (push) Blocked by required conditions
Server CI / Postgres (push) Blocked by required conditions
Server CI / Postgres (FIPS) (push) Blocked by required conditions
Server CI / Generate Test Coverage (push) Blocked by required conditions
Server CI / Run mmctl tests (push) Blocked by required conditions
Server CI / Run mmctl tests (FIPS) (push) Blocked by required conditions
Server CI / Build mattermost server app (push) Blocked by required conditions
Web App CI / check-lint (push) Waiting to run
Web App CI / check-i18n (push) Blocked by required conditions
Web App CI / check-types (push) Blocked by required conditions
Web App CI / test (platform) (push) Blocked by required conditions
Web App CI / test (mattermost-redux) (push) Blocked by required conditions
Web App CI / test (channels shard 1/4) (push) Blocked by required conditions
Web App CI / test (channels shard 2/4) (push) Blocked by required conditions
Web App CI / test (channels shard 3/4) (push) Blocked by required conditions
Web App CI / test (channels shard 4/4) (push) Blocked by required conditions
Web App CI / upload-coverage (push) Blocked by required conditions
Web App CI / build (push) Blocked by required conditions
* Add read receipt store for burn on read message types * update mocks * fix invalidation target * have consistent case on index creation * Add temporary posts table * add mock * add transaction support * reflect review comments * wip: Add reveal endpoint * user check error id instead * wip: Add ws events and cleanup for burn on read posts * add burn endpoint for explicitly burning messages * add translations * Added logic to associate files of BoR post with the post * Added test * fixes * disable pinning posts and review comments * MM-66594 - Burn on read UI integration (#34647) * MM-66244 - add BoR visual components to message editor * MM-66246 - BoR visual indicator for sender and receiver * MM-66607 - bor - add timer countdown and autodeletion * add the system console max time to live config * use the max expire at and create global scheduler to register bor messages * use seconds for BoR config values in BE * implement the read by text shown in the tooltip logic * unestack the posts from same receiver and BoR and fix styling * avoid opening reply RHS * remove unused dispatchers * persis the BoR label in the drafts * move expiration value to metadata * adjust unit tests to metadata insted of props * code clean up and some performance improvements; add period grace for deletion too * adjust migration serie number * hide bor messages when config is off * performance improvements on post component and code clean up * keep bor existing post functionality if config is disabled * Add read receipt store for burn on read message types * Add temporary posts table * add transaction support * reflect review comments * wip: Add reveal endpoint * user check error id instead * wip: Add ws events and cleanup for burn on read posts * avoid reacting to unrevealed bor messages * adjust migration number * Add read receipt store for burn on read message types * have consistent case on index creation * Add temporary posts table * add mock * add transaction support * reflect review comments * wip: Add reveal endpoint * user check error id instead * wip: Add ws events and cleanup for burn on read posts * add burn endpoint for explicitly burning messages * adjust post reveal and type with backend changes * use real config values, adjust icon usage and style * adjust the delete from from sender and receiver * improve self deleting logic by placing in badge, use burn endpoint * adjust websocket events handling for the read by sender label information * adjust styling for concealed and error state * update burn-on-read post event handling for improved recipient tracking and multi-device sync * replace burn_on_read with type in database migrations and model * remove burn_on_read metadata from PostMetadata and related structures * Added logic to associate files of BoR post with the post * Added test * adjust migration name and fix linter * Add read receipt store for burn on read message types * update mocks * have consistent case on index creation * Add temporary posts table * add mock * add transaction support * reflect review comments * wip: Add reveal endpoint * user check error id instead * wip: Add ws events and cleanup for burn on read posts * add burn endpoint for explicitly burning messages * Added logic to associate files of BoR post with the post * Added test * disable pinning posts and review comments * show attachment on bor reveal * remove unused translation * Enhance burn-on-read post handling and refine previous post ID retrieval logic * adjust the returning chunk to work with bor messages * read temp post from master db * read from master * show the copy link button to the sender * revert unnecessary check * restore correct json tag * remove unused error handling and clarify burn-on-read comment * improve type safety and use proper selectors * eliminate code duplication in deletion handler * optimize performance and add documentation * delete bor message for sender once all receivers reveal it * add burn on read to scheduled posts * add feature enable check * use master to avoid all read recipients race condition --------- Co-authored-by: Mattermost Build <build@mattermost.com> Co-authored-by: Ibrahim Serdar Acikgoz <serdaracikgoz86@gmail.com> Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com> * squash migrations into single file * add configuration for the scheduler * don't run messagehasbeenposted hook * remove parallel tests on burn on read * add clean up for closing opened modals from previous tests * simplify delete menu item rendering * add cleanup step to close open modals after each test to prevent pollution * streamline delete button visibility logic for Burn on Read posts * improve reliability of closing post menu and modals by using body ESC key --------- Co-authored-by: Harshil Sharma <harshilsharma63@gmail.com> Co-authored-by: Pablo Vélez <pablovv2012@gmail.com> Co-authored-by: Mattermost Build <build@mattermost.com>
313 lines
9 KiB
Go
313 lines
9 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package commands
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
|
|
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/client"
|
|
"github.com/mattermost/mattermost/server/v8/cmd/mmctl/printer"
|
|
|
|
"github.com/hashicorp/go-multierror"
|
|
"github.com/pkg/errors"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var PostCmd = &cobra.Command{
|
|
Use: "post",
|
|
Short: "Management of posts",
|
|
}
|
|
|
|
var PostCreateCmd = &cobra.Command{
|
|
Use: "create",
|
|
Short: "Create a post",
|
|
Example: ` post create myteam:mychannel --message "some text for the post"`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: withClient(postCreateCmdF),
|
|
}
|
|
|
|
var PostListCmd = &cobra.Command{
|
|
Use: "list",
|
|
Short: "List posts for a channel",
|
|
Example: ` post list myteam:mychannel
|
|
post list myteam:mychannel --number 20`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: withClient(postListCmdF),
|
|
}
|
|
|
|
var PostRevealCmd = &cobra.Command{
|
|
Use: "reveal [post]",
|
|
Short: "Reveal a post",
|
|
Example: ` post reveal udjmt396tjghi8wnsk3a1qs1sw`,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: withClient(revealPostCmdF),
|
|
}
|
|
|
|
var PostDeleteCmd = &cobra.Command{
|
|
Use: "delete [posts]",
|
|
Short: "Mark posts as deleted or permanently delete posts with the --permanent flag",
|
|
Long: `This command will mark the post as deleted and remove it from the user's clients, but it does not permanently delete the post from the database. Please use the --permanent flag to permanently delete a post and its attachments from your database.`,
|
|
Example: ` # Mark Post as deleted
|
|
$ mmctl post delete udjmt396tjghi8wnsk3a1qs1sw
|
|
|
|
# Permanently delete a post and it's file contents from the database and filestore
|
|
$ mmctl post delete udjmt396tjghi8wnsk3a1qs1sw --permanent
|
|
|
|
# Permanently delete multiple posts and their file contents from the database and filestore
|
|
$ mmctl post delete udjmt396tjghi8wnsk3a1qs1sw 7jgcjt7tyjyyu83qz81wo84w6o --permanent`,
|
|
Args: cobra.MinimumNArgs(1),
|
|
RunE: withClient(deletePostsCmdF),
|
|
}
|
|
|
|
const (
|
|
ISO8601Layout = "2006-01-02T15:04:05-07:00"
|
|
PostTimeFormat = "2006-01-02 15:04:05-07:00"
|
|
)
|
|
|
|
func init() {
|
|
PostCreateCmd.Flags().StringP("message", "m", "", "Message for the post")
|
|
PostCreateCmd.Flags().StringP("reply-to", "r", "", "Post id to reply to")
|
|
PostCreateCmd.Flags().BoolP("burn-on-read", "b", false, "Message will be deleted after a certain time after being read")
|
|
|
|
PostListCmd.Flags().IntP("number", "n", 20, "Number of messages to list")
|
|
PostListCmd.Flags().BoolP("show-ids", "i", false, "Show posts ids")
|
|
PostListCmd.Flags().BoolP("follow", "f", false, "Output appended data as new messages are posted to the channel")
|
|
PostListCmd.Flags().StringP("since", "s", "", "List messages posted after a certain time (ISO 8601)")
|
|
|
|
PostDeleteCmd.Flags().Bool("confirm", false, "Confirm you really want to delete the post and a DB backup has been performed")
|
|
PostDeleteCmd.Flags().Bool("permanent", false, "Permanently delete the post and its contents from the database")
|
|
|
|
PostCmd.AddCommand(
|
|
PostCreateCmd,
|
|
PostListCmd,
|
|
PostDeleteCmd,
|
|
PostRevealCmd,
|
|
)
|
|
|
|
RootCmd.AddCommand(PostCmd)
|
|
}
|
|
|
|
func postCreateCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
|
message, _ := cmd.Flags().GetString("message")
|
|
if message == "" {
|
|
return errors.New("message cannot be empty")
|
|
}
|
|
|
|
replyTo, _ := cmd.Flags().GetString("reply-to")
|
|
if replyTo != "" {
|
|
replyToPost, _, err := c.GetPost(context.TODO(), replyTo, "")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if replyToPost.RootId != "" {
|
|
replyTo = replyToPost.RootId
|
|
}
|
|
}
|
|
|
|
channel := getChannelFromChannelArg(c, args[0])
|
|
if channel == nil {
|
|
return errors.New("Unable to find channel '" + args[0] + "'")
|
|
}
|
|
|
|
post := &model.Post{
|
|
ChannelId: channel.Id,
|
|
Message: message,
|
|
RootId: replyTo,
|
|
}
|
|
|
|
if burnOnRead, _ := cmd.Flags().GetBool("burn-on-read"); burnOnRead {
|
|
post.Type = model.PostTypeBurnOnRead
|
|
}
|
|
|
|
url := "/posts" + "?set_online=false"
|
|
data, err := post.ToJSON()
|
|
if err != nil {
|
|
return fmt.Errorf("could not decode post: %w", err)
|
|
}
|
|
|
|
if _, err := c.DoAPIPost(context.TODO(), url, data); err != nil {
|
|
return fmt.Errorf("could not create post: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func eventDataToPost(eventData map[string]any) (*model.Post, error) {
|
|
post := &model.Post{}
|
|
var rawPost string
|
|
for k, v := range eventData {
|
|
if k == "post" {
|
|
rawPost = v.(string)
|
|
}
|
|
}
|
|
|
|
err := json.Unmarshal([]byte(rawPost), &post)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return post, nil
|
|
}
|
|
|
|
func printPost(c client.Client, post *model.Post, usernames map[string]string, showIds, showTimestamp bool) {
|
|
var username string
|
|
|
|
if usernames[post.UserId] != "" {
|
|
username = usernames[post.UserId]
|
|
} else {
|
|
user, _, err := c.GetUser(context.TODO(), post.UserId, "")
|
|
if err != nil {
|
|
username = post.UserId
|
|
} else {
|
|
usernames[post.UserId] = user.Username
|
|
username = user.Username
|
|
}
|
|
}
|
|
|
|
postTime := model.GetTimeForMillis(post.CreateAt)
|
|
createdAt := postTime.Format(PostTimeFormat)
|
|
|
|
var templatedMessage string
|
|
|
|
if showTimestamp {
|
|
templatedMessage = fmt.Sprintf("{{ if eq .Type \"burn_on_read\" }}🔥 {{ end }}\u001b[32m%s\u001b[0m \u001b[34;1m[%s]\u001b[0m {{.Message}}", createdAt, username)
|
|
} else {
|
|
if showIds {
|
|
templatedMessage = fmt.Sprintf("{{ if eq .Type \"burn_on_read\" }}🔥 {{ end }}\u001b[31m%s\u001b[0m \u001b[34;1m[%s]\u001b[0m {{.Message}}", post.Id, username)
|
|
} else {
|
|
templatedMessage = fmt.Sprintf("{{ if eq .Type \"burn_on_read\" }}🔥 {{ end }}\u001b[34;1m[%s]\u001b[0m {{.Message}}", username)
|
|
}
|
|
}
|
|
|
|
if post.Type == model.PostTypeBurnOnRead {
|
|
expireAt := post.Metadata.ExpireAt
|
|
if expireAt != 0 {
|
|
dur := time.Until(time.UnixMilli(expireAt))
|
|
templatedMessage = fmt.Sprintf("%s (expires in %s)", templatedMessage, dur.String())
|
|
}
|
|
}
|
|
printer.PrintT(templatedMessage, post)
|
|
}
|
|
|
|
func getPostList(client client.Client, channelID, since string, perPage int) (*model.PostList, *model.Response, error) {
|
|
if since == "" {
|
|
return client.GetPostsForChannel(context.TODO(), channelID, 0, perPage, "", false, false)
|
|
}
|
|
|
|
sinceTime, err := time.Parse(ISO8601Layout, since)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("invalid since time '%s'", since)
|
|
}
|
|
|
|
sinceTimeMillis := model.GetMillisForTime(sinceTime)
|
|
return client.GetPostsSince(context.TODO(), channelID, sinceTimeMillis, false)
|
|
}
|
|
|
|
func postListCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
|
printer.SetSingle(true)
|
|
|
|
channel := getChannelFromChannelArg(c, args[0])
|
|
if channel == nil {
|
|
return errors.New("Unable to find channel '" + args[0] + "'")
|
|
}
|
|
|
|
number, _ := cmd.Flags().GetInt("number")
|
|
showIds, _ := cmd.Flags().GetBool("show-ids")
|
|
follow, _ := cmd.Flags().GetBool("follow")
|
|
since, _ := cmd.Flags().GetString("since")
|
|
|
|
postList, _, err := getPostList(c, channel.Id, since, number)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
posts := postList.ToSlice()
|
|
showTimestamp := since != ""
|
|
usernames := map[string]string{}
|
|
for i := 1; i <= len(posts); i++ {
|
|
post := posts[len(posts)-i]
|
|
printPost(c, post, usernames, showIds, showTimestamp)
|
|
}
|
|
|
|
var multiErr *multierror.Error
|
|
if follow {
|
|
ws, err := InitWebSocketClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
appErr := ws.Connect()
|
|
if appErr != nil {
|
|
return errors.New(appErr.Error())
|
|
}
|
|
|
|
ws.Listen()
|
|
for {
|
|
event := <-ws.EventChannel
|
|
if event.EventType() == model.WebsocketEventPosted {
|
|
post, err := eventDataToPost(event.GetData())
|
|
if err != nil {
|
|
printer.PrintError("Error parsing incoming post: " + err.Error())
|
|
multiErr = multierror.Append(multiErr, err)
|
|
}
|
|
if post.ChannelId == channel.Id {
|
|
printPost(c, post, usernames, showIds, showTimestamp)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return multiErr.ErrorOrNil()
|
|
}
|
|
|
|
func deletePostsCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
|
permanent, err := cmd.Flags().GetBool("permanent")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
confirmFlag, _ := cmd.Flags().GetBool("confirm")
|
|
if !confirmFlag && permanent {
|
|
if err = getConfirmation("Are you sure you want to delete the posts specified?", true); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
var result *multierror.Error
|
|
var deleteFunc func(ctx context.Context, postID string) (*model.Response, error)
|
|
|
|
if permanent {
|
|
deleteFunc = c.PermanentDeletePost
|
|
} else {
|
|
deleteFunc = c.DeletePost
|
|
}
|
|
|
|
for _, postID := range args {
|
|
isValidId := model.IsValidId(postID)
|
|
if !isValidId {
|
|
printer.PrintError(fmt.Sprintf("Invalid postID: %s", postID))
|
|
result = multierror.Append(result, err)
|
|
continue
|
|
}
|
|
if _, err := deleteFunc(context.TODO(), postID); err != nil {
|
|
printer.PrintError(fmt.Sprintf("Error deleting post: %s. Error: %s", postID, err.Error()))
|
|
result = multierror.Append(result, err)
|
|
continue
|
|
}
|
|
printer.Print(fmt.Sprintf("%s successfully deleted", postID))
|
|
}
|
|
return result.ErrorOrNil()
|
|
}
|
|
|
|
func revealPostCmdF(c client.Client, cmd *cobra.Command, args []string) error {
|
|
postID := args[0]
|
|
post, _, err := c.RevealPost(context.TODO(), postID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
printPost(c, post, map[string]string{}, false, false)
|
|
return nil
|
|
}
|