PLT-857: Support for Incoming Webhooks - Try #2
Этот коммит содержится в:
14
api/post.go
14
api/post.go
@@ -147,7 +147,7 @@ func CreatePost(c *Context, post *model.Post, triggerWebhooks bool) (*model.Post
|
|||||||
return rpost, nil
|
return rpost, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func CreateWebhookPost(c *Context, channelId, text, overrideUsername, overrideIconUrl string) (*model.Post, *model.AppError) {
|
func CreateWebhookPost(c *Context, channelId, text, overrideUsername, overrideIconUrl string, props model.StringInterface, postType string) (*model.Post, *model.AppError) {
|
||||||
// parse links into Markdown format
|
// parse links into Markdown format
|
||||||
linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
|
linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`)
|
||||||
text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
|
text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})")
|
||||||
@@ -155,7 +155,7 @@ func CreateWebhookPost(c *Context, channelId, text, overrideUsername, overrideIc
|
|||||||
linkRegex := regexp.MustCompile(`<\s*(\S*)\s*>`)
|
linkRegex := regexp.MustCompile(`<\s*(\S*)\s*>`)
|
||||||
text = linkRegex.ReplaceAllString(text, "${1}")
|
text = linkRegex.ReplaceAllString(text, "${1}")
|
||||||
|
|
||||||
post := &model.Post{UserId: c.Session.UserId, ChannelId: channelId, Message: text}
|
post := &model.Post{UserId: c.Session.UserId, ChannelId: channelId, Message: text, Type: postType}
|
||||||
post.AddProp("from_webhook", "true")
|
post.AddProp("from_webhook", "true")
|
||||||
|
|
||||||
if utils.Cfg.ServiceSettings.EnablePostUsernameOverride {
|
if utils.Cfg.ServiceSettings.EnablePostUsernameOverride {
|
||||||
@@ -174,6 +174,14 @@ func CreateWebhookPost(c *Context, channelId, text, overrideUsername, overrideIc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(props) > 0 {
|
||||||
|
for key, val := range props {
|
||||||
|
if key != "override_icon_url" && key != "override_username" && key != "from_webhook" {
|
||||||
|
post.AddProp(key, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := CreatePost(c, post, false); err != nil {
|
if _, err := CreatePost(c, post, false); err != nil {
|
||||||
return nil, model.NewAppError("CreateWebhookPost", "Error creating post", "err="+err.Message)
|
return nil, model.NewAppError("CreateWebhookPost", "Error creating post", "err="+err.Message)
|
||||||
}
|
}
|
||||||
@@ -286,7 +294,7 @@ func handleWebhookEventsAndForget(c *Context, post *model.Post, team *model.Team
|
|||||||
newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL, 0}
|
newContext := &Context{mockSession, model.NewId(), "", c.Path, nil, c.teamURLValid, c.teamURL, c.siteURL, 0}
|
||||||
|
|
||||||
if text, ok := respProps["text"]; ok {
|
if text, ok := respProps["text"]; ok {
|
||||||
if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"]); err != nil {
|
if _, err := CreateWebhookPost(newContext, post.ChannelId, text, respProps["username"], respProps["icon_url"], post.Props, post.Type); err != nil {
|
||||||
l4g.Error("Failed to create response post, err=%v", err)
|
l4g.Error("Failed to create response post, err=%v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,10 +24,13 @@ type IncomingWebhook struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type IncomingWebhookRequest struct {
|
type IncomingWebhookRequest struct {
|
||||||
Text string `json:"text"`
|
Text string `json:"text"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
IconURL string `json:"icon_url"`
|
IconURL string `json:"icon_url"`
|
||||||
ChannelName string `json:"channel"`
|
ChannelName string `json:"channel"`
|
||||||
|
Props StringInterface `json:"props"`
|
||||||
|
Attachments interface{} `json:"attachments"`
|
||||||
|
Type string `json:"type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *IncomingWebhook) ToJson() string {
|
func (o *IncomingWebhook) ToJson() string {
|
||||||
|
|||||||
@@ -10,27 +10,28 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
POST_DEFAULT = ""
|
POST_DEFAULT = ""
|
||||||
POST_JOIN_LEAVE = "join_leave"
|
POST_SLACK_ATTACHMENT = "slack_attachment"
|
||||||
|
POST_JOIN_LEAVE = "join_leave"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Post struct {
|
type Post struct {
|
||||||
Id string `json:"id"`
|
Id string `json:"id"`
|
||||||
CreateAt int64 `json:"create_at"`
|
CreateAt int64 `json:"create_at"`
|
||||||
UpdateAt int64 `json:"update_at"`
|
UpdateAt int64 `json:"update_at"`
|
||||||
DeleteAt int64 `json:"delete_at"`
|
DeleteAt int64 `json:"delete_at"`
|
||||||
UserId string `json:"user_id"`
|
UserId string `json:"user_id"`
|
||||||
ChannelId string `json:"channel_id"`
|
ChannelId string `json:"channel_id"`
|
||||||
RootId string `json:"root_id"`
|
RootId string `json:"root_id"`
|
||||||
ParentId string `json:"parent_id"`
|
ParentId string `json:"parent_id"`
|
||||||
OriginalId string `json:"original_id"`
|
OriginalId string `json:"original_id"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
ImgCount int64 `json:"img_count"`
|
ImgCount int64 `json:"img_count"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Props StringMap `json:"props"`
|
Props StringInterface `json:"props"`
|
||||||
Hashtags string `json:"hashtags"`
|
Hashtags string `json:"hashtags"`
|
||||||
Filenames StringArray `json:"filenames"`
|
Filenames StringArray `json:"filenames"`
|
||||||
PendingPostId string `json:"pending_post_id" db:"-"`
|
PendingPostId string `json:"pending_post_id" db:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Post) ToJson() string {
|
func (o *Post) ToJson() string {
|
||||||
@@ -103,7 +104,8 @@ func (o *Post) IsValid() *AppError {
|
|||||||
return NewAppError("Post.IsValid", "Invalid hashtags", "id="+o.Id)
|
return NewAppError("Post.IsValid", "Invalid hashtags", "id="+o.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !(o.Type == POST_DEFAULT || o.Type == POST_JOIN_LEAVE) {
|
// should be removed once more message types are supported
|
||||||
|
if !(o.Type == POST_DEFAULT || o.Type == POST_JOIN_LEAVE || o.Type == POST_SLACK_ATTACHMENT) {
|
||||||
return NewAppError("Post.IsValid", "Invalid type", "id="+o.Type)
|
return NewAppError("Post.IsValid", "Invalid type", "id="+o.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +130,7 @@ func (o *Post) PreSave() {
|
|||||||
o.UpdateAt = o.CreateAt
|
o.UpdateAt = o.CreateAt
|
||||||
|
|
||||||
if o.Props == nil {
|
if o.Props == nil {
|
||||||
o.Props = make(map[string]string)
|
o.Props = make(map[string]interface{})
|
||||||
}
|
}
|
||||||
|
|
||||||
if o.Filenames == nil {
|
if o.Filenames == nil {
|
||||||
@@ -138,14 +140,14 @@ func (o *Post) PreSave() {
|
|||||||
|
|
||||||
func (o *Post) MakeNonNil() {
|
func (o *Post) MakeNonNil() {
|
||||||
if o.Props == nil {
|
if o.Props == nil {
|
||||||
o.Props = make(map[string]string)
|
o.Props = make(map[string]interface{})
|
||||||
}
|
}
|
||||||
if o.Filenames == nil {
|
if o.Filenames == nil {
|
||||||
o.Filenames = []string{}
|
o.Filenames = []string{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Post) AddProp(key string, value string) {
|
func (o *Post) AddProp(key string, value interface{}) {
|
||||||
|
|
||||||
o.MakeNonNil()
|
o.MakeNonNil()
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type StringInterface map[string]interface{}
|
||||||
type StringMap map[string]string
|
type StringMap map[string]string
|
||||||
type StringArray []string
|
type StringArray []string
|
||||||
type EncryptStringMap map[string]string
|
type EncryptStringMap map[string]string
|
||||||
@@ -125,6 +126,25 @@ func ArrayFromJson(data io.Reader) []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func StringInterfaceToJson(objmap map[string]interface{}) string {
|
||||||
|
if b, err := json.Marshal(objmap); err != nil {
|
||||||
|
return ""
|
||||||
|
} else {
|
||||||
|
return string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func StringInterfaceFromJson(data io.Reader) map[string]interface{} {
|
||||||
|
decoder := json.NewDecoder(data)
|
||||||
|
|
||||||
|
var objmap map[string]interface{}
|
||||||
|
if err := decoder.Decode(&objmap); err != nil {
|
||||||
|
return make(map[string]interface{})
|
||||||
|
} else {
|
||||||
|
return objmap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func IsLower(s string) bool {
|
func IsLower(s string) bool {
|
||||||
if strings.ToLower(s) == s {
|
if strings.ToLower(s) == s {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
l4g "code.google.com/p/log4go"
|
||||||
"github.com/mattermost/platform/model"
|
"github.com/mattermost/platform/model"
|
||||||
"github.com/mattermost/platform/utils"
|
"github.com/mattermost/platform/utils"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SqlPostStore struct {
|
type SqlPostStore struct {
|
||||||
@@ -38,6 +40,15 @@ func NewSqlPostStore(sqlStore *SqlStore) PostStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s SqlPostStore) UpgradeSchemaIfNeeded() {
|
func (s SqlPostStore) UpgradeSchemaIfNeeded() {
|
||||||
|
col := s.GetColumnInformation("Posts", "Props")
|
||||||
|
if col.Type != "text" {
|
||||||
|
_, err := s.GetMaster().Exec("ALTER TABLE Posts MODIFY COLUMN Props TEXT")
|
||||||
|
if err != nil {
|
||||||
|
l4g.Critical("Failed to alter column Posts.Props to TEXT: " + err.Error())
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
panic("Failed to alter column Posts.Props to TEXT: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s SqlPostStore) CreateIndexesIfNotExists() {
|
func (s SqlPostStore) CreateIndexesIfNotExists() {
|
||||||
|
|||||||
@@ -50,6 +50,15 @@ type SqlStore struct {
|
|||||||
preference PreferenceStore
|
preference PreferenceStore
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Column struct {
|
||||||
|
Field string
|
||||||
|
Type string
|
||||||
|
Null string
|
||||||
|
Key interface{}
|
||||||
|
Default interface{}
|
||||||
|
Extra interface{}
|
||||||
|
}
|
||||||
|
|
||||||
func NewSqlStore() Store {
|
func NewSqlStore() Store {
|
||||||
|
|
||||||
sqlStore := &SqlStore{}
|
sqlStore := &SqlStore{}
|
||||||
@@ -455,6 +464,20 @@ func IsUniqueConstraintError(err string, mysql string, postgres string) bool {
|
|||||||
return unique && field
|
return unique && field
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ss SqlStore) GetColumnInformation(tableName, columnName string) Column {
|
||||||
|
var col Column
|
||||||
|
err := ss.GetMaster().SelectOne(&col, "SHOW COLUMNS FROM "+tableName+" WHERE Field = :Columnname", map[string]interface{}{
|
||||||
|
"Columnname": columnName,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
l4g.Critical("Failed to get information for column %s from table %s: %v", tableName, columnName, err.Error())
|
||||||
|
time.Sleep(time.Second)
|
||||||
|
panic("Failed to get information for column " + tableName + " from table " + columnName + ": " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return col
|
||||||
|
}
|
||||||
|
|
||||||
func (ss SqlStore) GetMaster() *gorp.DbMap {
|
func (ss SqlStore) GetMaster() *gorp.DbMap {
|
||||||
return ss.master
|
return ss.master
|
||||||
}
|
}
|
||||||
@@ -529,6 +552,8 @@ func (me mattermConverter) ToDb(val interface{}) (interface{}, error) {
|
|||||||
return model.ArrayToJson(t), nil
|
return model.ArrayToJson(t), nil
|
||||||
case model.EncryptStringMap:
|
case model.EncryptStringMap:
|
||||||
return encrypt([]byte(utils.Cfg.SqlSettings.AtRestEncryptKey), model.MapToJson(t))
|
return encrypt([]byte(utils.Cfg.SqlSettings.AtRestEncryptKey), model.MapToJson(t))
|
||||||
|
case model.StringInterface:
|
||||||
|
return model.StringInterfaceToJson(t), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return val, nil
|
return val, nil
|
||||||
@@ -572,6 +597,16 @@ func (me mattermConverter) FromDb(target interface{}) (gorp.CustomScanner, bool)
|
|||||||
return json.Unmarshal(b, target)
|
return json.Unmarshal(b, target)
|
||||||
}
|
}
|
||||||
return gorp.CustomScanner{new(string), target, binder}, true
|
return gorp.CustomScanner{new(string), target, binder}, true
|
||||||
|
case *model.StringInterface:
|
||||||
|
binder := func(holder, target interface{}) error {
|
||||||
|
s, ok := holder.(*string)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("FromDb: Unable to convert StringInterface to *string")
|
||||||
|
}
|
||||||
|
b := []byte(*s)
|
||||||
|
return json.Unmarshal(b, target)
|
||||||
|
}
|
||||||
|
return gorp.CustomScanner{new(string), target, binder}, true
|
||||||
}
|
}
|
||||||
|
|
||||||
return gorp.CustomScanner{}, false
|
return gorp.CustomScanner{}, false
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const Utils = require('../utils/utils.jsx');
|
|||||||
const Constants = require('../utils/constants.jsx');
|
const Constants = require('../utils/constants.jsx');
|
||||||
const TextFormatting = require('../utils/text_formatting.jsx');
|
const TextFormatting = require('../utils/text_formatting.jsx');
|
||||||
const twemoji = require('twemoji');
|
const twemoji = require('twemoji');
|
||||||
const PostAttachmentList = require('./post_attachment_list.jsx');
|
const PostBodyAdditionalContent = require('./post_body_additional_content.jsx');
|
||||||
|
|
||||||
export default class PostBody extends React.Component {
|
export default class PostBody extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -317,15 +317,6 @@ export default class PostBody extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let postAttachments = '';
|
|
||||||
if (post.attachments && post.attachments.length) {
|
|
||||||
postAttachments = (
|
|
||||||
<PostAttachmentList
|
|
||||||
attachments={post.attachments}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='post-body'>
|
<div className='post-body'>
|
||||||
{comment}
|
{comment}
|
||||||
@@ -341,7 +332,9 @@ export default class PostBody extends React.Component {
|
|||||||
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.message)}}
|
dangerouslySetInnerHTML={{__html: TextFormatting.formatText(this.state.message)}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{postAttachments}
|
<PostBodyAdditionalContent
|
||||||
|
post={post}
|
||||||
|
/>
|
||||||
{fileAttachmentHolder}
|
{fileAttachmentHolder}
|
||||||
{embed}
|
{embed}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
56
web/react/components/post_body_additional_content.jsx
Обычный файл
56
web/react/components/post_body_additional_content.jsx
Обычный файл
@@ -0,0 +1,56 @@
|
|||||||
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
const PostAttachmentList = require('./post_attachment_list.jsx');
|
||||||
|
|
||||||
|
export default class PostBodyAdditionalContent extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.getSlackAttachment = this.getSlackAttachment.bind(this);
|
||||||
|
this.getComponent = this.getComponent.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillMount() {
|
||||||
|
this.setState({type: this.props.post.type, shouldRender: Boolean(this.props.post.type)});
|
||||||
|
}
|
||||||
|
|
||||||
|
getSlackAttachment() {
|
||||||
|
const attachments = this.props.post.props && this.props.post.props.attachments || [];
|
||||||
|
return (
|
||||||
|
<PostAttachmentList
|
||||||
|
key={'post_body_additional_content' + this.props.post.id}
|
||||||
|
attachments={attachments}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getComponent() {
|
||||||
|
switch (this.state.type) {
|
||||||
|
case 'slack_attachment':
|
||||||
|
return this.getSlackAttachment();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let content = [];
|
||||||
|
|
||||||
|
if (this.state.shouldRender) {
|
||||||
|
const component = this.getComponent();
|
||||||
|
|
||||||
|
if (component) {
|
||||||
|
content = component;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PostBodyAdditionalContent.propTypes = {
|
||||||
|
post: React.PropTypes.object.isRequired
|
||||||
|
};
|
||||||
@@ -668,6 +668,7 @@ body.ios {
|
|||||||
}
|
}
|
||||||
.attachment__image {
|
.attachment__image {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
margin-bottom: 1em;
|
||||||
}
|
}
|
||||||
.attachment__thumb-container {
|
.attachment__thumb-container {
|
||||||
width: 20%;
|
width: 20%;
|
||||||
@@ -682,5 +683,8 @@ body.ios {
|
|||||||
.attachment___field-caption {
|
.attachment___field-caption {
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
.attachment___field p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
11
web/web.go
11
web/web.go
@@ -990,6 +990,15 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
channelName := parsedRequest.ChannelName
|
channelName := parsedRequest.ChannelName
|
||||||
|
webhookType := parsedRequest.Type
|
||||||
|
|
||||||
|
if parsedRequest.Attachments != nil {
|
||||||
|
if len(parsedRequest.Props) == 0 {
|
||||||
|
parsedRequest.Props = make(model.StringInterface)
|
||||||
|
}
|
||||||
|
parsedRequest.Props["attachments"] = parsedRequest.Attachments
|
||||||
|
webhookType = model.POST_SLACK_ATTACHMENT
|
||||||
|
}
|
||||||
|
|
||||||
var hook *model.IncomingWebhook
|
var hook *model.IncomingWebhook
|
||||||
if result := <-hchan; result.Err != nil {
|
if result := <-hchan; result.Err != nil {
|
||||||
@@ -1039,7 +1048,7 @@ func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := api.CreateWebhookPost(c, channel.Id, text, overrideUsername, overrideIconUrl); err != nil {
|
if _, err := api.CreateWebhookPost(c, channel.Id, text, overrideUsername, overrideIconUrl, parsedRequest.Props, webhookType); err != nil {
|
||||||
c.Err = err
|
c.Err = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user