3
0

add(messaging): webhooks implementation

This commit is contained in:
Tit Petric
2019-04-26 20:39:12 +02:00
parent 1dabd7a838
commit 60ab73b03e
17 changed files with 1306 additions and 38 deletions
+124
View File
@@ -0,0 +1,124 @@
package repository
import (
"context"
"time"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"github.com/crusttech/crust/messaging/types"
)
type (
WebhookRepository interface {
With(ctx context.Context, db *factory.DB) WebhookRepository
Create(*types.Webhook) (*types.Webhook, error)
Update(*types.Webhook) (*types.Webhook, error)
Get(webhookID uint64) (*types.Webhook, error)
GetByToken(webhookID uint64, webhookToken string) (*types.Webhook, error)
Find(filter *types.WebhookFilter) (types.WebhookSet, error)
Delete(webhookID uint64) error
DeleteByToken(webhookID uint64, webhookToken string) error
}
webhook struct {
webhook string
*repository
}
)
func Webhook(ctx context.Context, db *factory.DB) WebhookRepository {
return (&webhook{}).With(ctx, db)
}
func (r *webhook) With(ctx context.Context, db *factory.DB) WebhookRepository {
return &webhook{
webhook: "messaging_webhook",
repository: r.repository.With(ctx, db),
}
}
func (r *webhook) Create(webhook *types.Webhook) (*types.Webhook, error) {
webhook.ID = factory.Sonyflake.NextID()
webhook.CreatedAt = time.Now()
return webhook, errors.WithStack(r.db().Insert(r.webhook, webhook))
}
func (r *webhook) Update(webhook *types.Webhook) (*types.Webhook, error) {
webhook.UpdatedAt = timeNowPtr()
return webhook, errors.WithStack(r.db().Replace(r.webhook, webhook))
}
func (r *webhook) Get(webhookID uint64) (*types.Webhook, error) {
hook := &types.Webhook{}
if err := r.db().Get(hook, "select * from "+r.webhook+" where id=?", webhookID); err != nil {
return nil, errors.WithStack(err)
}
return hook, nil
}
func (r *webhook) GetByToken(webhookID uint64, webhookToken string) (*types.Webhook, error) {
webhook, err := r.Get(webhookID)
switch {
case err != nil:
return nil, errors.WithStack(err)
case webhook.AuthToken == webhookToken:
return webhook, nil
default:
return nil, errors.New("Invalid Webhook Token")
}
}
// Find webhooks based on filter
//
// If ChannelID > 0 it returns webhooks created on a specific channel
// If OwnerUserID > 0 it returns webhooks owned by a specific user
func (r *webhook) Find(filter *types.WebhookFilter) (types.WebhookSet, error) {
params := make([]interface{}, 0)
vv := types.WebhookSet{}
sql := "select * from messaging_webhook where 1=1"
if filter != nil {
if filter.OwnerUserID > 0 {
// scope: only channel we have access to
sql += " AND rel_owner=?"
params = append(params, filter.OwnerUserID)
}
if filter.OutgoingTrigger != "" {
// scope: only channel we have access to
sql += " AND outgoing_trigger=?"
params = append(params, filter.OutgoingTrigger)
}
if filter.ChannelID > 0 {
// scope: only channel we have access to
sql += " AND rel_channel=?"
params = append(params, filter.ChannelID)
}
}
return vv, errors.WithStack(r.db().Select(&vv, sql, params...))
}
func (r *webhook) Delete(webhookID uint64) error {
if _, err := r.Get(webhookID); err != nil {
return err
}
_, err := r.db().Exec("delete from "+r.webhook+" where id=?", webhookID)
return errors.WithStack(err)
}
func (r *webhook) DeleteByToken(webhookID uint64, webhookToken string) error {
if _, err := r.GetByToken(webhookID, webhookToken); err != nil {
return err
}
_, err := r.db().Exec("delete from "+r.webhook+" where id=?", webhookID)
return errors.WithStack(err)
}
+73
View File
@@ -0,0 +1,73 @@
package service
import (
"context"
"github.com/crusttech/crust/messaging/types"
)
type (
command struct {
ctx context.Context
}
CommandService interface {
With(context.Context) CommandService
Do(channelID uint64, command, input string) (*types.Message, error)
}
)
func Command(ctx context.Context) CommandService {
return (&command{}).With(ctx)
}
func (svc *command) With(ctx context.Context) CommandService {
return &command{
ctx: ctx,
}
}
func (svc *command) Do(channelID uint64, command, input string) (*types.Message, error) {
switch command {
case "me":
if input != "" {
return DefaultMessage.With(svc.ctx).Create(&types.Message{
Type: types.MessageTypeIlleism,
ChannelID: channelID,
Message: input,
})
}
return nil, nil
case "tableflip":
fallthrough
case "unflip":
fallthrough
case "shrug":
messages := map[string]string{
"tableflip": `(╯°□°)╯︵ ┻━┻`,
"unflip": `┬─┬ ( ゜-゜ノ)`,
"shrug": `¯\\_(ツ)_/¯`,
}
msg := &types.Message{
ChannelID: channelID,
Message: messages[command],
}
if input != "" {
msg.Message = input + " " + msg.Message
}
return DefaultMessage.With(svc.ctx).Create(msg)
default:
webhookSvc := DefaultWebhook.With(svc.ctx)
webhooks, err := webhookSvc.Find(&types.WebhookFilter{
ChannelID: channelID,
OutgoingTrigger: command,
})
if err != nil || len(webhooks) == 0 {
return nil, err
}
return webhookSvc.Do(webhooks[0], input)
}
return nil, ErrUnknownCommand.new()
}
+10 -1
View File
@@ -1,5 +1,9 @@
package service
import (
"github.com/pkg/errors"
)
type (
readableError string
)
@@ -8,6 +12,11 @@ func (e readableError) Error() string {
return string(e)
}
func (e readableError) new() error {
return errors.WithStack(e)
}
const (
ErrNoPermissions readableError = "You don't have permissions for this operation"
ErrNoPermissions readableError = "You don't have permissions for this operation"
ErrUnknownCommand readableError = "Unknown command"
)
+6 -1
View File
@@ -30,11 +30,16 @@ func TestMain(m *testing.M) {
factory.Database.Add("messaging", dsn)
factory.Database.Add("system", dsn)
// @todo: add flags to configure the sql profiler in tests
db := factory.Database.MustGet()
db.Profiler = &factory.Database.ProfilerStdout
dbSystem := factory.Database.MustGet("system")
dbSystem.Profiler = &factory.Database.ProfilerStdout
// migrate database schema
if err := systemMigrate.Migrate(db); err != nil {
if err := systemMigrate.Migrate(dbSystem); err != nil {
log.Printf("Error running migrations: %+v\n", err)
return
}
+8 -1
View File
@@ -2,6 +2,7 @@ package service
import (
"context"
"io"
"regexp"
"strings"
@@ -37,9 +38,10 @@ type (
FindThreads(filter *types.MessageFilter) (types.MessageSet, error)
Create(messages *types.Message) (*types.Message, error)
Update(messages *types.Message) (*types.Message, error)
CreateWithAvatar(message *types.Message, avatar io.Reader) (*types.Message, error)
React(messageID uint64, reaction string) error
RemoveReaction(messageID uint64, reaction string) error
@@ -129,6 +131,11 @@ func (svc *message) FindThreads(filter *types.MessageFilter) (mm types.MessageSe
return mm, svc.preload(mm)
}
func (svc *message) CreateWithAvatar(in *types.Message, avatar io.Reader) (*types.Message, error) {
// @todo: avatar
return svc.Create(in)
}
func (svc message) channelAccessCheck(IDs ...uint64) error {
for _, ID := range IDs {
if ID > 0 {
+5 -1
View File
@@ -22,6 +22,8 @@ var (
DefaultPubSub *pubSub
DefaultEvent EventService
DefaultPermissions PermissionsService
DefaultCommand CommandService
DefaultWebhook WebhookService
)
func Init() error {
@@ -30,7 +32,7 @@ func Init() error {
return err
}
_, err = http.New(&config.HTTPClient{
client, err := http.New(&config.HTTPClient{
Timeout: 10,
})
if err != nil {
@@ -45,6 +47,8 @@ func Init() error {
DefaultMessage = Message(ctx)
DefaultChannel = Channel(ctx)
DefaultPubSub = PubSub()
DefaultCommand = Command(ctx)
DefaultWebhook = Webhook(ctx, client)
return nil
}
+254
View File
@@ -0,0 +1,254 @@
package service
import (
"context"
"encoding/json"
"io"
"io/ioutil"
"net/url"
"strings"
"github.com/pkg/errors"
"github.com/crusttech/crust/internal/auth"
"github.com/crusttech/crust/internal/http"
"github.com/crusttech/crust/internal/store"
"github.com/crusttech/crust/messaging/internal/repository"
"github.com/crusttech/crust/messaging/types"
systemService "github.com/crusttech/crust/system/service"
systemTypes "github.com/crusttech/crust/system/types"
)
type (
webhook struct {
db db
ctx context.Context
client *http.Client
users systemService.UserService
webhook repository.WebhookRepository
perms PermissionsService
}
WebhookService interface {
With(ctx context.Context) WebhookService
Get(webhookID uint64) (*types.Webhook, error)
Find(*types.WebhookFilter) (types.WebhookSet, error)
Delete(webhookID uint64) error
DeleteByToken(webhookID uint64, webhookToken string) error
Message(webhookID uint64, webhookToken string, username string, avatar io.Reader, message string) (interface{}, error)
Create(kind types.WebhookKind, channelID uint64, params types.WebhookRequest) (*types.Webhook, error)
Update(webhookID uint64, kind types.WebhookKind, channelID uint64, params types.WebhookRequest) (*types.Webhook, error)
Do(webhook *types.Webhook, message string) (*types.Message, error)
}
)
func Webhook(ctx context.Context, client *http.Client) WebhookService {
return (&webhook{
client: client,
}).With(ctx)
}
func (svc *webhook) With(ctx context.Context) WebhookService {
db := repository.DB(ctx)
return &webhook{
db: db,
ctx: ctx,
client: svc.client,
users: systemService.User(ctx),
webhook: repository.Webhook(ctx, db),
perms: Permissions(ctx),
}
}
func (svc *webhook) Create(kind types.WebhookKind, channelID uint64, params types.WebhookRequest) (*types.Webhook, error) {
var userID = repository.Identity(svc.ctx)
webhook := &types.Webhook{
Kind: kind,
OwnerUserID: userID,
ChannelID: channelID,
OutgoingTrigger: params.OutgoingTrigger,
OutgoingURL: params.OutgoingURL,
}
if !svc.perms.CanManageWebhooks(webhook) && !svc.perms.CanManageOwnWebhooks(webhook) {
return nil, errors.WithStack(ErrNoPermissions)
}
botUser := &systemTypes.User{
Username: params.Username,
Name: params.Username,
Handle: params.Username,
Kind: systemTypes.BotUser,
RelatedUserID: userID,
}
user, err := svc.users.CreateWithAvatar(botUser, params.Avatar)
if err != nil {
return nil, err
}
webhook.UserID = user.ID
wh, err := svc.webhook.Create(webhook)
if err != nil {
// cross service rollback (delete user)
svc.users.Delete(user.ID)
return nil, err
}
return wh, err
}
func (svc *webhook) Update(webhookID uint64, kind types.WebhookKind, channelID uint64, params types.WebhookRequest) (*types.Webhook, error) {
var userID = repository.Identity(svc.ctx)
webhook, err := svc.Get(webhookID)
if err != nil {
return nil, err
}
if !svc.perms.CanManageWebhooks(webhook) && !(webhook.OwnerUserID == userID && svc.perms.CanManageOwnWebhooks(webhook)) {
return nil, errors.WithStack(ErrNoPermissions)
}
botUser, err := svc.users.FindByID(webhook.UserID)
if err != nil {
return nil, errors.Wrapf(err, "Error when looking for User ID %d", webhook.UserID)
}
if _, err := svc.users.UpdateWithAvatar(botUser, params.Avatar); err != nil {
return nil, err
}
webhook.Kind = kind
webhook.ChannelID = channelID
webhook.OutgoingTrigger = params.OutgoingTrigger
webhook.OutgoingURL = params.OutgoingURL
return svc.webhook.Update(webhook)
}
func (svc *webhook) Get(webhookID uint64) (*types.Webhook, error) {
return svc.webhook.Get(webhookID)
}
func (svc *webhook) Find(filter *types.WebhookFilter) (types.WebhookSet, error) {
return svc.webhook.Find(filter)
}
func (svc *webhook) Delete(webhookID uint64) error {
webhook, err := svc.Get(webhookID)
if err != nil {
return err
}
if svc.perms.CanManageWebhooks(webhook) {
return svc.webhook.Delete(webhookID)
}
var userID = repository.Identity(svc.ctx)
if webhook.OwnerUserID == userID && svc.perms.CanManageOwnWebhooks(webhook) {
return svc.webhook.Delete(webhookID)
}
return errors.WithStack(ErrNoPermissions)
}
func (svc *webhook) DeleteByToken(webhookID uint64, webhookToken string) error {
return svc.webhook.DeleteByToken(webhookID, webhookToken)
}
func (svc *webhook) Message(webhookID uint64, webhookToken string, username string, avatar io.Reader, message string) (interface{}, error) {
if webhook, err := svc.webhook.GetByToken(webhookID, webhookToken); err != nil {
return nil, err
} else {
msg := &types.Message{
Message: message,
Meta: &types.MessageMeta{
Username: username,
},
}
return svc.sendMessage(webhook, msg, avatar)
}
}
// Do executes an outgoing HTTP webhook request
func (svc *webhook) Do(webhook *types.Webhook, message string) (*types.Message, error) {
if webhook.Kind != types.OutgoingWebhook {
return nil, errors.Errorf("Unsupported webhook type: %s", webhook.Kind)
}
// replace url query %s with message
url := strings.Replace(webhook.OutgoingURL, "%s", url.QueryEscape(message), -1)
// post body contains only `text`
requestBody := types.WebhookBody{
Text: message,
}
req, err := svc.client.Post(url, requestBody)
if err != nil {
return nil, err
}
// execute outgoing webhook
resp, err := svc.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
// parse response body
responseBody := types.WebhookBody{}
contentType := resp.Header.Get("Content-Type")
switch {
case strings.Contains(contentType, "text/plain"):
// keep plain/text as-is
if b, err := ioutil.ReadAll(resp.Body); err != nil {
return nil, errors.WithStack(err)
} else {
responseBody.Text = string(b)
}
default:
switch resp.StatusCode {
case 200:
// assume the response is an expected json structure
if err := json.NewDecoder(resp.Body).Decode(&responseBody); err != nil {
return nil, errors.WithStack(err)
}
if responseBody.Text == "" {
return nil, errors.New("Empty webhook response")
}
default:
return nil, http.ToError(resp)
}
}
msg := &types.Message{
Message: responseBody.Text,
Meta: &types.MessageMeta{
Username: responseBody.Username,
},
}
avatar, err := store.FromURL(responseBody.Avatar)
if err != nil {
return svc.sendMessage(webhook, msg, nil)
}
defer avatar.Close()
return svc.sendMessage(webhook, msg, avatar)
}
func (svc *webhook) sendMessage(webhook *types.Webhook, msg *types.Message, avatar io.Reader) (*types.Message, error) {
// We need a webhook user context for message service
ctx := auth.SetIdentityToContext(svc.ctx, &systemTypes.User{ID: webhook.UserID})
messageSvc := Message(ctx)
msg.ChannelID = webhook.ChannelID
msg.UserID = webhook.UserID
return messageSvc.CreateWithAvatar(msg, avatar)
}
@@ -0,0 +1,87 @@
// +build integration,external
package service
import (
"context"
"strings"
"testing"
"github.com/crusttech/crust/internal/auth"
"github.com/crusttech/crust/internal/config"
"github.com/crusttech/crust/internal/http"
"github.com/crusttech/crust/internal/test"
"github.com/crusttech/crust/messaging/internal/repository"
"github.com/crusttech/crust/messaging/types"
systemService "github.com/crusttech/crust/system/service"
systemTypes "github.com/crusttech/crust/system/types"
)
func TestOutgoingWebhook(t *testing.T) {
var user = &systemTypes.User{ID: 1}
var channel = &types.Channel{ID: 1}
ctx := context.WithValue(context.Background(), "testing", true)
ctx = auth.SetIdentityToContext(ctx, user)
// set up user
{
svc := systemService.User(ctx)
_, err := svc.Create(user)
test.Assert(t, err == nil, "Error when creating user: %+v", err)
}
// set up channel
{
rpo := repository.Channel(ctx, repository.DB(ctx))
_, err := rpo.Create(channel)
test.Assert(t, err == nil, "Error when creating channel: %+v", err)
}
client, err := http.New(&config.HTTPClient{
Timeout: 10,
})
test.Assert(t, err == nil, "Error creating HTTP client: %+v", err)
svc := Webhook(ctx, client)
{
/* create outgoing webhook */
webhook, err := svc.Create(types.OutgoingWebhook, channel.ID, types.WebhookRequest{
Username: "test-webhook",
OutgoingTrigger: "fortune",
OutgoingURL: "https://api.scene-si.org/fortune.php",
})
test.Assert(t, err == nil, "Error when creating webhook: %+v", err)
/* find outgoing webhook */
webhooks, err := svc.Find(&types.WebhookFilter{
OutgoingTrigger: webhook.OutgoingTrigger,
})
test.Assert(t, err == nil, "Error when finding webhook: %+v", err)
test.Assert(t, len(webhooks) == 1, "Expected to find 1 webhook, got %d", len(webhooks))
/* trigger outgoing webhook */
{
message, err := svc.Do(webhooks[0], "")
test.Assert(t, err == nil, "Error when triggering webhook: %+v", err)
test.Assert(t, strings.Contains(message.Message, "BOFH"), "Unexpected webhook output: %s", message.Message)
}
// update webhook
wh, err := svc.Update(webhook.ID, types.OutgoingWebhook, channel.ID, types.WebhookRequest{
Username: "test-webhook-json",
OutgoingTrigger: "fortune-json",
OutgoingURL: "https://api.scene-si.org/fortune.php?username=test",
})
test.Assert(t, err == nil, "Error when updating webhook: %+v", err)
/* trigger outgoing webhook */
{
message, err := svc.Do(wh, "")
test.Assert(t, err == nil, "Error when triggering webhook: %+v", err)
test.Assert(t, message.Meta.Username == "test", "Expected message.meta.username = 'test', got: '%s'", message.Meta.Username)
test.Assert(t, strings.Contains(message.Message, "BOFH"), "Unexpected webhook output: %s", message.Message)
}
}
}
+6
View File
@@ -27,5 +27,11 @@ func (ctrl *Commands) List(ctx context.Context, r *request.CommandsList) (interf
&types.Command{
Name: "shrug",
Description: "It does exactly what it says on the tin"},
&types.Command{
Name: "tableflip",
Description: "Flip a table in anger"},
&types.Command{
Name: "unflip",
Description: "Put the table back from a flip"},
}, nil
}
+96
View File
@@ -0,0 +1,96 @@
package handlers
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `webhooks.go`, `webhooks.util.go` or `webhooks_test.go` to
implement your API calls, helper functions and tests. The file `webhooks.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"context"
"net/http"
"github.com/go-chi/chi"
"github.com/titpetric/factory/resputil"
"github.com/crusttech/crust/messaging/rest/request"
)
// Internal API interface
type WebhooksAPI interface {
List(context.Context, *request.WebhooksList) (interface{}, error)
Create(context.Context, *request.WebhooksCreate) (interface{}, error)
Update(context.Context, *request.WebhooksUpdate) (interface{}, error)
Get(context.Context, *request.WebhooksGet) (interface{}, error)
Delete(context.Context, *request.WebhooksDelete) (interface{}, error)
}
// HTTP API interface
type Webhooks struct {
List func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
Get func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
}
func NewWebhooks(wh WebhooksAPI) *Webhooks {
return &Webhooks{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewWebhooksList()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return wh.List(r.Context(), params)
})
},
Create: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewWebhooksCreate()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return wh.Create(r.Context(), params)
})
},
Update: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewWebhooksUpdate()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return wh.Update(r.Context(), params)
})
},
Get: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewWebhooksGet()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return wh.Get(r.Context(), params)
})
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewWebhooksDelete()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return wh.Delete(r.Context(), params)
})
},
}
}
func (wh *Webhooks) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/webhooks/", wh.List)
r.Post("/webhooks/", wh.Create)
r.Post("/webhooks/{webhookID}", wh.Update)
r.Get("/webhooks/{webhookID}", wh.Get)
r.Delete("/webhooks/{webhookID}", wh.Delete)
})
}
@@ -0,0 +1,66 @@
package handlers
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `webhooks_public.go`, `webhooks_public.util.go` or `webhooks_public_test.go` to
implement your API calls, helper functions and tests. The file `webhooks_public.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"context"
"net/http"
"github.com/go-chi/chi"
"github.com/titpetric/factory/resputil"
"github.com/crusttech/crust/messaging/rest/request"
)
// Internal API interface
type WebhooksPublicAPI interface {
Delete(context.Context, *request.WebhooksPublicDelete) (interface{}, error)
Create(context.Context, *request.WebhooksPublicCreate) (interface{}, error)
}
// HTTP API interface
type WebhooksPublic struct {
Delete func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
}
func NewWebhooksPublic(wh WebhooksPublicAPI) *WebhooksPublic {
return &WebhooksPublic{
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewWebhooksPublicDelete()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return wh.Delete(r.Context(), params)
})
},
Create: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewWebhooksPublicCreate()
resputil.JSON(w, params.Fill(r), func() (interface{}, error) {
return wh.Create(r.Context(), params)
})
},
}
}
func (wh *WebhooksPublic) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Delete("/webhooks/{webhookID}/{webhookToken}", wh.Delete)
r.Post("/webhooks/{webhookID}/{webhookToken}", wh.Create)
})
}
+4 -28
View File
@@ -17,8 +17,8 @@ var _ = errors.Wrap
type (
Message struct {
svc struct {
msg service.MessageService
event service.EventService
msg service.MessageService
command service.CommandService
}
}
)
@@ -26,6 +26,7 @@ type (
func (Message) New() *Message {
ctrl := &Message{}
ctrl.svc.msg = service.DefaultMessage
ctrl.svc.command = service.DefaultCommand
return ctrl
}
@@ -53,32 +54,7 @@ func (ctrl *Message) Edit(ctx context.Context, r *request.MessageEdit) (interfac
}
func (ctrl Message) ExecuteCommand(ctx context.Context, r *request.MessageExecuteCommand) (interface{}, error) {
switch r.Command {
case "me":
if r.Input != "" {
return ctrl.svc.msg.With(ctx).Create(&types.Message{
Type: types.MessageTypeIlleism,
ChannelID: r.ChannelID,
Message: r.Input,
})
}
return nil, nil
case "shrug":
msg := &types.Message{
ChannelID: r.ChannelID,
Message: `¯\\_(ツ)_/¯`,
}
if r.Input != "" {
msg.Message = r.Input + " " + msg.Message
}
return ctrl.svc.msg.With(ctx).Create(msg)
}
return nil, errors.New("unknown command")
return ctrl.svc.command.With(ctx).Do(r.ChannelID, r.Command, r.Input)
}
func (ctrl *Message) Delete(ctx context.Context, r *request.MessageDelete) (interface{}, error) {
+324
View File
@@ -0,0 +1,324 @@
package request
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `webhooks.go`, `webhooks.util.go` or `webhooks_test.go` to
implement your API calls, helper functions and tests. The file `webhooks.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"io"
"strings"
"encoding/json"
"mime/multipart"
"net/http"
"github.com/go-chi/chi"
"github.com/pkg/errors"
"github.com/crusttech/crust/messaging/types"
)
var _ = chi.URLParam
var _ = multipart.FileHeader{}
// Webhooks list request parameters
type WebhooksList struct {
ChannelID uint64 `json:",string"`
UserID uint64 `json:",string"`
}
func NewWebhooksList() *WebhooksList {
return &WebhooksList{}
}
func (wReq *WebhooksList) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(wReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["channelID"]; ok {
wReq.ChannelID = parseUInt64(val)
}
if val, ok := get["userID"]; ok {
wReq.UserID = parseUInt64(val)
}
return err
}
var _ RequestFiller = NewWebhooksList()
// Webhooks create request parameters
type WebhooksCreate struct {
ChannelID uint64 `json:",string"`
Kind types.WebhookKind
Trigger string
Url string
Username string
Avatar *multipart.FileHeader
AvatarURL string
}
func NewWebhooksCreate() *WebhooksCreate {
return &WebhooksCreate{}
}
func (wReq *WebhooksCreate) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(wReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseMultipartForm(32 << 20); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := post["channelID"]; ok {
wReq.ChannelID = parseUInt64(val)
}
if val, ok := post["kind"]; ok {
wReq.Kind = types.WebhookKind(val)
}
if val, ok := post["trigger"]; ok {
wReq.Trigger = val
}
if val, ok := post["url"]; ok {
wReq.Url = val
}
if val, ok := post["username"]; ok {
wReq.Username = val
}
if _, wReq.Avatar, err = r.FormFile("avatar"); err != nil {
return errors.Wrap(err, "error procesing uploaded file")
}
if val, ok := post["avatarURL"]; ok {
wReq.AvatarURL = val
}
return err
}
var _ RequestFiller = NewWebhooksCreate()
// Webhooks update request parameters
type WebhooksUpdate struct {
WebhookID uint64 `json:",string"`
ChannelID uint64 `json:",string"`
Kind types.WebhookKind
Trigger string
Url string
Username string
Avatar *multipart.FileHeader
AvatarURL string
}
func NewWebhooksUpdate() *WebhooksUpdate {
return &WebhooksUpdate{}
}
func (wReq *WebhooksUpdate) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(wReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseMultipartForm(32 << 20); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
wReq.WebhookID = parseUInt64(chi.URLParam(r, "webhookID"))
if val, ok := post["channelID"]; ok {
wReq.ChannelID = parseUInt64(val)
}
if val, ok := post["kind"]; ok {
wReq.Kind = types.WebhookKind(val)
}
if val, ok := post["trigger"]; ok {
wReq.Trigger = val
}
if val, ok := post["url"]; ok {
wReq.Url = val
}
if val, ok := post["username"]; ok {
wReq.Username = val
}
if _, wReq.Avatar, err = r.FormFile("avatar"); err != nil {
return errors.Wrap(err, "error procesing uploaded file")
}
if val, ok := post["avatarURL"]; ok {
wReq.AvatarURL = val
}
return err
}
var _ RequestFiller = NewWebhooksUpdate()
// Webhooks get request parameters
type WebhooksGet struct {
WebhookID uint64 `json:",string"`
}
func NewWebhooksGet() *WebhooksGet {
return &WebhooksGet{}
}
func (wReq *WebhooksGet) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(wReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
wReq.WebhookID = parseUInt64(chi.URLParam(r, "webhookID"))
return err
}
var _ RequestFiller = NewWebhooksGet()
// Webhooks delete request parameters
type WebhooksDelete struct {
WebhookID uint64 `json:",string"`
}
func NewWebhooksDelete() *WebhooksDelete {
return &WebhooksDelete{}
}
func (wReq *WebhooksDelete) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(wReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
wReq.WebhookID = parseUInt64(chi.URLParam(r, "webhookID"))
return err
}
var _ RequestFiller = NewWebhooksDelete()
+136
View File
@@ -0,0 +1,136 @@
package request
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `webhooks_public.go`, `webhooks_public.util.go` or `webhooks_public_test.go` to
implement your API calls, helper functions and tests. The file `webhooks_public.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"io"
"strings"
"encoding/json"
"mime/multipart"
"net/http"
"github.com/go-chi/chi"
"github.com/pkg/errors"
)
var _ = chi.URLParam
var _ = multipart.FileHeader{}
// WebhooksPublic delete request parameters
type WebhooksPublicDelete struct {
WebhookID uint64 `json:",string"`
WebhookToken string
}
func NewWebhooksPublicDelete() *WebhooksPublicDelete {
return &WebhooksPublicDelete{}
}
func (wReq *WebhooksPublicDelete) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(wReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
wReq.WebhookID = parseUInt64(chi.URLParam(r, "webhookID"))
wReq.WebhookToken = chi.URLParam(r, "webhookToken")
return err
}
var _ RequestFiller = NewWebhooksPublicDelete()
// WebhooksPublic create request parameters
type WebhooksPublicCreate struct {
Username string
AvatarURL string
Content string
WebhookID uint64 `json:",string"`
WebhookToken string
}
func NewWebhooksPublicCreate() *WebhooksPublicCreate {
return &WebhooksPublicCreate{}
}
func (wReq *WebhooksPublicCreate) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(wReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["username"]; ok {
wReq.Username = val
}
if val, ok := get["avatarURL"]; ok {
wReq.AvatarURL = val
}
if val, ok := get["content"]; ok {
wReq.Content = val
}
wReq.WebhookID = parseUInt64(chi.URLParam(r, "webhookID"))
wReq.WebhookToken = chi.URLParam(r, "webhookToken")
return err
}
var _ RequestFiller = NewWebhooksPublicCreate()
+69
View File
@@ -0,0 +1,69 @@
package rest
import (
"context"
"github.com/pkg/errors"
"github.com/crusttech/crust/internal/store"
"github.com/crusttech/crust/messaging/internal/service"
"github.com/crusttech/crust/messaging/rest/request"
"github.com/crusttech/crust/messaging/types"
)
var _ = errors.Wrap
type Webhooks struct {
webhook service.WebhookService
}
func (Webhooks) New() *Webhooks {
return &Webhooks{}
}
func (ctrl *Webhooks) Get(ctx context.Context, r *request.WebhooksGet) (interface{}, error) {
return ctrl.webhook.With(ctx).Get(r.WebhookID)
}
func (ctrl *Webhooks) Delete(ctx context.Context, r *request.WebhooksDelete) (interface{}, error) {
return nil, ctrl.webhook.With(ctx).Delete(r.WebhookID)
}
func (ctrl *Webhooks) List(ctx context.Context, r *request.WebhooksList) (interface{}, error) {
return ctrl.webhook.With(ctx).Find(&types.WebhookFilter{
ChannelID: r.ChannelID,
OwnerUserID: r.UserID,
})
}
func (ctrl *Webhooks) Create(ctx context.Context, r *request.WebhooksCreate) (interface{}, error) {
avatar, err := store.FromAny(r.Avatar, r.AvatarURL)
if err != nil {
return nil, err
}
defer avatar.Close()
// Webhook request parameters
parameters := types.WebhookRequest{
r.Username,
avatar,
r.Trigger,
r.Url,
}
return ctrl.webhook.With(ctx).Create(r.Kind, r.ChannelID, parameters)
}
func (ctrl *Webhooks) Update(ctx context.Context, r *request.WebhooksUpdate) (interface{}, error) {
avatar, err := store.FromAny(r.Avatar, r.AvatarURL)
if err != nil {
return nil, err
}
defer avatar.Close()
// Webhook request parameters
parameters := types.WebhookRequest{
r.Username,
avatar,
r.Trigger,
r.Url,
}
return ctrl.webhook.With(ctx).Update(r.WebhookID, r.Kind, r.ChannelID, parameters)
}
+34
View File
@@ -0,0 +1,34 @@
package rest
import (
"context"
"github.com/pkg/errors"
"github.com/crusttech/crust/internal/store"
"github.com/crusttech/crust/messaging/internal/service"
"github.com/crusttech/crust/messaging/rest/request"
)
var _ = errors.Wrap
type WebhooksPublic struct {
webhook service.WebhookService
}
func (WebhooksPublic) New() *WebhooksPublic {
return &WebhooksPublic{}
}
func (ctrl *WebhooksPublic) Delete(ctx context.Context, r *request.WebhooksPublicDelete) (interface{}, error) {
return nil, ctrl.webhook.With(ctx).DeleteByToken(r.WebhookID, r.WebhookToken)
}
func (ctrl *WebhooksPublic) Create(ctx context.Context, r *request.WebhooksPublicCreate) (interface{}, error) {
avatar, err := store.FromAny(nil, r.AvatarURL)
if err != nil {
return nil, err
}
defer avatar.Close()
return ctrl.webhook.With(ctx).Message(r.WebhookID, r.WebhookToken, r.Username, avatar, r.Content)
}
+4 -6
View File
@@ -1,10 +1,9 @@
package types
import (
"io"
"time"
"mime/multipart"
"github.com/crusttech/crust/internal/rules"
)
@@ -12,8 +11,8 @@ type (
Webhook struct {
ID uint64 `json:"id" db:"id"`
Kind WebhookKind `json:"kind" db:"webhook_kind"`
AuthToken string `json:"-" db:"webhook_token"`
Kind WebhookKind `json:"kind" db:"kind"`
AuthToken string `json:"-" db:"token"`
OwnerUserID uint64 `json:"userId" db:"rel_owner"`
@@ -33,8 +32,7 @@ type (
WebhookRequest struct {
Username string
Avatar *multipart.FileHeader
AvatarURL string
Avatar io.Reader
OutgoingTrigger string
OutgoingURL string