Skip to content

Commit

Permalink
Use Adaptive Cards when sending reviews to MS teams (#538)
Browse files Browse the repository at this point in the history
* Only process review webhook events that are in the pending state
  • Loading branch information
sandromello authored Nov 7, 2024
1 parent 6f18c6c commit 7d49a40
Show file tree
Hide file tree
Showing 2 changed files with 145 additions and 86 deletions.
107 changes: 60 additions & 47 deletions gateway/transport/plugins/webhooks/microsoftteams.review.create.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,65 +5,78 @@
"title": "",
"description": "This event is triggered when a review is created. Refer to this documentation: https://hoop.dev/docs/features/microsoft-teams",
"properties": {
"@context": {
"type": {
"type": "string"
},
"@type": {
"type": "string"
},
"sections": {
"attachments": {
"type": "array"
},
"summary": {
"type": "string"
},
"themeColor": {
"type": "string"
}
},
"required": [],
"additionalProperties": false,
"examples": [
{
"@context": "http://schema.org/extensions",
"@type": "MessageCard",
"sections": [
{
"facts": [
{
"name": "Created By:",
"value": "John Doe | jonhdoe@unknown.corp"
},
{
"name": "Approval Groups:",
"value": "[\"admin\"]"
},
{
"name": "Session Time:",
"value": "```-```"
}
],
"startGroup": true,
"title": "• Session Created [6819488c-4cb6-49f4-be19-4f66ccbd919d](http://localhost:8009/sessions/6819488c-4cb6-49f4-be19-4f66ccbd919d)"
},
"type": "message",
"attachments": [
{
"facts": [
{
"name": "Connection:",
"value": "bash"
},
{
"name": "Script:",
"value": "```env```"
"content": {
"body": [
{
"separator": true,
"size": "Large",
"text": "Session Created",
"type": "TextBlock",
"weight": "Bolder"
},
{
"separator": false,
"text": "[6bb1f879-f994-4ade-9d69-121676c03a59](http://localhost:8009/sessions/6bb1f879-f994-4ade-9d69-121676c03a59)",
"type": "TextBlock"
},
{
"facts": [
{
"title": "Created By",
"value": "Sandro Mello | sandro@hoop.dev"
},
{
"title": "Approval Groups",
"value": "[\"admin\"]"
},
{
"title": "Session Time",
"value": "`-`"
},
{
"title": "Connection",
"value": "postgres-demo"
}
],
"separator": true,
"spacing": "ExtraLarge",
"type": "FactSet"
},
{
"bleed": false,
"items": [
{
"codeSnippet": "SELECT c.firstname, c.lastname, o.orderid, o.orderdate, SUM(ol.quantity) AS total_quantity, SUM(ol.quantity * p.price) AS total_amount\nFROM customers c\nJOIN orders o ON c.customerid = o.customerid\nJOIN orderlines ol ON o.orderid = ol.orderid\nJOIN products p ON ol.prod_id = p.prod_id\nWHERE c.country = 'US'\nGROUP BY c.firstname, c.lastname, o.orderid, o.orderdate\nORDER BY total_amount DESC;",
"language": "SQL",
"type": "CodeBlock"
}
],
"separator": false,
"style": "default",
"type": "Container"
}
],
"msteams": {
"width": "full"
}
],
"title": "Session Details"
},
"contentType": "application/vnd.microsoft.card.adaptive"
}
],
"summary": "Review Created",
"themeColor": "0076D7"
]
}
]
}


124 changes: 85 additions & 39 deletions gateway/transport/plugins/webhooks/webhooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"fmt"
"strconv"
"strings"
"time"

"github.com/google/uuid"
Expand Down Expand Up @@ -100,13 +101,39 @@ func (p *plugin) OnReceive(ctx plugintypes.Context, pkt *pb.Packet) (*plugintype
return nil, nil
}

// https://learn.microsoft.com/en-us/microsoftteams/platform/task-modules-and-cards/cards/cards-format?tabs=adaptive-md%2Cdesktop%2Cdesktop1%2Cdesktop2%2Cconnector-html#codeblock-in-adaptive-cards
func parseLangCodeBlock(connType, connSubtype string) string {
switch connType {
case "database":
return "SQL"
case "application":
switch connSubtype {
case "go", "java", "perl":
return strings.ToTitle(connSubtype)
case "json":
return "JSON"
case "xml":
return "XML"
case "powershell":
return "PowerShell"
case "php":
return "PHP"
default:
return "Bash"
}
case "custom":
return "Bash"
}
return "PlainText"
}

func (p *plugin) processReviewCreateEvent(ctx plugintypes.Context) {
rev, err := pgreview.New().FetchOneBySid(ctx, ctx.SID)
if err != nil {
log.Warnf("failed obtaining review, err=%v", err)
return
}
if rev == nil {
if rev == nil || rev.Status != types.ReviewStatusPending {
return
}
// it's recommended to sent events up to 20KB (Microsoft Teams)
Expand All @@ -121,51 +148,70 @@ func (p *plugin) processReviewCreateEvent(ctx plugintypes.Context) {
eventID := uuid.NewString()
accessDuration := rev.AccessDuration.String()
if accessDuration == "0s" {
accessDuration = "```-```"
accessDuration = "`-`"
}
apiURL := appconfig.Get().FullApiURL()
out, err := p.client.Message.Create(ctxtimeout, appID, &svix.MessageIn{
EventType: eventMSTeamsReviewCreateType,
EventId: *svix.NullableString(func() *string { v := eventID; return &v }()),
Payload: map[string]any{
"@type": "MessageCard",
"@context": "http://schema.org/extensions",
"themeColor": "0076D7",
"summary": "Review Created",
"sections": []map[string]any{
{
"startGroup": true,
"title": fmt.Sprintf("• Session Created [%s](%s/sessions/%s)", rev.Session, apiURL, rev.Session),
"facts": []map[string]string{
{
"name": "Created By:",
"value": fmt.Sprintf("%s | %s", rev.ReviewOwner.Name, rev.ReviewOwner.Email),
},
{
"name": "Approval Groups:",
"value": fmt.Sprintf("%q", parseGroups(rev.ReviewGroupsData)),
},
{
"name": "Session Time:",
"value": accessDuration,
},
svixPayload := map[string]any{
"type": "message",
"attachments": []map[string]any{{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": map[string]any{
"msteams": map[string]any{"width": "full"},
"body": []map[string]any{
{
"type": "TextBlock",
"text": "Session Created",
"size": "Large",
"separator": true,
"weight": "Bolder",
},
},
{
"title": "Session Details",
"facts": []map[string]string{
{
"name": "Connection:",
"value": rev.Connection.Name,
},
{
"name": "Script:",
"value": fmt.Sprintf("```%s```", rev.Input),
{
"type": "TextBlock",
"text": fmt.Sprintf("[%s](%s/sessions/%s)", rev.Session, apiURL, rev.Session),
"separator": false,
},
{
"spacing": "ExtraLarge",
"separator": true,
"type": "FactSet",
"facts": []map[string]any{
{
"title": "Created By",
"value": fmt.Sprintf("%s | %s", rev.ReviewOwner.Name, rev.ReviewOwner.Email),
},
{
"title": "Approval Groups",
"value": fmt.Sprintf("%q", parseGroups(rev.ReviewGroupsData)),
},
{
"title": "Session Time",
"value": accessDuration,
},
{
"title": "Connection",
"value": rev.Connection.Name,
},
},
},
{
"type": "Container",
"separator": true,
"style": "default",
"bleed": false,
"items": []map[string]any{{
"type": "CodeBlock",
"codeSnippet": rev.Input,
"language": parseLangCodeBlock(ctx.ConnectionType, ctx.ConnectionSubType),
}},
},
},
},
},
}},
}
out, err := p.client.Message.Create(ctxtimeout, appID, &svix.MessageIn{
EventType: eventMSTeamsReviewCreateType,
EventId: *svix.NullableString(func() *string { v := eventID; return &v }()),
Payload: svixPayload,
})
if err != nil {
log.With("appid", appID).Warnf("failed sending webhook event to remote source, err=%v", err)
Expand Down

0 comments on commit 7d49a40

Please sign in to comment.