Exemple #1
0
func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel) []string {
	pchan := Srv.Store.User().GetProfilesInChannel(channel.Id, -1, -1, true)
	fchan := Srv.Store.FileInfo().GetForPost(post.Id)

	var profileMap map[string]*model.User
	if result := <-pchan; result.Err != nil {
		l4g.Error(utils.T("api.post.handle_post_events_and_forget.profiles.error"), c.TeamId, result.Err)
		return nil
	} else {
		profileMap = result.Data.(map[string]*model.User)
	}

	// If the user who made the post is mention don't send a notification
	if _, ok := profileMap[post.UserId]; !ok {
		l4g.Error(utils.T("api.post.send_notifications_and_forget.user_id.error"), post.UserId)
		return nil
	}

	mentionedUserIds := make(map[string]bool)
	allActivityPushUserIds := []string{}
	hereNotification := false
	updateMentionChans := []store.StoreChannel{}

	if channel.Type == model.CHANNEL_DIRECT {
		var otherUserId string
		if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId {
			otherUserId = userIds[1]
		} else {
			otherUserId = userIds[0]
		}

		mentionedUserIds[otherUserId] = true
	} else {
		keywords := getMentionKeywordsInChannel(profileMap)

		var potentialOtherMentions []string
		mentionedUserIds, potentialOtherMentions, hereNotification = getExplicitMentions(post.Message, keywords)

		// get users that have comment thread mentions enabled
		if len(post.RootId) > 0 {
			if result := <-Srv.Store.Post().Get(post.RootId); result.Err != nil {
				l4g.Error(utils.T("api.post.send_notifications_and_forget.comment_thread.error"), post.RootId, result.Err)
				return nil
			} else {
				list := result.Data.(*model.PostList)

				for _, threadPost := range list.Posts {
					profile := profileMap[threadPost.UserId]
					if profile.NotifyProps["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == list.Order[0]) {
						mentionedUserIds[threadPost.UserId] = true
					}
				}
			}
		}

		// prevent the user from mentioning themselves
		if post.Props["from_webhook"] != "true" {
			delete(mentionedUserIds, post.UserId)
		}

		if len(potentialOtherMentions) > 0 {
			if result := <-Srv.Store.User().GetProfilesByUsernames(potentialOtherMentions, team.Id); result.Err == nil {
				outOfChannelMentions := result.Data.(map[string]*model.User)
				go sendOutOfChannelMentions(c, post, outOfChannelMentions)
			}
		}

		// find which users in the channel are set up to always receive mobile notifications
		for _, profile := range profileMap {
			if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL &&
				(post.UserId != profile.Id || post.Props["from_webhook"] == "true") &&
				!post.IsSystemMessage() {
				allActivityPushUserIds = append(allActivityPushUserIds, profile.Id)
			}
		}
	}

	mentionedUsersList := make([]string, 0, len(mentionedUserIds))
	for id := range mentionedUserIds {
		mentionedUsersList = append(mentionedUsersList, id)
		updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id))
	}

	senderName := ""

	var sender *model.User
	if post.IsSystemMessage() {
		senderName = c.T("system.message.name")
	} else if profile, ok := profileMap[post.UserId]; ok {
		if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" {
			senderName = value.(string)
		} else {
			senderName = profile.Username
		}
		sender = profile
	}

	if utils.Cfg.EmailSettings.SendEmailNotifications {
		for _, id := range mentionedUsersList {
			userAllowsEmails := profileMap[id].NotifyProps["email"] != "false"

			var status *model.Status
			var err *model.AppError
			if status, err = GetStatus(id); err != nil {
				status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""}
			}

			if userAllowsEmails && status.Status != model.STATUS_ONLINE {
				sendNotificationEmail(c, post, profileMap[id], channel, team, senderName, sender)
			}
		}
	}

	if hereNotification {
		if result := <-Srv.Store.Status().GetOnline(); result.Err != nil {
			l4g.Warn(utils.T("api.post.notification.here.warn"), result.Err)
			return nil
		} else {
			statuses := result.Data.([]*model.Status)
			for _, status := range statuses {
				if status.UserId == post.UserId {
					continue
				}

				_, profileFound := profileMap[status.UserId]
				_, alreadyMentioned := mentionedUserIds[status.UserId]

				if status.Status == model.STATUS_ONLINE && profileFound && !alreadyMentioned {
					mentionedUsersList = append(mentionedUsersList, status.UserId)
					updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, status.UserId))
				}
			}
		}
	}

	// Make sure all mention updates are complete to prevent race
	// Probably better to batch these DB updates in the future
	// MUST be completed before push notifications send
	for _, uchan := range updateMentionChans {
		if result := <-uchan; result.Err != nil {
			l4g.Warn(utils.T("api.post.update_mention_count_and_forget.update_error"), post.Id, post.ChannelId, result.Err)
		}
	}

	sendPushNotifications := false
	if *utils.Cfg.EmailSettings.SendPushNotifications {
		pushServer := *utils.Cfg.EmailSettings.PushNotificationServer
		if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) {
			l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn"))
			sendPushNotifications = false
		} else {
			sendPushNotifications = true
		}
	}

	if sendPushNotifications {
		for _, id := range mentionedUsersList {
			var status *model.Status
			var err *model.AppError
			if status, err = GetStatus(id); err != nil {
				status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""}
			}

			if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) {
				sendPushNotification(post, profileMap[id], channel, senderName, true)
			}
		}

		for _, id := range allActivityPushUserIds {
			if _, ok := mentionedUserIds[id]; !ok {
				var status *model.Status
				var err *model.AppError
				if status, err = GetStatus(id); err != nil {
					status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""}
				}

				if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) {
					sendPushNotification(post, profileMap[id], channel, senderName, false)
				}
			}
		}
	}

	message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POSTED, "", post.ChannelId, "", nil)
	message.Add("post", post.ToJson())
	message.Add("channel_type", channel.Type)
	message.Add("channel_display_name", channel.DisplayName)
	message.Add("sender_name", senderName)
	message.Add("team_id", team.Id)

	if len(post.FileIds) != 0 {
		message.Add("otherFile", "true")

		var infos []*model.FileInfo
		if result := <-fchan; result.Err != nil {
			l4g.Warn(utils.T("api.post.send_notifications.files.error"), post.Id, result.Err)
		} else {
			infos = result.Data.([]*model.FileInfo)
		}

		for _, info := range infos {
			if info.IsImage() {
				message.Add("image", "true")
				break
			}
		}
	}

	if len(mentionedUsersList) != 0 {
		message.Add("mentions", model.ArrayToJson(mentionedUsersList))
	}

	Publish(message)
	return mentionedUsersList
}
Exemple #2
0
func updatePost(c *Context, w http.ResponseWriter, r *http.Request) {
	post := model.PostFromJson(r.Body)

	if post == nil {
		c.SetInvalidParam("updatePost", "post")
		return
	}

	pchan := app.Srv.Store.Post().Get(post.Id)

	if !HasPermissionToChannelContext(c, post.ChannelId, model.PERMISSION_EDIT_POST) {
		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.NewLocAppError("updatePost", "api.post.update_post.find.app_error", nil, "id="+post.Id)
			c.Err.StatusCode = http.StatusBadRequest
			return
		}

		if oldPost.UserId != c.Session.UserId {
			c.Err = model.NewLocAppError("updatePost", "api.post.update_post.permissions.app_error", nil, "oldUserId="+oldPost.UserId)
			c.Err.StatusCode = http.StatusForbidden
			return
		}

		if oldPost.DeleteAt != 0 {
			c.Err = model.NewLocAppError("updatePost", "api.post.update_post.permissions.app_error", nil,
				c.T("api.post.update_post.permissions_details.app_error", map[string]interface{}{"PostId": post.Id}))
			c.Err.StatusCode = http.StatusForbidden
			return
		}

		if oldPost.IsSystemMessage() {
			c.Err = model.NewLocAppError("updatePost", "api.post.update_post.system_message.app_error", nil, "id="+post.Id)
			c.Err.StatusCode = http.StatusForbidden
			return
		}
	}

	newPost := &model.Post{}
	*newPost = *oldPost

	newPost.Message = post.Message
	newPost.EditAt = model.GetMillis()
	newPost.Hashtags, _ = model.ParseHashtags(post.Message)

	if result := <-app.Srv.Store.Post().Update(newPost, oldPost); result.Err != nil {
		c.Err = result.Err
		return
	} else {
		rpost := result.Data.(*model.Post)

		message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", rpost.ChannelId, "", nil)
		message.Add("post", rpost.ToJson())

		go app.Publish(message)

		app.InvalidateCacheForChannelPosts(rpost.ChannelId)

		w.Write([]byte(rpost.ToJson()))
	}
}
Exemple #3
0
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.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.NewLocAppError("updatePost", "api.post.update_post.find.app_error", nil, "id="+post.Id)
			c.Err.StatusCode = http.StatusBadRequest
			return
		}

		if oldPost.UserId != c.Session.UserId {
			c.Err = model.NewLocAppError("updatePost", "api.post.update_post.permissions.app_error", nil, "oldUserId="+oldPost.UserId)
			c.Err.StatusCode = http.StatusForbidden
			return
		}

		if oldPost.DeleteAt != 0 {
			c.Err = model.NewLocAppError("updatePost", "api.post.update_post.permissions.app_error", nil,
				c.T("api.post.update_post.permissions_details.app_error", map[string]interface{}{"PostId": post.Id}))
			c.Err.StatusCode = http.StatusForbidden
			return
		}

		if oldPost.IsSystemMessage() {
			c.Err = model.NewLocAppError("updatePost", "api.post.update_post.system_message.app_error", nil, "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.TeamId, rpost.ChannelId, c.Session.UserId, model.ACTION_POST_EDITED)
		message.Add("post", rpost.ToJson())

		go Publish(message)

		w.Write([]byte(rpost.ToJson()))
	}
}
Exemple #4
0
func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel, profileMap map[string]*model.User, members []model.ChannelMember) {
	if _, ok := profileMap[post.UserId]; !ok {
		l4g.Error(utils.T("api.post.send_notifications_and_forget.user_id.error"), post.UserId)
		return
	}

	mentionedUserIds := make(map[string]bool)
	alwaysNotifyUserIds := []string{}

	if channel.Type == model.CHANNEL_DIRECT {

		var otherUserId string
		if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId {
			otherUserId = userIds[1]
		} else {
			otherUserId = userIds[0]
		}

		mentionedUserIds[otherUserId] = true

	} else {
		// Find out who is a member of the channel, only keep those profiles
		tempProfileMap := make(map[string]*model.User)
		for _, member := range members {
			if profile, ok := profileMap[member.UserId]; ok {
				tempProfileMap[member.UserId] = profile
			}
		}

		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" {
				keywordMap[profile.FirstName] = append(keywordMap[profile.FirstName], profile.Id)
			}

			// Add @channel and @all to keywords if user has them turned on
			if profile.NotifyProps["channel"] == "true" {
				keywordMap["@channel"] = append(keywordMap["@channel"], profile.Id)
				keywordMap["@all"] = append(keywordMap["@all"], profile.Id)
			}

			if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL &&
				(post.UserId != profile.Id || post.Props["from_webhook"] == "true") &&
				!post.IsSystemMessage() {
				alwaysNotifyUserIds = append(alwaysNotifyUserIds, profile.Id)
			}
		}

		// Build a map as a list of unique user_ids that are mentioned in this post
		splitF := func(c rune) bool {
			return model.SplitRunes[c]
		}
		splitMessage := strings.Fields(post.Message)
		for _, word := range splitMessage {
			var userIds []string

			// Non-case-sensitive check for regular keys
			if ids, match := keywordMap[strings.ToLower(word)]; match {
				userIds = append(userIds, ids...)
			}

			// Case-sensitive check for first name
			if ids, match := keywordMap[word]; match {
				userIds = append(userIds, ids...)
			}

			if len(userIds) == 0 {
				// No matches were found with the string split just on whitespace so try further splitting
				// the message on punctuation
				splitWords := strings.FieldsFunc(word, splitF)

				for _, splitWord := range splitWords {
					// Non-case-sensitive check for regular keys
					if ids, match := keywordMap[strings.ToLower(splitWord)]; match {
						userIds = append(userIds, ids...)
					}

					// Case-sensitive check for first name
					if ids, match := keywordMap[splitWord]; match {
						userIds = append(userIds, ids...)
					}
				}
			}

			for _, userId := range userIds {
				if post.UserId == userId && post.Props["from_webhook"] != "true" {
					continue
				}

				mentionedUserIds[userId] = true
			}
		}

		for id := range mentionedUserIds {
			go updateMentionCount(post.ChannelId, id)
		}
	}

	mentionedUsersList := make([]string, 0, len(mentionedUserIds))

	senderName := ""
	if post.IsSystemMessage() {
		senderName = c.T("system.message.name")
	} else if profile, ok := profileMap[post.UserId]; ok {
		senderName = profile.Username
	}

	for id := range mentionedUserIds {
		mentionedUsersList = append(mentionedUsersList, id)
	}

	if utils.Cfg.EmailSettings.SendEmailNotifications {
		for _, id := range mentionedUsersList {
			userAllowsEmails := profileMap[id].NotifyProps["email"] != "false"

			if userAllowsEmails && (profileMap[id].IsAway() || profileMap[id].IsOffline()) {
				sendNotificationEmail(c, post, profileMap[id], channel, team, senderName)
			}
		}
	}

	sendPushNotifications := false
	if *utils.Cfg.EmailSettings.SendPushNotifications {
		pushServer := *utils.Cfg.EmailSettings.PushNotificationServer
		if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) {
			l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn"))
			sendPushNotifications = false
		} else {
			sendPushNotifications = true
		}
	}

	if sendPushNotifications {
		for _, id := range mentionedUsersList {
			if profileMap[id].NotifyProps["push"] != "none" {
				sendPushNotification(post, profileMap[id], channel, senderName, true)
			}
		}
		for _, id := range alwaysNotifyUserIds {
			if _, ok := mentionedUserIds[id]; !ok {
				sendPushNotification(post, profileMap[id], channel, senderName, false)
			}
		}
	}

	message := model.NewMessage(c.TeamId, post.ChannelId, post.UserId, model.ACTION_POSTED)
	message.Add("post", post.ToJson())
	message.Add("channel_type", channel.Type)
	message.Add("channel_display_name", channel.DisplayName)
	message.Add("sender_name", senderName)
	message.Add("team_id", team.Id)

	if len(post.Filenames) != 0 {
		message.Add("otherFile", "true")

		for _, filename := range post.Filenames {
			ext := filepath.Ext(filename)
			if model.IsFileExtImage(ext) {
				message.Add("image", "true")
				break
			}
		}
	}

	if len(mentionedUsersList) != 0 {
		message.Add("mentions", model.ArrayToJson(mentionedUsersList))
	}

	go Publish(message)
}
Exemple #5
0
func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel) {
	// get profiles for all users we could be mentioning
	pchan := Srv.Store.User().GetProfiles(c.TeamId)
	dpchan := Srv.Store.User().GetDirectProfiles(c.Session.UserId)
	mchan := Srv.Store.Channel().GetMembers(post.ChannelId)

	var profileMap map[string]*model.User
	if result := <-pchan; result.Err != nil {
		l4g.Error(utils.T("api.post.handle_post_events_and_forget.profiles.error"), c.TeamId, result.Err)
		return
	} else {
		profileMap = result.Data.(map[string]*model.User)
	}

	if result := <-dpchan; result.Err != nil {
		l4g.Error(utils.T("api.post.handle_post_events_and_forget.profiles.error"), c.TeamId, result.Err)
		return
	} else {
		dps := result.Data.(map[string]*model.User)
		for k, v := range dps {
			profileMap[k] = v
		}
	}

	if _, ok := profileMap[post.UserId]; !ok {
		l4g.Error(utils.T("api.post.send_notifications_and_forget.user_id.error"), post.UserId)
		return
	}
	// using a map as a pseudo-set since we're checking for containment a lot
	members := make(map[string]string)
	if result := <-mchan; result.Err != nil {
		l4g.Error(utils.T("api.post.handle_post_events_and_forget.members.error"), post.ChannelId, result.Err)
		return
	} else {
		for _, member := range result.Data.([]model.ChannelMember) {
			members[member.UserId] = member.UserId
		}
	}

	mentionedUserIds := make(map[string]bool)
	allActivityPushUserIds := []string{}
	hereNotification := false
	updateMentionChans := []store.StoreChannel{}

	if channel.Type == model.CHANNEL_DIRECT {
		var otherUserId string
		if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId {
			otherUserId = userIds[1]
		} else {
			otherUserId = userIds[0]
		}

		mentionedUserIds[otherUserId] = true
	} else {
		keywords := getMentionKeywords(profileMap, members)

		// get users that are explicitly mentioned
		var mentioned map[string]bool
		mentioned, hereNotification = getExplicitMentions(post.Message, keywords)

		// get users that have comment thread mentions enabled
		if len(post.RootId) > 0 {
			if result := <-Srv.Store.Post().Get(post.RootId); result.Err != nil {
				l4g.Error(utils.T("api.post.send_notifications_and_forget.comment_thread.error"), post.RootId, result.Err)
				return
			} else {
				list := result.Data.(*model.PostList)

				for _, threadPost := range list.Posts {
					profile := profileMap[threadPost.UserId]
					if profile.NotifyProps["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == list.Order[0]) {
						mentioned[threadPost.UserId] = true
					}
				}
			}
		}

		// prevent the user from mentioning themselves
		if post.Props["from_webhook"] != "true" {
			delete(mentioned, post.UserId)
		}

		outOfChannelMentions := make(map[string]bool)
		for id := range mentioned {
			if _, inChannel := members[id]; inChannel {
				mentionedUserIds[id] = true
			} else {
				outOfChannelMentions[id] = true
			}
		}

		go sendOutOfChannelMentions(c, post, profileMap, outOfChannelMentions)

		// find which users in the channel are set up to always receive mobile notifications
		for id := range members {
			profile := profileMap[id]
			if profile == nil {
				l4g.Warn(utils.T("api.post.notification.member_profile.warn"), id)
				continue
			}

			if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL &&
				(post.UserId != profile.Id || post.Props["from_webhook"] == "true") &&
				!post.IsSystemMessage() {
				allActivityPushUserIds = append(allActivityPushUserIds, profile.Id)
			}
		}
	}

	mentionedUsersList := make([]string, 0, len(mentionedUserIds))

	senderName := ""

	var sender *model.User
	if post.IsSystemMessage() {
		senderName = c.T("system.message.name")
	} else if profile, ok := profileMap[post.UserId]; ok {
		senderName = profile.Username
		sender = profile
	}

	for id := range mentionedUserIds {
		mentionedUsersList = append(mentionedUsersList, id)
		updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id))
	}

	if utils.Cfg.EmailSettings.SendEmailNotifications {
		for _, id := range mentionedUsersList {
			userAllowsEmails := profileMap[id].NotifyProps["email"] != "false"

			var status *model.Status
			var err *model.AppError
			if status, err = GetStatus(id); err != nil {
				status = &model.Status{id, model.STATUS_OFFLINE, false, 0}
			}

			if userAllowsEmails && status.Status != model.STATUS_ONLINE {
				sendNotificationEmail(c, post, profileMap[id], channel, team, senderName, sender)
			}
		}
	}

	if hereNotification {
		if result := <-Srv.Store.Status().GetOnline(); result.Err != nil {
			l4g.Warn(utils.T("api.post.notification.here.warn"), result.Err)
			return
		} else {
			statuses := result.Data.([]*model.Status)
			for _, status := range statuses {
				if status.UserId == post.UserId {
					continue
				}

				_, profileFound := profileMap[status.UserId]
				_, isChannelMember := members[status.UserId]
				_, alreadyMentioned := mentionedUserIds[status.UserId]

				if status.Status == model.STATUS_ONLINE && profileFound && isChannelMember && !alreadyMentioned {
					mentionedUsersList = append(mentionedUsersList, status.UserId)
					updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, status.UserId))
				}
			}
		}
	}

	// Make sure all mention updates are complete to prevent race
	// Probably better to batch these DB updates in the future
	// MUST be completed before push notifications send
	for _, uchan := range updateMentionChans {
		if result := <-uchan; result.Err != nil {
			l4g.Warn(utils.T("api.post.update_mention_count_and_forget.update_error"), post.Id, post.ChannelId, result.Err)
		}
	}

	sendPushNotifications := false
	if *utils.Cfg.EmailSettings.SendPushNotifications {
		pushServer := *utils.Cfg.EmailSettings.PushNotificationServer
		if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) {
			l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn"))
			sendPushNotifications = false
		} else {
			sendPushNotifications = true
		}
	}

	if sendPushNotifications {
		for _, id := range mentionedUsersList {
			var status *model.Status
			var err *model.AppError
			if status, err = GetStatus(id); err != nil {
				status = &model.Status{id, model.STATUS_OFFLINE, false, 0}
			}

			if profileMap[id].StatusAllowsPushNotification(status) {
				sendPushNotification(post, profileMap[id], channel, senderName, true)
			}
		}

		for _, id := range allActivityPushUserIds {
			if _, ok := mentionedUserIds[id]; !ok {
				var status *model.Status
				var err *model.AppError
				if status, err = GetStatus(id); err != nil {
					status = &model.Status{id, model.STATUS_OFFLINE, false, 0}
				}

				if profileMap[id].StatusAllowsPushNotification(status) {
					sendPushNotification(post, profileMap[id], channel, senderName, false)
				}
			}
		}
	}

	message := model.NewWebSocketEvent(c.TeamId, post.ChannelId, post.UserId, model.WEBSOCKET_EVENT_POSTED)
	message.Add("post", post.ToJson())
	message.Add("channel_type", channel.Type)
	message.Add("channel_display_name", channel.DisplayName)
	message.Add("sender_name", senderName)
	message.Add("team_id", team.Id)

	if len(post.Filenames) != 0 {
		message.Add("otherFile", "true")

		for _, filename := range post.Filenames {
			ext := filepath.Ext(filename)
			if model.IsFileExtImage(ext) {
				message.Add("image", "true")
				break
			}
		}
	}

	if len(mentionedUsersList) != 0 {
		message.Add("mentions", model.ArrayToJson(mentionedUsersList))
	}

	go Publish(message)
}
Exemple #6
0
func sendNotifications(c *Context, post *model.Post, team *model.Team, channel *model.Channel, profileMap map[string]*model.User, members []model.ChannelMember) {
	if _, ok := profileMap[post.UserId]; !ok {
		l4g.Error(utils.T("api.post.send_notifications_and_forget.user_id.error"), post.UserId)
		return
	}

	mentionedUserIds := make(map[string]bool)
	alwaysNotifyUserIds := []string{}
	hereNotification := false
	updateMentionChans := []store.StoreChannel{}

	if channel.Type == model.CHANNEL_DIRECT {

		var otherUserId string
		if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId {
			otherUserId = userIds[1]
		} else {
			otherUserId = userIds[0]
		}

		mentionedUserIds[otherUserId] = true

	} else {
		// Find out who is a member of the channel, only keep those profiles
		tempProfileMap := make(map[string]*model.User)
		for _, member := range members {
			if profile, ok := profileMap[member.UserId]; ok {
				tempProfileMap[member.UserId] = profile
			}
		}

		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" {
				keywordMap[profile.FirstName] = append(keywordMap[profile.FirstName], profile.Id)
			}

			// Add @channel and @all to keywords if user has them turned on
			if profile.NotifyProps["channel"] == "true" {
				keywordMap["@channel"] = append(keywordMap["@channel"], profile.Id)
				keywordMap["@all"] = append(keywordMap["@all"], profile.Id)
			}

			if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL &&
				(post.UserId != profile.Id || post.Props["from_webhook"] == "true") &&
				!post.IsSystemMessage() {
				alwaysNotifyUserIds = append(alwaysNotifyUserIds, profile.Id)
			}
		}

		// Build a map as a list of unique user_ids that are mentioned in this post
		splitF := func(c rune) bool {
			return model.SplitRunes[c]
		}
		splitMessage := strings.Fields(post.Message)
		var userIds []string
		for _, word := range splitMessage {
			if word == "@here" {
				hereNotification = true
				continue
			}

			// Non-case-sensitive check for regular keys
			if ids, match := keywordMap[strings.ToLower(word)]; match {
				userIds = append(userIds, ids...)
			}

			// Case-sensitive check for first name
			if ids, match := keywordMap[word]; match {
				userIds = append(userIds, ids...)
			}

			if len(userIds) == 0 {
				// No matches were found with the string split just on whitespace so try further splitting
				// the message on punctuation
				splitWords := strings.FieldsFunc(word, splitF)

				for _, splitWord := range splitWords {
					// Non-case-sensitive check for regular keys
					if ids, match := keywordMap[strings.ToLower(splitWord)]; match {
						userIds = append(userIds, ids...)
					}

					// Case-sensitive check for first name
					if ids, match := keywordMap[splitWord]; match {
						userIds = append(userIds, ids...)
					}
				}
			}
		}

		if len(post.RootId) > 0 {
			if result := <-Srv.Store.Post().Get(post.RootId); result.Err != nil {
				l4g.Error(utils.T("api.post.send_notifications_and_forget.comment_thread.error"), post.RootId, result.Err)
				return
			} else {
				list := result.Data.(*model.PostList)

				for _, threadPost := range list.Posts {
					profile := profileMap[threadPost.UserId]
					if profile.NotifyProps["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == list.Order[0]) {
						userIds = append(userIds, threadPost.UserId)
					}
				}
			}
		}

		for _, userId := range userIds {
			if post.UserId == userId && post.Props["from_webhook"] != "true" {
				continue
			}

			mentionedUserIds[userId] = true
		}

		for id := range mentionedUserIds {
			updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id))
		}
	}

	mentionedUsersList := make([]string, 0, len(mentionedUserIds))

	senderName := ""
	if post.IsSystemMessage() {
		senderName = c.T("system.message.name")
	} else if profile, ok := profileMap[post.UserId]; ok {
		senderName = profile.Username
	}

	for id := range mentionedUserIds {
		mentionedUsersList = append(mentionedUsersList, id)
	}

	if utils.Cfg.EmailSettings.SendEmailNotifications {
		for _, id := range mentionedUsersList {
			userAllowsEmails := profileMap[id].NotifyProps["email"] != "false"

			var status *model.Status
			var err *model.AppError
			if status, err = GetStatus(id); err != nil {
				status = &model.Status{id, model.STATUS_OFFLINE, 0}
			}

			if userAllowsEmails && status.Status != model.STATUS_ONLINE {
				sendNotificationEmail(c, post, profileMap[id], channel, team, senderName)
			}
		}
	}

	if hereNotification {
		if result := <-Srv.Store.Status().GetOnline(); result.Err != nil {
			l4g.Warn(utils.T("api.post.notification.here.warn"), result.Err)
			return
		} else {
			statuses := result.Data.([]*model.Status)
			for _, status := range statuses {
				if status.UserId == post.UserId {
					continue
				}

				_, profileFound := profileMap[status.UserId]
				_, alreadyAdded := mentionedUserIds[status.UserId]

				if status.Status == model.STATUS_ONLINE && profileFound && !alreadyAdded {
					mentionedUsersList = append(mentionedUsersList, status.UserId)
					updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, status.UserId))
				}
			}
		}
	}

	sendPushNotifications := false
	if *utils.Cfg.EmailSettings.SendPushNotifications {
		pushServer := *utils.Cfg.EmailSettings.PushNotificationServer
		if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) {
			l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn"))
			sendPushNotifications = false
		} else {
			sendPushNotifications = true
		}
	}

	if sendPushNotifications {
		for _, id := range mentionedUsersList {
			if profileMap[id].NotifyProps["push"] != "none" {
				sendPushNotification(post, profileMap[id], channel, senderName, true)
			}
		}
		for _, id := range alwaysNotifyUserIds {
			if _, ok := mentionedUserIds[id]; !ok {
				sendPushNotification(post, profileMap[id], channel, senderName, false)
			}
		}
	}

	message := model.NewWebSocketEvent(c.TeamId, post.ChannelId, post.UserId, model.WEBSOCKET_EVENT_POSTED)
	message.Add("post", post.ToJson())
	message.Add("channel_type", channel.Type)
	message.Add("channel_display_name", channel.DisplayName)
	message.Add("sender_name", senderName)
	message.Add("team_id", team.Id)

	if len(post.Filenames) != 0 {
		message.Add("otherFile", "true")

		for _, filename := range post.Filenames {
			ext := filepath.Ext(filename)
			if model.IsFileExtImage(ext) {
				message.Add("image", "true")
				break
			}
		}
	}

	if len(mentionedUsersList) != 0 {
		message.Add("mentions", model.ArrayToJson(mentionedUsersList))
	}

	// Make sure all mention updates are complete to prevent race
	// Probably better to batch these DB updates in the future
	for _, uchan := range updateMentionChans {
		if result := <-uchan; result.Err != nil {
			l4g.Warn(utils.T("api.post.update_mention_count_and_forget.update_error"), post.Id, post.ChannelId, result.Err)
		}
	}

	go Publish(message)
}
Exemple #7
0
func SendNotifications(post *model.Post, team *model.Team, channel *model.Channel) ([]string, *model.AppError) {
	pchan := Srv.Store.User().GetProfilesInChannel(channel.Id, -1, -1, true)
	fchan := Srv.Store.FileInfo().GetForPost(post.Id)

	var profileMap map[string]*model.User
	if result := <-pchan; result.Err != nil {
		return nil, result.Err
	} else {
		profileMap = result.Data.(map[string]*model.User)
	}

	// If the user who made the post isn't in the channel, don't send a notification
	if _, ok := profileMap[post.UserId]; !ok {
		l4g.Debug(utils.T("api.post.send_notifications.user_id.debug"), post.Id, channel.Id, post.UserId)
		return []string{}, nil
	}

	mentionedUserIds := make(map[string]bool)
	allActivityPushUserIds := []string{}
	hereNotification := false
	channelNotification := false
	allNotification := false
	updateMentionChans := []store.StoreChannel{}

	if channel.Type == model.CHANNEL_DIRECT {
		var otherUserId string
		if userIds := strings.Split(channel.Name, "__"); userIds[0] == post.UserId {
			otherUserId = userIds[1]
		} else {
			otherUserId = userIds[0]
		}

		mentionedUserIds[otherUserId] = true
		if post.Props["from_webhook"] == "true" {
			mentionedUserIds[post.UserId] = true
		}
	} else {
		keywords := GetMentionKeywordsInChannel(profileMap)

		var potentialOtherMentions []string
		mentionedUserIds, potentialOtherMentions, hereNotification, channelNotification, allNotification = GetExplicitMentions(post.Message, keywords)

		// get users that have comment thread mentions enabled
		if len(post.RootId) > 0 {
			if result := <-Srv.Store.Post().Get(post.RootId); result.Err != nil {
				return nil, result.Err
			} else {
				list := result.Data.(*model.PostList)

				for _, threadPost := range list.Posts {
					profile := profileMap[threadPost.UserId]
					if profile.NotifyProps["comments"] == "any" || (profile.NotifyProps["comments"] == "root" && threadPost.Id == list.Order[0]) {
						mentionedUserIds[threadPost.UserId] = true
					}
				}
			}
		}

		// prevent the user from mentioning themselves
		if post.Props["from_webhook"] != "true" {
			delete(mentionedUserIds, post.UserId)
		}

		if len(potentialOtherMentions) > 0 {
			if result := <-Srv.Store.User().GetProfilesByUsernames(potentialOtherMentions, team.Id); result.Err == nil {
				outOfChannelMentions := result.Data.(map[string]*model.User)
				go sendOutOfChannelMentions(post, team.Id, outOfChannelMentions)
			}
		}

		// find which users in the channel are set up to always receive mobile notifications
		for _, profile := range profileMap {
			if profile.NotifyProps["push"] == model.USER_NOTIFY_ALL &&
				(post.UserId != profile.Id || post.Props["from_webhook"] == "true") &&
				!post.IsSystemMessage() {
				allActivityPushUserIds = append(allActivityPushUserIds, profile.Id)
			}
		}
	}

	mentionedUsersList := make([]string, 0, len(mentionedUserIds))
	for id := range mentionedUserIds {
		mentionedUsersList = append(mentionedUsersList, id)
		updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, id))
	}

	var sender *model.User
	senderName := make(map[string]string)
	for _, id := range mentionedUsersList {
		senderName[id] = ""
		if post.IsSystemMessage() {
			senderName[id] = utils.T("system.message.name")
		} else if profile, ok := profileMap[post.UserId]; ok {
			if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" {
				senderName[id] = value.(string)
			} else {
				// Get the Display name preference from the receiver
				if result := <-Srv.Store.Preference().Get(id, model.PREFERENCE_CATEGORY_DISPLAY_SETTINGS, "name_format"); result.Err != nil {
					// Show default sender's name if user doesn't set display settings.
					senderName[id] = profile.Username
				} else {
					senderName[id] = profile.GetDisplayNameForPreference(result.Data.(model.Preference).Value)
				}
			}
			sender = profile
		}
	}

	var senderUsername string
	if value, ok := post.Props["override_username"]; ok && post.Props["from_webhook"] == "true" {
		senderUsername = value.(string)
	} else {
		senderUsername = profileMap[post.UserId].Username
	}

	if utils.Cfg.EmailSettings.SendEmailNotifications {
		for _, id := range mentionedUsersList {
			userAllowsEmails := profileMap[id].NotifyProps["email"] != "false"

			var status *model.Status
			var err *model.AppError
			if status, err = GetStatus(id); err != nil {
				status = &model.Status{
					UserId:         id,
					Status:         model.STATUS_OFFLINE,
					Manual:         false,
					LastActivityAt: 0,
					ActiveChannel:  "",
				}
			}

			if userAllowsEmails && status.Status != model.STATUS_ONLINE && profileMap[id].DeleteAt == 0 {
				sendNotificationEmail(post, profileMap[id], channel, team, senderName[id], sender)
			}
		}
	}

	T := utils.GetUserTranslations(profileMap[post.UserId].Locale)

	// If the channel has more than 1K users then @here is disabled
	if hereNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
		hereNotification = false
		SendEphemeralPost(
			team.Id,
			post.UserId,
			&model.Post{
				ChannelId: post.ChannelId,
				Message:   T("api.post.disabled_here", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}),
				CreateAt:  post.CreateAt + 1,
			},
		)
	}

	// If the channel has more than 1K users then @channel is disabled
	if channelNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
		SendEphemeralPost(
			team.Id,
			post.UserId,
			&model.Post{
				ChannelId: post.ChannelId,
				Message:   T("api.post.disabled_channel", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}),
				CreateAt:  post.CreateAt + 1,
			},
		)
	}

	// If the channel has more than 1K users then @all is disabled
	if allNotification && int64(len(profileMap)) > *utils.Cfg.TeamSettings.MaxNotificationsPerChannel {
		SendEphemeralPost(
			team.Id,
			post.UserId,
			&model.Post{
				ChannelId: post.ChannelId,
				Message:   T("api.post.disabled_all", map[string]interface{}{"Users": *utils.Cfg.TeamSettings.MaxNotificationsPerChannel}),
				CreateAt:  post.CreateAt + 1,
			},
		)
	}

	if hereNotification {
		if result := <-Srv.Store.Status().GetOnline(); result.Err != nil {
			return nil, result.Err
		} else {
			statuses := result.Data.([]*model.Status)
			for _, status := range statuses {
				if status.UserId == post.UserId {
					continue
				}

				_, profileFound := profileMap[status.UserId]
				_, alreadyMentioned := mentionedUserIds[status.UserId]

				if status.Status == model.STATUS_ONLINE && profileFound && !alreadyMentioned {
					mentionedUsersList = append(mentionedUsersList, status.UserId)
					updateMentionChans = append(updateMentionChans, Srv.Store.Channel().IncrementMentionCount(post.ChannelId, status.UserId))
				}
			}
		}
	}

	// Make sure all mention updates are complete to prevent race
	// Probably better to batch these DB updates in the future
	// MUST be completed before push notifications send
	for _, uchan := range updateMentionChans {
		if result := <-uchan; result.Err != nil {
			l4g.Warn(utils.T("api.post.update_mention_count_and_forget.update_error"), post.Id, post.ChannelId, result.Err)
		}
	}

	sendPushNotifications := false
	if *utils.Cfg.EmailSettings.SendPushNotifications {
		pushServer := *utils.Cfg.EmailSettings.PushNotificationServer
		if pushServer == model.MHPNS && (!utils.IsLicensed || !*utils.License.Features.MHPNS) {
			l4g.Warn(utils.T("api.post.send_notifications_and_forget.push_notification.mhpnsWarn"))
			sendPushNotifications = false
		} else {
			sendPushNotifications = true
		}
	}

	if sendPushNotifications {
		for _, id := range mentionedUsersList {
			var status *model.Status
			var err *model.AppError
			if status, err = GetStatus(id); err != nil {
				status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""}
			}

			if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) {
				sendPushNotification(post, profileMap[id], channel, senderName[id], true)
			}
		}

		for _, id := range allActivityPushUserIds {
			if _, ok := mentionedUserIds[id]; !ok {
				var status *model.Status
				var err *model.AppError
				if status, err = GetStatus(id); err != nil {
					status = &model.Status{id, model.STATUS_OFFLINE, false, 0, ""}
				}

				if DoesStatusAllowPushNotification(profileMap[id], status, post.ChannelId) {
					sendPushNotification(post, profileMap[id], channel, senderName[id], false)
				}
			}
		}
	}

	message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POSTED, "", post.ChannelId, "", nil)
	message.Add("post", post.ToJson())
	message.Add("channel_type", channel.Type)
	message.Add("channel_display_name", channel.DisplayName)
	message.Add("channel_name", channel.Name)
	message.Add("sender_name", senderUsername)
	message.Add("team_id", team.Id)

	if len(post.FileIds) != 0 {
		message.Add("otherFile", "true")

		var infos []*model.FileInfo
		if result := <-fchan; result.Err != nil {
			l4g.Warn(utils.T("api.post.send_notifications.files.error"), post.Id, result.Err)
		} else {
			infos = result.Data.([]*model.FileInfo)
		}

		for _, info := range infos {
			if info.IsImage() {
				message.Add("image", "true")
				break
			}
		}
	}

	if len(mentionedUsersList) != 0 {
		message.Add("mentions", model.ArrayToJson(mentionedUsersList))
	}

	Publish(message)
	return mentionedUsersList, nil
}