Skip to content

Commit

Permalink
Merge pull request #25 from krishnateja262/feature/webhooks
Browse files Browse the repository at this point in the history
first draft with webhook support
  • Loading branch information
plutov authored Oct 16, 2024
2 parents 7a2b162 + ee2122c commit b84276e
Show file tree
Hide file tree
Showing 16 changed files with 174 additions and 21 deletions.
7 changes: 0 additions & 7 deletions api/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,11 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
Expand Down
Empty file.
9 changes: 9 additions & 0 deletions api/migrations/postgres/000002_webhooks.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE
surveys_webhook_responses (
id serial NOT NULL PRIMARY KEY,
created_at timestamp without time zone default (now () at time zone 'utc'),
session_id integer NOT NULL,
response_status integer NOT NULL,
response TEXT,
CONSTRAINT fk_surveys_webhooks1 FOREIGN KEY (session_id) REFERENCES surveys_sessions (id) ON DELETE CASCADE,
);
Empty file.
8 changes: 8 additions & 0 deletions api/migrations/sqlite/000002_webhooks.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE surveys_webhook_responses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
created_at TEXT,
session_id INTEGER NOT NULL,
response_status INTEGER NOT NULL,
response TEXT,
FOREIGN KEY (session_id) REFERENCES surveys_sessions (id) ON DELETE CASCADE
);
19 changes: 14 additions & 5 deletions api/pkg/controllers/survey_sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/labstack/echo/v4"
"github.com/plutov/formulosity/api/pkg/http/response"

"github.com/plutov/formulosity/api/pkg/surveys"
surveyspkg "github.com/plutov/formulosity/api/pkg/surveys"
"github.com/plutov/formulosity/api/pkg/types"
Expand Down Expand Up @@ -102,6 +103,14 @@ func (h *Handler) submitSurveyAnswer(c echo.Context) error {
return response.NotFound(c, err.Error())
}

if session.Status == types.SurveySessionStatus_Completed {
go func() {
if err := surveyspkg.CallWebhook(h.Services, survey, session); err != nil {
log.Println("call webhook error:", err)
}
}()
}

return response.Ok(c, *session)
}

Expand Down Expand Up @@ -150,16 +159,16 @@ func (h *Handler) getUploadedFile(c echo.Context, req []byte) (*types.File, erro
return nil, errors.New("file not provided")
}
fileName := header.Filename
fileExt := strings.ToLower(filepath.Ext(fileName))
fileExt := strings.ToLower(filepath.Ext(fileName))

defer file.Close()

uploadedFile = &types.File{
Name: header.Filename,
Data: file,
Size: header.Size,
Name: header.Filename,
Data: file,
Size: header.Size,
Format: fileExt,
}
}
return uploadedFile, nil
}
}
4 changes: 3 additions & 1 deletion api/pkg/storage/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ type Interface interface {
GetSurveySessionsWithAnswers(surveyUUID string, filter *types.SurveySessionsFilter) ([]types.SurveySession, int, error)
GetSurveySessionAnswers(sessionUUID string) ([]types.QuestionAnswer, error)
UpsertSurveyQuestionAnswer(sessionUUID string, questionUUID string, answer types.Answer) error

StoreWebhookResponse(sessionId int, responseStatus int, response string) error
}

type FileInterface interface {
Init() error

SaveFile(file *types.File) (string, error)
}
}
19 changes: 17 additions & 2 deletions api/pkg/storage/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os"
"strings"
"time"

"github.com/golang-migrate/migrate/v4"
migratepg "github.com/golang-migrate/migrate/v4/database/postgres"
Expand Down Expand Up @@ -347,11 +348,12 @@ func (p *Postgres) GetSurveySessionsWithAnswers(surveyUUID string, filter *types
LIMIT %d OFFSET %d
)
SELECT
ss.id, ss.uuid, ss.created_at, ss.completed_at, ss.status, q.id, q.uuid, sa.answer
ss.id, ss.uuid, ss.created_at, ss.completed_at, ss.status, q.id, q.uuid, sa.answer, w.response_status, w.response
FROM limited_sessions AS ss
INNER JOIN surveys AS s ON s.id = ss.survey_id
LEFT JOIN surveys_answers AS sa ON sa.session_id = ss.id
LEFT JOIN surveys_questions AS q ON q.id = sa.question_id
LEFT JOIN surveys_webhook_responses AS w ON w.session_id = ss.id
WHERE s.uuid=$1
ORDER BY ss.%s %s
;`, filter.SortBy, filter.Order, filter.Limit, filter.Offset, filter.SortBy, filter.Order)
Expand All @@ -365,17 +367,20 @@ func (p *Postgres) GetSurveySessionsWithAnswers(surveyUUID string, filter *types
sessionsMap := map[string]types.SurveySession{}
for rows.Next() {
session := types.SurveySession{}
webhookData := types.WebhookData{}
answer := types.QuestionAnswer{}
var (
questionID sql.NullString
questionUUID sql.NullString
)

err := rows.Scan(&session.ID, &session.UUID, &session.CreatedAt, &session.CompletedAt, &session.Status, &questionID, &questionUUID, &answer.AnswerBytes)
err := rows.Scan(&session.ID, &session.UUID, &session.CreatedAt, &session.CompletedAt, &session.Status, &questionID, &questionUUID, &answer.AnswerBytes, &webhookData.StatusCode, &webhookData.Response)
if err != nil {
return nil, 0, err
}

session.WebhookData = webhookData

if _, ok := sessionsMap[session.UUID]; !ok {
session.QuestionAnswers = []types.QuestionAnswer{}
sessionsMap[session.UUID] = session
Expand Down Expand Up @@ -417,3 +422,13 @@ func (p *Postgres) getSurveySessionsCount(surveyUUID string) (int, error) {
err := row.Scan(&count)
return count, err
}

func (p *Postgres) StoreWebhookResponse(sessionId int, responseStatus int, response string) error {
query := `INSERT INTO surveys_webhook_responses
(created_at, session_id, response_status, response)
VALUES ($1, $2, $3, $4);`

createdAtStr := time.Now().UTC().Format(types.DateTimeFormat)
_, err := p.conn.Exec(query, createdAtStr, sessionId, responseStatus, response)
return err
}
25 changes: 23 additions & 2 deletions api/pkg/storage/sqlite.go
Original file line number Diff line number Diff line change
Expand Up @@ -386,11 +386,12 @@ func (p *Sqlite) GetSurveySessionsWithAnswers(surveyUUID string, filter *types.S
LIMIT %d OFFSET %d
)
SELECT
ss.id, ss.uuid, ss.created_at, ss.completed_at, ss.status, q.id, q.uuid, sa.answer
ss.id, ss.uuid, ss.created_at, ss.completed_at, ss.status, q.id, q.uuid, sa.answer, w.response_status, w.response
FROM limited_sessions AS ss
INNER JOIN surveys AS s ON s.id = ss.survey_id
LEFT JOIN surveys_answers AS sa ON sa.session_id = ss.id
LEFT JOIN surveys_questions AS q ON q.id = sa.question_id
LEFT JOIN surveys_webhook_responses AS w ON w.session_id = ss.id
WHERE s.uuid=$1
ORDER BY ss.%s %s
;`, filter.SortBy, filter.Order, filter.Limit, filter.Offset, filter.SortBy, filter.Order)
Expand All @@ -411,9 +412,11 @@ func (p *Sqlite) GetSurveySessionsWithAnswers(surveyUUID string, filter *types.S
createdAtStr sql.NullString
completedAtStr sql.NullString
answerStr sql.NullString
httpStatusCode sql.NullInt16
httpResponse sql.NullString
)

err := rows.Scan(&session.ID, &session.UUID, &createdAtStr, &completedAtStr, &session.Status, &questionID, &questionUUID, &answerStr)
err := rows.Scan(&session.ID, &session.UUID, &createdAtStr, &completedAtStr, &session.Status, &questionID, &questionUUID, &answerStr, &httpStatusCode, &httpResponse)
if err != nil {
return nil, 0, err
}
Expand All @@ -425,6 +428,13 @@ func (p *Sqlite) GetSurveySessionsWithAnswers(surveyUUID string, filter *types.S
}
answer.AnswerBytes = []byte(answerStr.String)

if httpStatusCode.Valid && httpResponse.Valid {
session.WebhookData = types.WebhookData{
StatusCode: httpStatusCode.Int16,
Response: httpResponse.String,
}
}

if _, ok := sessionsMap[session.UUID]; !ok {
session.QuestionAnswers = []types.QuestionAnswer{}
sessionsMap[session.UUID] = session
Expand Down Expand Up @@ -466,3 +476,14 @@ func (p *Sqlite) getSurveySessionsCount(surveyUUID string) (int, error) {
err := row.Scan(&count)
return count, err
}

func (p *Sqlite) StoreWebhookResponse(sessionId int, responseStatus int, response string) error {
query := `INSERT INTO surveys_webhook_responses
(created_at, session_id, response_status, response)
VALUES ($1, $2, $3, $4);`

createdAtStr := time.Now().UTC().Format(types.DateTimeFormat)

_, err := p.conn.Exec(query, createdAtStr, sessionId, responseStatus, response)
return err
}
37 changes: 37 additions & 0 deletions api/pkg/surveys/sessions.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
package surveys

import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"time"

"github.com/plutov/formulosity/api/pkg/log"
"github.com/plutov/formulosity/api/pkg/services"
Expand Down Expand Up @@ -105,3 +110,35 @@ func GetSurveySessions(svc services.Services, survey types.Survey, filter *types

return sessions, pagesCount, nil
}

func CallWebhook(svc services.Services, survey *types.Survey, session *types.SurveySession) error {
client := &http.Client{
Timeout: 10 * time.Second,
}

data, err := json.Marshal(session)
if err != nil {
return fmt.Errorf("invalid post data, err: %v", err)
}

req, err := http.NewRequest(survey.Config.Webhook.Method, survey.Config.Webhook.URL, bytes.NewBuffer(data))
if err != nil {
return fmt.Errorf("invalid http request, err: %v", err)
}

req.Header.Set("Content-Type", "application/json")

resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("error making request, err: %v", err)
}
defer resp.Body.Close()

statusCode := resp.StatusCode
responseBody, err := io.ReadAll(resp.Body)
if err != nil {
responseBody = []byte{}
}

return svc.Storage.StoreWebhookResponse(int(session.ID), statusCode, string(responseBody))
}
15 changes: 11 additions & 4 deletions api/pkg/types/survey.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,11 @@ type SurveyStats struct {
}

type SurveyConfig struct {
Title string `json:"title" yaml:"title"`
Intro string `json:"intro" yaml:"intro"`
Outro string `json:"outro" yaml:"outro"`
Theme string `json:"theme" yaml:"theme"`
Title string `json:"title" yaml:"title"`
Intro string `json:"intro" yaml:"intro"`
Outro string `json:"outro" yaml:"outro"`
Theme string `json:"theme" yaml:"theme"`
Webhook *WebhookConfig `json:"webhook" yaml:"webhook"`

Hash string `json:"hash" yaml:"-"`
Questions *Questions `json:"questions" yaml:"-"`
Expand Down Expand Up @@ -122,6 +123,12 @@ func (s *SurveyConfig) Validate() error {
return err
}

if s.Webhook != nil {
if err := s.Webhook.Validate(); err != nil {
return err
}
}

return nil
}

Expand Down
6 changes: 6 additions & 0 deletions api/pkg/types/survey_session.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ type SurveySession struct {
SurveyUUID string `json:"survey_uuid"`
IPAddr string `json:"ip_addr"`
QuestionAnswers []QuestionAnswer `json:"question_answers"`
WebhookData WebhookData `json:"webhookData"`
}

type WebhookData struct {
StatusCode int16 `json:"statusCode"`
Response string `json:"response"`
}

type SurveySessionsFilter struct {
Expand Down
33 changes: 33 additions & 0 deletions api/pkg/types/webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package types

import (
"errors"
"net/url"
"strings"
)

type WebhookConfig struct {
URL string `json:"url" yaml:"url"`
Method string `json:"method" yaml:"method"`
}

func (wc *WebhookConfig) Validate() error {
parsedUrl, err := url.ParseRequestURI(wc.URL)
if err != nil {
return errors.New("webhook url format invalid")
}

if parsedUrl.Scheme != "http" && parsedUrl.Scheme != "https" {
return errors.New("webhook scheme invalid")
}

if parsedUrl.Host == "" {
return errors.New("webhook host invalid")
}

if !strings.EqualFold(wc.Method, "POST") {
return errors.New("unsupported http method for webhook")
}

return nil
}
3 changes: 3 additions & 0 deletions api/surveys/short/metadata.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ intro: |
Welcome to the survey.
outro: |
Thank you for taking the survey.
webhook:
url: https://formulosity.requestcatcher.com/
method: POST
4 changes: 4 additions & 0 deletions ui/src/components/app/SurveyResponsesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ export function SurveyResponsesPage({
{col.label}
</Table.HeadCell>
))}
<Table.HeadCell>Webhook Status</Table.HeadCell>
<Table.HeadCell>Actions</Table.HeadCell>
</Table.Head>
<Table.Body className="divide-y">
Expand Down Expand Up @@ -182,6 +183,9 @@ export function SurveyResponsesPage({
{session.completed_at &&
moment(session.completed_at).format('MMM D, YYYY h:mm a')}
</Table.Cell>
<Table.Cell>
{session.webhookData.statusCode}
</Table.Cell>
<Table.Cell>
<Button
className="h-8 bg-crimson-9 enabled:hover:bg-crimson-11 p-2"
Expand Down
6 changes: 6 additions & 0 deletions ui/src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@ export type SurveySession = {
created_at: string
completed_at: string
question_answers: SurveyQuestionAnswer[]
webhookData: WebhookData
}

export type WebhookData = {
response: string
statusCode: number
}

export type SurveyQuestionAnswer = {
Expand Down

0 comments on commit b84276e

Please sign in to comment.