-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Allow sending basic messages from the command line.
- Loading branch information
Showing
9 changed files
with
482 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"` | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
Oops, something went wrong.