summaryrefslogtreecommitdiffstats
path: root/api/post.go
diff options
context:
space:
mode:
Diffstat (limited to 'api/post.go')
-rw-r--r--api/post.go692
1 files changed, 692 insertions, 0 deletions
diff --git a/api/post.go b/api/post.go
new file mode 100644
index 000000000..25a68304d
--- /dev/null
+++ b/api/post.go
@@ -0,0 +1,692 @@
+// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
+// See License.txt for license information.
+
+package api
+
+import (
+ l4g "code.google.com/p/log4go"
+ "fmt"
+ "github.com/gorilla/mux"
+ "github.com/mattermost/platform/model"
+ "github.com/mattermost/platform/store"
+ "github.com/mattermost/platform/utils"
+ "net/http"
+ "strconv"
+ "strings"
+ "time"
+)
+
+func InitPost(r *mux.Router) {
+ l4g.Debug("Initializing post api routes")
+
+ r.Handle("/posts/search", ApiUserRequired(searchPosts)).Methods("GET")
+
+ sr := r.PathPrefix("/channels/{id:[A-Za-z0-9]+}").Subrouter()
+ sr.Handle("/create", ApiUserRequired(createPost)).Methods("POST")
+ sr.Handle("/valet_create", ApiUserRequired(createValetPost)).Methods("POST")
+ sr.Handle("/update", ApiUserRequired(updatePost)).Methods("POST")
+ sr.Handle("/posts/{offset:[0-9]+}/{limit:[0-9]+}", ApiUserRequiredActivity(getPosts, false)).Methods("GET")
+ sr.Handle("/post/{post_id:[A-Za-z0-9]+}", ApiUserRequired(getPost)).Methods("GET")
+ sr.Handle("/post/{post_id:[A-Za-z0-9]+}/delete", ApiUserRequired(deletePost)).Methods("POST")
+}
+
+func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
+ post := model.PostFromJson(r.Body)
+ if post == nil {
+ c.SetInvalidParam("createPost", "post")
+ return
+ }
+
+ // Create and save post object to channel
+ cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, post.ChannelId, c.Session.UserId)
+
+ if !c.HasPermissionsToChannel(cchan, "createPost") {
+ return
+ }
+
+ if rp, err := CreatePost(c, post, true); err != nil {
+ c.Err = err
+
+ if strings.Contains(c.Err.Message, "parameter") {
+ c.Err.StatusCode = http.StatusBadRequest
+ }
+
+ return
+ } else {
+ w.Write([]byte(rp.ToJson()))
+ }
+}
+
+func createValetPost(c *Context, w http.ResponseWriter, r *http.Request) {
+ post := model.PostFromJson(r.Body)
+ if post == nil {
+ c.SetInvalidParam("createValetPost", "post")
+ return
+ }
+
+ // Any one with access to the team can post as valet to any open channel
+ cchan := Srv.Store.Channel().CheckOpenChannelPermissions(c.Session.TeamId, post.ChannelId)
+
+ if !c.HasPermissionsToChannel(cchan, "createValetPost") {
+ return
+ }
+
+ if rp, err := CreateValetPost(c, post); err != nil {
+ c.Err = err
+
+ if strings.Contains(c.Err.Message, "parameter") {
+ c.Err.StatusCode = http.StatusBadRequest
+ }
+
+ return
+ } else {
+ w.Write([]byte(rp.ToJson()))
+ }
+}
+
+func CreateValetPost(c *Context, post *model.Post) (*model.Post, *model.AppError) {
+ post.Hashtags, _ = model.ParseHashtags(post.Message)
+
+ post.Filenames = []string{} // no files allowed in valet posts yet
+
+ if result := <-Srv.Store.User().GetByUsername(c.Session.TeamId, "valet"); result.Err != nil {
+ // if the bot doesn't exist, create it
+ if tresult := <-Srv.Store.Team().Get(c.Session.TeamId); tresult.Err != nil {
+ return nil, tresult.Err
+ } else {
+ post.UserId = (CreateValet(c, tresult.Data.(*model.Team))).Id
+ }
+ } else {
+ post.UserId = result.Data.(*model.User).Id
+ }
+
+ var rpost *model.Post
+ if result := <-Srv.Store.Post().Save(post); result.Err != nil {
+ return nil, result.Err
+ } else {
+ rpost = result.Data.(*model.Post)
+ }
+
+ fireAndForgetNotifications(rpost, c.Session.TeamId, c.TeamUrl)
+
+ return rpost, nil
+}
+
+func CreatePost(c *Context, post *model.Post, doUpdateLastViewed bool) (*model.Post, *model.AppError) {
+ var pchan store.StoreChannel
+ if len(post.RootId) > 0 {
+ pchan = Srv.Store.Post().Get(post.RootId)
+ }
+
+ // Verify the parent/child relationships are correct
+ if pchan != nil {
+ if presult := <-pchan; presult.Err != nil {
+ return nil, model.NewAppError("createPost", "Invalid RootId parameter", "")
+ } else {
+ list := presult.Data.(*model.PostList)
+ if len(list.Posts) == 0 || !list.IsChannelId(post.ChannelId) {
+ return nil, model.NewAppError("createPost", "Invalid ChannelId for RootId parameter", "")
+ }
+
+ if post.ParentId == "" {
+ post.ParentId = post.RootId
+ }
+
+ if post.RootId != post.ParentId {
+ parent := list.Posts[post.ParentId]
+ if parent == nil {
+ return nil, model.NewAppError("createPost", "Invalid ParentId parameter", "")
+ }
+ }
+ }
+ }
+
+ post.Hashtags, _ = model.ParseHashtags(post.Message)
+
+ post.UserId = c.Session.UserId
+
+ if len(post.Filenames) > 0 {
+ doRemove := false
+ for i := len(post.Filenames) - 1; i >= 0; i-- {
+ path := post.Filenames[i]
+
+ doRemove = false
+ if model.UrlRegex.MatchString(path) {
+ continue
+ } else if model.PartialUrlRegex.MatchString(path) {
+ matches := model.PartialUrlRegex.FindAllStringSubmatch(path, -1)
+ if len(matches) == 0 || len(matches[0]) < 5 {
+ doRemove = true
+ }
+
+ channelId := matches[0][2]
+ if channelId != post.ChannelId {
+ doRemove = true
+ }
+
+ userId := matches[0][3]
+ if userId != post.UserId {
+ doRemove = true
+ }
+ } else {
+ doRemove = true
+ }
+ if doRemove {
+ l4g.Error("Bad filename discarded, filename=%v", path)
+ post.Filenames = append(post.Filenames[:i], post.Filenames[i+1:]...)
+ }
+ }
+ }
+
+ var rpost *model.Post
+ if result := <-Srv.Store.Post().Save(post); result.Err != nil {
+ return nil, result.Err
+ } else if doUpdateLastViewed && (<-Srv.Store.Channel().UpdateLastViewedAt(post.ChannelId, c.Session.UserId)).Err != nil {
+ return nil, result.Err
+ } else {
+ rpost = result.Data.(*model.Post)
+
+ fireAndForgetNotifications(rpost, c.Session.TeamId, c.TeamUrl)
+
+ }
+
+ return rpost, nil
+}
+
+func fireAndForgetNotifications(post *model.Post, teamId, teamUrl string) {
+
+ go func() {
+ // Get a list of user names (to be used as keywords) and ids for the given team
+ uchan := Srv.Store.User().GetProfiles(teamId)
+ echan := Srv.Store.Channel().GetMembers(post.ChannelId)
+ cchan := Srv.Store.Channel().Get(post.ChannelId)
+ tchan := Srv.Store.Team().Get(teamId)
+
+ var channel *model.Channel
+ var channelName string
+ var bodyText string
+ var subjectText string
+ if result := <-cchan; result.Err != nil {
+ l4g.Error("Failed to retrieve channel channel_id=%v, err=%v", post.ChannelId, result.Err)
+ return
+ } else {
+ channel = result.Data.(*model.Channel)
+ if channel.Type == model.CHANNEL_DIRECT {
+ bodyText = "You have one new message."
+ subjectText = "New Direct Message"
+ } else {
+ bodyText = "You have one new mention."
+ subjectText = "New Mention"
+ channelName = channel.DisplayName
+ }
+ }
+
+ var mentionedUsers []string
+
+ if result := <-uchan; result.Err != nil {
+ l4g.Error("Failed to retrieve user profiles team_id=%v, err=%v", teamId, result.Err)
+ return
+ } else {
+ profileMap := result.Data.(map[string]*model.User)
+
+ if _, ok := profileMap[post.UserId]; !ok {
+ l4g.Error("Post user_id not returned by GetProfiles user_id=%v", post.UserId)
+ return
+ }
+ senderName := profileMap[post.UserId].Username
+
+ toEmailMap := make(map[string]bool)
+
+ if channel.Type == model.CHANNEL_DIRECT {
+
+ var otherUserId string
+ if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId {
+ otherUserId = userIds[1]
+ channelName = profileMap[userIds[1]].Username
+ } else {
+ otherUserId = userIds[0]
+ channelName = profileMap[userIds[0]].Username
+ }
+
+ otherUser := profileMap[otherUserId]
+ sendEmail := true
+ if _, ok := otherUser.NotifyProps["email"]; ok && otherUser.NotifyProps["email"] == "false" {
+ sendEmail = false
+ }
+ if sendEmail && (otherUser.IsOffline() || otherUser.IsAway()) {
+ toEmailMap[otherUserId] = true
+ }
+
+ } else {
+
+ // Find out who is a member of the channel only keep those profiles
+ if eResult := <-echan; eResult.Err != nil {
+ l4g.Error("Failed to get channel members channel_id=%v err=%v", post.ChannelId, eResult.Err.Message)
+ return
+ } else {
+ tempProfileMap := make(map[string]*model.User)
+ members := eResult.Data.([]model.ChannelMember)
+ for _, member := range members {
+ tempProfileMap[member.UserId] = profileMap[member.UserId]
+ }
+
+ profileMap = tempProfileMap
+ }
+
+ // Build map for keywords
+ keywordMap := make(map[string][]string)
+ for _, profile := range profileMap {
+ if len(profile.NotifyProps["mention_keys"]) > 0 {
+
+ // Add all the user's mention keys
+ splitKeys := strings.Split(profile.NotifyProps["mention_keys"], ",")
+ for _, k := range splitKeys {
+ keywordMap[k] = append(keywordMap[strings.ToLower(k)], profile.Id)
+ }
+
+ // If turned on, add the user's case sensitive first name
+ if profile.NotifyProps["first_name"] == "true" {
+ splitName := strings.Split(profile.FullName, " ")
+ if len(splitName) > 0 && splitName[0] != "" {
+ keywordMap[splitName[0]] = append(keywordMap[splitName[0]], profile.Id)
+ }
+ }
+ }
+ }
+
+ // Build a map as a list of unique user_ids that are mentioned in this post
+ splitF := func(c rune) bool {
+ return c == ',' || c == ' ' || c == '.' || c == '!' || c == '?' || c == ':' || c == '<' || c == '>'
+ }
+ splitMessage := strings.FieldsFunc(strings.Replace(post.Message, "<br>", " ", -1), splitF)
+ for _, word := range splitMessage {
+
+ // Non-case-sensitive check for regular keys
+ userIds1, keyMatch := keywordMap[strings.ToLower(word)]
+
+ // Case-sensitive check for first name
+ userIds2, firstNameMatch := keywordMap[word]
+
+ userIds := append(userIds1, userIds2...)
+
+ // If one of the non-case-senstive keys or the first name matches the word
+ // then we add en entry to the sendEmail map
+ if keyMatch || firstNameMatch {
+ for _, userId := range userIds {
+ if post.UserId == userId {
+ continue
+ }
+ sendEmail := true
+ if _, ok := profileMap[userId].NotifyProps["email"]; ok && profileMap[userId].NotifyProps["email"] == "false" {
+ sendEmail = false
+ }
+ if sendEmail && (profileMap[userId].IsAway() || profileMap[userId].IsOffline()) {
+ toEmailMap[userId] = true
+ } else {
+ toEmailMap[userId] = false
+ }
+ }
+ }
+ }
+
+ for id, _ := range toEmailMap {
+ fireAndForgetMentionUpdate(post.ChannelId, id)
+ }
+ }
+
+ if len(toEmailMap) != 0 {
+ mentionedUsers = make([]string, 0, len(toEmailMap))
+ for k := range toEmailMap {
+ mentionedUsers = append(mentionedUsers, k)
+ }
+
+ var teamName string
+ if result := <-tchan; result.Err != nil {
+ l4g.Error("Failed to retrieve team team_id=%v, err=%v", teamId, result.Err)
+ return
+ } else {
+ teamName = result.Data.(*model.Team).Name
+ }
+
+ // Build and send the emails
+ location, _ := time.LoadLocation("UTC")
+ tm := time.Unix(post.CreateAt/1000, 0).In(location)
+
+ subjectPage := NewServerTemplatePage("post_subject", teamUrl)
+ subjectPage.Props["TeamName"] = teamName
+ subjectPage.Props["SubjectText"] = subjectText
+ subjectPage.Props["Month"] = tm.Month().String()[:3]
+ subjectPage.Props["Day"] = fmt.Sprintf("%d", tm.Day())
+ subjectPage.Props["Year"] = fmt.Sprintf("%d", tm.Year())
+
+ for id, doSend := range toEmailMap {
+
+ if !doSend {
+ continue
+ }
+
+ // skip if inactive
+ if profileMap[id].DeleteAt > 0 {
+ continue
+ }
+
+ firstName := strings.Split(profileMap[id].FullName, " ")[0]
+
+ bodyPage := NewServerTemplatePage("post_body", teamUrl)
+ bodyPage.Props["FullName"] = firstName
+ bodyPage.Props["TeamName"] = teamName
+ bodyPage.Props["ChannelName"] = channelName
+ bodyPage.Props["BodyText"] = bodyText
+ bodyPage.Props["SenderName"] = senderName
+ bodyPage.Props["Hour"] = fmt.Sprintf("%02d", tm.Hour())
+ bodyPage.Props["Minute"] = fmt.Sprintf("%02d", tm.Minute())
+ bodyPage.Props["Month"] = tm.Month().String()[:3]
+ bodyPage.Props["Day"] = fmt.Sprintf("%d", tm.Day())
+ bodyPage.Props["PostMessage"] = model.ClearMentionTags(post.Message)
+ bodyPage.Props["TeamLink"] = teamUrl + "/channels/" + channel.Name
+
+ if err := utils.SendMail(profileMap[id].Email, subjectPage.Render(), bodyPage.Render()); err != nil {
+ l4g.Error("Failed to send mention email successfully email=%v err=%v", profileMap[id].Email, err)
+ }
+
+ if len(utils.Cfg.EmailSettings.ApplePushServer) > 0 {
+ sessionChan := Srv.Store.Session().GetSessions(id)
+ if result := <-sessionChan; result.Err != nil {
+ l4g.Error("Failed to retrieve sessions in notifications id=%v, err=%v", id, result.Err)
+ } else {
+ sessions := result.Data.([]*model.Session)
+ alreadySeen := make(map[string]string)
+
+ for _, session := range sessions {
+ if len(session.DeviceId) > 0 && alreadySeen[session.DeviceId] == "" {
+
+ alreadySeen[session.DeviceId] = session.DeviceId
+
+ utils.FireAndForgetSendAppleNotify(session.DeviceId, subjectPage.Render(), 1)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ message := model.NewMessage(teamId, post.ChannelId, post.UserId, model.ACTION_POSTED)
+ message.Add("post", post.ToJson())
+ if len(mentionedUsers) != 0 {
+ message.Add("mentions", model.ArrayToJson(mentionedUsers))
+ }
+
+ store.PublishAndForget(message)
+ }()
+}
+
+func fireAndForgetMentionUpdate(channelId, userId string) {
+ go func() {
+ if result := <-Srv.Store.Channel().IncrementMentionCount(channelId, userId); result.Err != nil {
+ l4g.Error("Failed to update mention count for user_id=%v on channel_id=%v err=%v", userId, channelId, result.Err)
+ }
+ }()
+}
+
+func updatePost(c *Context, w http.ResponseWriter, r *http.Request) {
+ post := model.PostFromJson(r.Body)
+
+ if post == nil {
+ c.SetInvalidParam("updatePost", "post")
+ return
+ }
+
+ cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, post.ChannelId, c.Session.UserId)
+ pchan := Srv.Store.Post().Get(post.Id)
+
+ if !c.HasPermissionsToChannel(cchan, "updatePost") {
+ return
+ }
+
+ var oldPost *model.Post
+ if result := <-pchan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ oldPost = result.Data.(*model.PostList).Posts[post.Id]
+
+ if oldPost == nil {
+ c.Err = model.NewAppError("updatePost", "We couldn't find the existing post or comment to update.", "id="+post.Id)
+ c.Err.StatusCode = http.StatusBadRequest
+ return
+ }
+
+ if oldPost.UserId != c.Session.UserId {
+ c.Err = model.NewAppError("updatePost", "You do not have the appropriate permissions", "oldUserId="+oldPost.UserId)
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+
+ if oldPost.DeleteAt != 0 {
+ c.Err = model.NewAppError("updatePost", "You do not have the appropriate permissions", "Already delted id="+post.Id)
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+ }
+
+ hashtags, _ := model.ParseHashtags(post.Message)
+
+ if result := <-Srv.Store.Post().Update(oldPost, post.Message, hashtags); result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ rpost := result.Data.(*model.Post)
+
+ message := model.NewMessage(c.Session.TeamId, rpost.ChannelId, c.Session.UserId, model.ACTION_POST_EDITED)
+ message.Add("post_id", rpost.Id)
+ message.Add("channel_id", rpost.ChannelId)
+ message.Add("message", rpost.Message)
+
+ store.PublishAndForget(message)
+
+ w.Write([]byte(rpost.ToJson()))
+ }
+}
+
+func getPosts(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+
+ id := params["id"]
+ if len(id) != 26 {
+ c.SetInvalidParam("getPosts", "channelId")
+ return
+ }
+
+ offset, err := strconv.Atoi(params["offset"])
+ if err != nil {
+ c.SetInvalidParam("getPosts", "offset")
+ return
+ }
+
+ limit, err := strconv.Atoi(params["limit"])
+ if err != nil {
+ c.SetInvalidParam("getPosts", "limit")
+ return
+ }
+
+ cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, id, c.Session.UserId)
+ etagChan := Srv.Store.Post().GetEtag(id)
+
+ if !c.HasPermissionsToChannel(cchan, "getPosts") {
+ return
+ }
+
+ etag := (<-etagChan).Data.(string)
+
+ if HandleEtag(etag, w, r) {
+ return
+ }
+
+ pchan := Srv.Store.Post().GetPosts(id, offset, limit)
+
+ if result := <-pchan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ list := result.Data.(*model.PostList)
+
+ w.Header().Set(model.HEADER_ETAG_SERVER, etag)
+ w.Write([]byte(list.ToJson()))
+ }
+
+}
+
+func getPost(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+
+ channelId := params["id"]
+ if len(channelId) != 26 {
+ c.SetInvalidParam("getPost", "channelId")
+ return
+ }
+
+ postId := params["post_id"]
+ if len(postId) != 26 {
+ c.SetInvalidParam("getPost", "postId")
+ return
+ }
+
+ cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId)
+ pchan := Srv.Store.Post().Get(postId)
+
+ if !c.HasPermissionsToChannel(cchan, "getPost") {
+ return
+ }
+
+ if result := <-pchan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else if HandleEtag(result.Data.(*model.PostList).Etag(), w, r) {
+ return
+ } else {
+ list := result.Data.(*model.PostList)
+
+ if !list.IsChannelId(channelId) {
+ c.Err = model.NewAppError("getPost", "You do not have the appropriate permissions", "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+
+ w.Header().Set(model.HEADER_ETAG_SERVER, list.Etag())
+ w.Write([]byte(list.ToJson()))
+ }
+}
+
+func deletePost(c *Context, w http.ResponseWriter, r *http.Request) {
+ params := mux.Vars(r)
+
+ channelId := params["id"]
+ if len(channelId) != 26 {
+ c.SetInvalidParam("deletePost", "channelId")
+ return
+ }
+
+ postId := params["post_id"]
+ if len(postId) != 26 {
+ c.SetInvalidParam("deletePost", "postId")
+ return
+ }
+
+ cchan := Srv.Store.Channel().CheckPermissionsTo(c.Session.TeamId, channelId, c.Session.UserId)
+ pchan := Srv.Store.Post().Get(postId)
+
+ if !c.HasPermissionsToChannel(cchan, "deletePost") {
+ return
+ }
+
+ if result := <-pchan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ post := result.Data.(*model.PostList).Posts[postId]
+
+ if post == nil {
+ c.SetInvalidParam("deletePost", "postId")
+ return
+ }
+
+ if post.ChannelId != channelId {
+ c.Err = model.NewAppError("deletePost", "You do not have the appropriate permissions", "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+
+ if post.UserId != c.Session.UserId {
+ c.Err = model.NewAppError("deletePost", "You do not have the appropriate permissions", "")
+ c.Err.StatusCode = http.StatusForbidden
+ return
+ }
+
+ if dresult := <-Srv.Store.Post().Delete(postId, model.GetMillis()); dresult.Err != nil {
+ c.Err = dresult.Err
+ return
+ }
+
+ message := model.NewMessage(c.Session.TeamId, post.ChannelId, c.Session.UserId, model.ACTION_POST_DELETED)
+ message.Add("post_id", post.Id)
+ message.Add("channel_id", post.ChannelId)
+
+ store.PublishAndForget(message)
+
+ result := make(map[string]string)
+ result["id"] = postId
+ w.Write([]byte(model.MapToJson(result)))
+ }
+}
+
+func searchPosts(c *Context, w http.ResponseWriter, r *http.Request) {
+ terms := r.FormValue("terms")
+
+ if len(terms) == 0 {
+ c.SetInvalidParam("search", "terms")
+ return
+ }
+
+ hashtagTerms, plainTerms := model.ParseHashtags(terms)
+
+ var hchan store.StoreChannel
+ if len(hashtagTerms) != 0 {
+ hchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, hashtagTerms, true)
+ }
+
+ var pchan store.StoreChannel
+ if len(plainTerms) != 0 {
+ pchan = Srv.Store.Post().Search(c.Session.TeamId, c.Session.UserId, terms, false)
+ }
+
+ mainList := &model.PostList{}
+ if hchan != nil {
+ if result := <-hchan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ mainList = result.Data.(*model.PostList)
+ }
+ }
+
+ plainList := &model.PostList{}
+ if pchan != nil {
+ if result := <-pchan; result.Err != nil {
+ c.Err = result.Err
+ return
+ } else {
+ plainList = result.Data.(*model.PostList)
+ }
+ }
+
+ for _, postId := range plainList.Order {
+ if _, ok := mainList.Posts[postId]; !ok {
+ mainList.AddPost(plainList.Posts[postId])
+ mainList.AddOrder(postId)
+ }
+
+ }
+
+ w.Write([]byte(mainList.ToJson()))
+}