Skip to content

Commit

Permalink
Initial implementation.
Browse files Browse the repository at this point in the history
Allow sending basic messages from the command line.
  • Loading branch information
ronoaldo committed May 24, 2022
1 parent 1641c36 commit 989d75d
Show file tree
Hide file tree
Showing 9 changed files with 482 additions and 0 deletions.
92 changes: 92 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Google Chat Notifier

Simple Google Chat utilities for automated notifications.

# Install

As of now, I don't provide any pre-built binaries but you can
easily install with Go:

```
go install github.com/ronoaldo/chat-build-notifier/cmd/google-chat@latest
```

This will put the program `google-chat` in your `$GOPATH/bin` folder.

# Setup

Create a Google Chat space and add a new webhook. Grab the webhook URL and
store it in an environment variable, like:

```
export CHAT_WEBHOOK="https://chat.googleapis.com/v1/spaces/ABCDeFGh97I/messages?key=NoThisisNotAKEY..."
```

Alternatively, you can set an alias/script and pass the webhook URL with the `-webhook` option:

```
alias google-chat='google-chat -webhook="https://chat.googleapi..."
```

This could allow you to have different aliases for different spaces or bots.

# Usage

## Send a basic text message

After you have the environment variable set, you can start sending messages
like this:

```
google-chat -message "Testing chat webhook."
```

And they should show up in the space, like this:

![Sample chat message in the Google Chat space](./resources/sample-message.png)

## Send a message with an action link

Sometimes you also want to send a notification link with the message.
This can be done this way:

```
google-chat \
-message "Check out the codebase from Chat Build Notifier" \
-link https://github.com/ronoaldo/chat-build-notifier
```

![Sample chat message with a link at the bottom](./resources/chat-message-with-link.png)

## Send an error message with a link

You can change the title with the `-type` parameter as well as change how the
code

```
google-chat \
-type error \
-message "The backup failed to run!" \
-link "https://example.com/logs/backup.log" \
-link-name "View logs"
```

![Sample image showing all options in use to show an error notification](./resources/sample-error-message.png)


## All options

```
$ google-chat --help
Usage of google-chat:
-link URL
An optional link URL for the user to click
-link-name NAME
The action link NAME. (default "View details")
-message MESSAGE
The MESSAGE to send via webhook
-type TYPE
The TYPE of the message to send: [yes, info, error, warning] (default "info")
-webhook URL
The webtook URL to send the message to. If not provided, will try the environment var CHAT_WEBHOOK
```
123 changes: 123 additions & 0 deletions chat/card.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package chat

// Type Message represents a Google Chat message object that can be sent via
// Webhook. It is expected that you only pass either Text or Cards value, but
// not both.
type Message struct {
Text string `json:"text,omitempty"`

Cards []Card `json:"cards,omitempty"`
FallbackText string `json:"fallbackText,omitempty"`
}

// Type Card defines a card that can be used to compose a more elaborated
// message object.
type Card struct {
// Name of the card
Name string `json:"name,string"`

// Header is shown at the top of the card.
Header *CardHeader `json:"header,omitempty"`

// The card must have at least one section, which is composed by several
// widgets.
Sections []CardSection `json:"sections,omitempty"`
}

// Type CardHeader has the text details of a header shown in a card.
type CardHeader struct {
Title string `json:"title,omitempty"`
Subtitle string `json:"subtitle,omitempty"`
ImageURL string `json:"imageUrl,omitempty"`
ImageStyle string `json:"imageStyle,omitempty"`
}

// Type CardSection is one element of the card, composed by an optional header
// and one type of a Widget.
type CardSection struct {
Header string `json:"header,omitempty"`
Widgets []Widget `json:"widgets,omitempty"`
}

// Type Widget represents a single widget inside the card section.
//
// The widget type is expected to have only one of the available fields filled:
// you can set either TextParagraph, KeyValue or Image fields, but not more than
// one at the same time.
//
// The widget can also have one or more buttons.
type Widget struct {
// Your widget will have only one of TextParagraph, KeyValue or Image options.
TextParagraph *TextWidget `json:"textParagraph,omitempty"`
KeyValue *KeyValueWidget `json:"keyValue,omitempty"`
Image *ImageWidget `json:"image,omitempty"`

// Your widget can have several buttons.
Buttons []Button `json:"buttons,omitempty"`
}

// Type TextWidget represents very simple widget with just a set of text on it.
type TextWidget struct {
Text string `json:"text,omitempty"`
}

// Type KeyValueWidget displays a lable and a value.
type KeyValueWidget struct {
TopLabel string `json:"topLabel,omitempty"`
Content string `json:"content,omitempty"`
ContentMultiline bool `json:"contentMultiline,omitempty"`
BottomLabel string `json:"bottomLabel,omitempty"`

Icon string `json:"icon,omitempty"`
IconURL string `json:"iconUrl,omitempty"`
Button *Button `json:"button,omitempty"`
OnClick *ClickEvent `json:"onClick,omitempty"`
}

// Type ImageWidget displays an image.
type ImageWidget struct {
ImageURL string `json:"imageUrl,omitempty"`
OnClick *ClickEvent `json:"onClick,omitempty"`
}

// Type ClickEvent allows you to add actions to several elements. You are
// expected to fill either the OpenLink action, or define a custom function
// action. Custom function actions may not work as expected if you are only
// using a webhook call. Check the full documentation on the Chat API on
// https://developers.google.com/chat/api/ to learn more.
type ClickEvent struct {
OpenLink *OpenLinkAction `json:"openLink,omitempty"`
Action *FormAction `json:"action,omitempty"`
}

// Type OpenLinkAction allows the user to open the specified URL when the element is
// clicked.
type OpenLinkAction struct {
URL string `json:"url,omitempty"`
}

type FormAction struct {
MethodName string `json:"actionMethodName,omitempty"`
Parameters map[string]string `json:"parameters,omitempty"`
}

// Type Button allows messages to have either Text or Image buttons. Like the
// Widget, it is expected that you specify either TextButton or ImageButton.
type Button struct {
TextButton *TextButton `json:"textButton,omitempty"`
}

// Type TextButton is a simple button with text.
type TextButton struct {
Text string `json:"text,omitempty"`
OnClick *ClickEvent `json:"onClick,omitempty"`
}

// Type ImageButton is a more fancy button with an icon image. Either specify
// one of the built-in icons or an icon via URL.
type ImageButton struct {
Icon string `json:"icon,omitempty"`
IconURL string `json:"iconUrl,omitempty"`

OnClick *ClickEvent `json:"onClick,omitempty"`
}
139 changes: 139 additions & 0 deletions chat/card_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package chat

import (
"encoding/json"
"testing"

"github.com/nsf/jsondiff"
)

// Basic example of all functionality. From:
// https://developers.google.com/chat/api/guides/message-formats/cards#full_example_pizza_bot
var pizzaBotExample = `{
"cards": [
{
"header": {
"title": "Pizza Bot Customer Support",
"subtitle": "[email protected]",
"imageUrl": "https://goo.gl/aeDtrS"
},
"sections": [
{
"widgets": [
{
"keyValue": {
"topLabel": "Order No.",
"content": "12345"
}
},
{
"keyValue": {
"topLabel": "Status",
"content": "In Delivery"
}
}
]
},
{
"header": "Location",
"widgets": [
{
"image": {
"imageUrl": "https://maps.googleapis.com/..."
}
}
]
},
{
"widgets": [
{
"buttons": [
{
"textButton": {
"text": "OPEN ORDER",
"onClick": {
"openLink": {
"url": "https://example.com/orders/..."
}
}
}
}
]
}
]
}
]
}
]
}`

func TestPizzaBotFullExample(t *testing.T) {
cardMessage := Message{
Cards: []Card{
{
Header: &CardHeader{
Title: "Pizza Bot Customer Support",
Subtitle: "[email protected]",
ImageURL: "https://goo.gl/aeDtrS",
},
Sections: []CardSection{
{
Widgets: []Widget{
{
KeyValue: &KeyValueWidget{
TopLabel: "Order No.",
Content: "12345",
},
},
{
KeyValue: &KeyValueWidget{
TopLabel: "Status",
Content: "In Delivery",
},
},
},
},
{
Header: "Location",
Widgets: []Widget{
{
Image: &ImageWidget{
ImageURL: "https://maps.googleapis.com/...",
},
},
},
},
{
Widgets: []Widget{
{
Buttons: []Button{
{
TextButton: &TextButton{
Text: "OPEN ORDER",
OnClick: &ClickEvent{
OpenLink: &OpenLinkAction{
URL: "https://example.com/orders/...",
},
},
},
},
},
},
},
},
},
},
},
}

message, err := json.MarshalIndent(cardMessage, "", " ")
if err != nil {
t.Fatalf("Failed to encode message: %v", err)
}

opts := jsondiff.DefaultConsoleOptions()
diff, desc := jsondiff.Compare([]byte(message), []byte(pizzaBotExample), &opts)
if diff != jsondiff.FullMatch {
t.Errorf("Unexpected resulting message: %v,\n%v", diff, desc)
}
}
Loading

0 comments on commit 989d75d

Please sign in to comment.