Files
mostlymatter/app/post.go
Josta Yee e86add77ad Add http_proxy support for http client (#5571)
- if 'http_proxy' environment variable is set, respect it when creating http client
- otherwise initialize a http client with timeout settings

Add ogjson to cache even when it fails

in this way we can prevent from requesting unparsable urls repeatedly

Extend expire time of cached link preview data to a week

There's no need to invalidate cache and send request again frequently

Revert timeout

Revert cache_expire_time
2017-03-20 10:20:42 -03:00

525 строки
15 KiB
Go

// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package app
import (
"net"
"net/http"
"net/url"
"os"
"regexp"
"time"
l4g "github.com/alecthomas/log4go"
"github.com/dyatlov/go-opengraph/opengraph"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
)
var (
httpClient *http.Client
httpTimeout = time.Duration(5 * time.Second)
linkWithTextRegex = regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
)
func dialTimeout(network, addr string) (net.Conn, error) {
return net.DialTimeout(network, addr, httpTimeout)
}
func init() {
p, ok := os.LookupEnv("HTTP_PROXY")
if ok {
if u, err := url.Parse(p); err == nil {
httpClient = &http.Client{
Transport: &http.Transport{
Proxy: http.ProxyURL(u),
Dial: dialTimeout,
},
}
return
}
}
httpClient = &http.Client{
Timeout: httpTimeout,
}
}
func CreatePostAsUser(post *model.Post, siteURL string) (*model.Post, *model.AppError) {
// Check that channel has not been deleted
var channel *model.Channel
if result := <-Srv.Store.Channel().Get(post.ChannelId, true); result.Err != nil {
err := model.NewLocAppError("CreatePostAsUser", "api.context.invalid_param.app_error", map[string]interface{}{"Name": "post.channel_id"}, result.Err.Error())
err.StatusCode = http.StatusBadRequest
return nil, err
} else {
channel = result.Data.(*model.Channel)
}
if channel.DeleteAt != 0 {
err := model.NewLocAppError("createPost", "api.post.create_post.can_not_post_to_deleted.error", nil, "")
err.StatusCode = http.StatusBadRequest
return nil, err
}
if rp, err := CreatePost(post, channel.TeamId, true, siteURL); err != nil {
if err.Id == "api.post.create_post.root_id.app_error" ||
err.Id == "api.post.create_post.channel_root_id.app_error" ||
err.Id == "api.post.create_post.parent_id.app_error" {
err.StatusCode = http.StatusBadRequest
}
return nil, err
} else {
// Update the LastViewAt only if the post does not have from_webhook prop set (eg. Zapier app)
if _, ok := post.Props["from_webhook"]; !ok {
if result := <-Srv.Store.Channel().UpdateLastViewedAt([]string{post.ChannelId}, post.UserId); result.Err != nil {
l4g.Error(utils.T("api.post.create_post.last_viewed.error"), post.ChannelId, post.UserId, result.Err)
}
}
return rp, nil
}
}
func CreatePost(post *model.Post, teamId string, triggerWebhooks bool, siteURL string) (*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.NewLocAppError("createPost", "api.post.create_post.root_id.app_error", nil, "")
} else {
list := presult.Data.(*model.PostList)
if len(list.Posts) == 0 || !list.IsChannelId(post.ChannelId) {
return nil, model.NewLocAppError("createPost", "api.post.create_post.channel_root_id.app_error", nil, "")
}
if post.ParentId == "" {
post.ParentId = post.RootId
}
if post.RootId != post.ParentId {
parent := list.Posts[post.ParentId]
if parent == nil {
return nil, model.NewLocAppError("createPost", "api.post.create_post.parent_id.app_error", nil, "")
}
}
}
}
post.Hashtags, _ = model.ParseHashtags(post.Message)
var rpost *model.Post
if result := <-Srv.Store.Post().Save(post); result.Err != nil {
return nil, result.Err
} else {
rpost = result.Data.(*model.Post)
}
if einterfaces.GetMetricsInterface() != nil {
einterfaces.GetMetricsInterface().IncrementPostCreate()
}
if len(post.FileIds) > 0 {
// There's a rare bug where the client sends up duplicate FileIds so protect against that
post.FileIds = utils.RemoveDuplicatesFromStringArray(post.FileIds)
for _, fileId := range post.FileIds {
if result := <-Srv.Store.FileInfo().AttachToPost(fileId, post.Id); result.Err != nil {
l4g.Error(utils.T("api.post.create_post.attach_files.error"), post.Id, post.FileIds, post.UserId, result.Err)
}
}
if einterfaces.GetMetricsInterface() != nil {
einterfaces.GetMetricsInterface().IncrementPostFileAttachment(len(post.FileIds))
}
}
if err := handlePostEvents(rpost, teamId, triggerWebhooks, siteURL); err != nil {
return nil, err
}
return rpost, nil
}
func handlePostEvents(post *model.Post, teamId string, triggerWebhooks bool, siteURL string) *model.AppError {
var tchan store.StoreChannel
if len(teamId) > 0 {
tchan = Srv.Store.Team().Get(teamId)
}
cchan := Srv.Store.Channel().Get(post.ChannelId, true)
uchan := Srv.Store.User().Get(post.UserId)
var team *model.Team
if tchan != nil {
if result := <-tchan; result.Err != nil {
return result.Err
} else {
team = result.Data.(*model.Team)
}
} else {
// Blank team for DMs
team = &model.Team{}
}
var channel *model.Channel
if result := <-cchan; result.Err != nil {
return result.Err
} else {
channel = result.Data.(*model.Channel)
}
InvalidateCacheForChannel(channel)
InvalidateCacheForChannelPosts(channel.Id)
var user *model.User
if result := <-uchan; result.Err != nil {
return result.Err
} else {
user = result.Data.(*model.User)
}
if _, err := SendNotifications(post, team, channel, user, siteURL); err != nil {
return err
}
if triggerWebhooks {
go func() {
if err := handleWebhookEvents(post, team, channel, user, siteURL); err != nil {
l4g.Error(err.Error())
}
}()
}
return nil
}
// This method only parses and processes the attachments,
// all else should be set in the post which is passed
func parseSlackAttachment(post *model.Post, attachments []*model.SlackAttachment) {
post.Type = model.POST_SLACK_ATTACHMENT
for _, attachment := range attachments {
attachment.Text = parseSlackLinksToMarkdown(attachment.Text)
attachment.Pretext = parseSlackLinksToMarkdown(attachment.Pretext)
for _, field := range attachment.Fields {
if value, ok := field.Value.(string); ok {
field.Value = parseSlackLinksToMarkdown(value)
}
}
}
post.AddProp("attachments", attachments)
}
func parseSlackLinksToMarkdown(text string) string {
return linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
}
func SendEphemeralPost(teamId, userId string, post *model.Post) *model.Post {
post.Type = model.POST_EPHEMERAL
// fill in fields which haven't been specified which have sensible defaults
if post.Id == "" {
post.Id = model.NewId()
}
if post.CreateAt == 0 {
post.CreateAt = model.GetMillis()
}
if post.Props == nil {
post.Props = model.StringInterface{}
}
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_EPHEMERAL_MESSAGE, "", post.ChannelId, userId, nil)
message.Add("post", post.ToJson())
go Publish(message)
return post
}
func UpdatePost(post *model.Post) (*model.Post, *model.AppError) {
if utils.IsLicensed {
if *utils.Cfg.ServiceSettings.AllowEditPost == model.ALLOW_EDIT_POST_NEVER {
err := model.NewLocAppError("updatePost", "api.post.update_post.permissions_denied.app_error", nil, "")
err.StatusCode = http.StatusForbidden
return nil, err
}
}
var oldPost *model.Post
if result := <-Srv.Store.Post().Get(post.Id); result.Err != nil {
return nil, result.Err
} else {
oldPost = result.Data.(*model.PostList).Posts[post.Id]
if oldPost == nil {
err := model.NewLocAppError("updatePost", "api.post.update_post.find.app_error", nil, "id="+post.Id)
err.StatusCode = http.StatusBadRequest
return nil, err
}
if oldPost.UserId != post.UserId {
err := model.NewLocAppError("updatePost", "api.post.update_post.permissions.app_error", nil, "oldUserId="+oldPost.UserId)
err.StatusCode = http.StatusBadRequest
return nil, err
}
if oldPost.DeleteAt != 0 {
err := model.NewLocAppError("updatePost", "api.post.update_post.permissions_details.app_error", map[string]interface{}{"PostId": post.Id}, "")
err.StatusCode = http.StatusBadRequest
return nil, err
}
if oldPost.IsSystemMessage() {
err := model.NewLocAppError("updatePost", "api.post.update_post.system_message.app_error", nil, "id="+post.Id)
err.StatusCode = http.StatusBadRequest
return nil, err
}
if utils.IsLicensed {
if *utils.Cfg.ServiceSettings.AllowEditPost == model.ALLOW_EDIT_POST_TIME_LIMIT && model.GetMillis() > oldPost.CreateAt+int64(*utils.Cfg.ServiceSettings.PostEditTimeLimit*1000) {
err := model.NewLocAppError("updatePost", "api.post.update_post.permissions_time_limit.app_error", map[string]interface{}{"timeLimit": *utils.Cfg.ServiceSettings.PostEditTimeLimit}, "")
err.StatusCode = http.StatusBadRequest
return nil, err
}
}
}
newPost := &model.Post{}
*newPost = *oldPost
newPost.Message = post.Message
newPost.EditAt = model.GetMillis()
newPost.Hashtags, _ = model.ParseHashtags(post.Message)
if result := <-Srv.Store.Post().Update(newPost, oldPost); result.Err != nil {
return nil, result.Err
} else {
rpost := result.Data.(*model.Post)
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_EDITED, "", rpost.ChannelId, "", nil)
message.Add("post", rpost.ToJson())
go Publish(message)
InvalidateCacheForChannelPosts(rpost.ChannelId)
return rpost, nil
}
}
func GetPostsPage(channelId string, page int, perPage int) (*model.PostList, *model.AppError) {
if result := <-Srv.Store.Post().GetPosts(channelId, page*perPage, perPage, true); result.Err != nil {
return nil, result.Err
} else {
return result.Data.(*model.PostList), nil
}
}
func GetPosts(channelId string, offset int, limit int) (*model.PostList, *model.AppError) {
if result := <-Srv.Store.Post().GetPosts(channelId, offset, limit, true); result.Err != nil {
return nil, result.Err
} else {
return result.Data.(*model.PostList), nil
}
}
func GetPostsEtag(channelId string) string {
return (<-Srv.Store.Post().GetEtag(channelId, true)).Data.(string)
}
func GetPostsSince(channelId string, time int64) (*model.PostList, *model.AppError) {
if result := <-Srv.Store.Post().GetPostsSince(channelId, time, true); result.Err != nil {
return nil, result.Err
} else {
return result.Data.(*model.PostList), nil
}
}
func GetSinglePost(postId string) (*model.Post, *model.AppError) {
if result := <-Srv.Store.Post().GetSingle(postId); result.Err != nil {
return nil, result.Err
} else {
return result.Data.(*model.Post), nil
}
}
func GetPostThread(postId string) (*model.PostList, *model.AppError) {
if result := <-Srv.Store.Post().Get(postId); result.Err != nil {
return nil, result.Err
} else {
return result.Data.(*model.PostList), nil
}
}
func GetFlaggedPosts(userId string, offset int, limit int) (*model.PostList, *model.AppError) {
if result := <-Srv.Store.Post().GetFlaggedPosts(userId, offset, limit); result.Err != nil {
return nil, result.Err
} else {
return result.Data.(*model.PostList), nil
}
}
func GetPermalinkPost(postId string, userId string, siteURL string) (*model.PostList, *model.AppError) {
if result := <-Srv.Store.Post().Get(postId); result.Err != nil {
return nil, result.Err
} else {
list := result.Data.(*model.PostList)
if len(list.Order) != 1 {
return nil, model.NewLocAppError("getPermalinkTmp", "api.post_get_post_by_id.get.app_error", nil, "")
}
post := list.Posts[list.Order[0]]
var channel *model.Channel
var err *model.AppError
if channel, err = GetChannel(post.ChannelId); err != nil {
return nil, err
}
if err = JoinChannel(channel, userId, siteURL); err != nil {
return nil, err
}
return list, nil
}
}
func GetPostsAroundPost(postId, channelId string, offset, limit int, before bool) (*model.PostList, *model.AppError) {
var pchan store.StoreChannel
if before {
pchan = Srv.Store.Post().GetPostsBefore(channelId, postId, limit, offset)
} else {
pchan = Srv.Store.Post().GetPostsAfter(channelId, postId, limit, offset)
}
if result := <-pchan; result.Err != nil {
return nil, result.Err
} else {
return result.Data.(*model.PostList), nil
}
}
func DeletePost(postId string) (*model.Post, *model.AppError) {
if result := <-Srv.Store.Post().GetSingle(postId); result.Err != nil {
result.Err.StatusCode = http.StatusBadRequest
return nil, result.Err
} else {
post := result.Data.(*model.Post)
if result := <-Srv.Store.Post().Delete(postId, model.GetMillis()); result.Err != nil {
return nil, result.Err
}
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_POST_DELETED, "", post.ChannelId, "", nil)
message.Add("post", post.ToJson())
go Publish(message)
go DeletePostFiles(post)
go DeleteFlaggedPosts(post.Id)
InvalidateCacheForChannelPosts(post.ChannelId)
return post, nil
}
}
func DeleteFlaggedPosts(postId string) {
if result := <-Srv.Store.Preference().DeleteCategoryAndName(model.PREFERENCE_CATEGORY_FLAGGED_POST, postId); result.Err != nil {
l4g.Warn(utils.T("api.post.delete_flagged_post.app_error.warn"), result.Err)
return
}
}
func DeletePostFiles(post *model.Post) {
if len(post.FileIds) != 0 {
return
}
if result := <-Srv.Store.FileInfo().DeleteForPost(post.Id); result.Err != nil {
l4g.Warn(utils.T("api.post.delete_post_files.app_error.warn"), post.Id, result.Err)
}
}
func SearchPostsInTeam(terms string, userId string, teamId string, isOrSearch bool) (*model.PostList, *model.AppError) {
paramsList := model.ParseSearchParams(terms)
channels := []store.StoreChannel{}
for _, params := range paramsList {
params.OrTerms = isOrSearch
// don't allow users to search for everything
if params.Terms != "*" {
channels = append(channels, Srv.Store.Post().Search(teamId, userId, params))
}
}
posts := model.NewPostList()
for _, channel := range channels {
if result := <-channel; result.Err != nil {
return nil, result.Err
} else {
data := result.Data.(*model.PostList)
posts.Extend(data)
}
}
return posts, nil
}
func GetFileInfosForPost(postId string, readFromMaster bool) ([]*model.FileInfo, *model.AppError) {
pchan := Srv.Store.Post().GetSingle(postId)
fchan := Srv.Store.FileInfo().GetForPost(postId, readFromMaster, true)
var infos []*model.FileInfo
if result := <-fchan; result.Err != nil {
return nil, result.Err
} else {
infos = result.Data.([]*model.FileInfo)
}
if len(infos) == 0 {
// No FileInfos were returned so check if they need to be created for this post
var post *model.Post
if result := <-pchan; result.Err != nil {
return nil, result.Err
} else {
post = result.Data.(*model.Post)
}
if len(post.Filenames) > 0 {
Srv.Store.FileInfo().InvalidateFileInfosForPostCache(postId)
// The post has Filenames that need to be replaced with FileInfos
infos = MigrateFilenamesToFileInfos(post)
}
}
return infos, nil
}
func GetOpenGraphMetadata(url string) *opengraph.OpenGraph {
og := opengraph.NewOpenGraph()
res, err := httpClient.Get(url)
if err != nil {
l4g.Error(err.Error())
return og
}
defer CloseBody(res)
if err := og.ProcessHTML(res.Body); err != nil {
l4g.Error(err.Error())
}
return og
}