My first Stride bot
This commit is contained in:
commit
ff5abbad6c
45
config.go
Normal file
45
config.go
Normal file
|
@ -0,0 +1,45 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Config is the configuration for the bot
|
||||
type Config struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
Token Token
|
||||
TokenCreated time.Time
|
||||
}
|
||||
|
||||
func loadConfig(filename string) (*Config, error) {
|
||||
f, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't open: %q: %q", filename, err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var conf Config
|
||||
err = json.NewDecoder(f).Decode(&conf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't read json: %q: %q", filename, err)
|
||||
}
|
||||
|
||||
return &conf, nil
|
||||
}
|
||||
|
||||
func saveConfig(filename string, conf *Config) error {
|
||||
f, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
err = json.NewEncoder(f).Encode(&conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
4
config.json.sample
Normal file
4
config.json.sample
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"client_id": "<copy from app page>",
|
||||
"client_secret": "<copy from app page>",
|
||||
}
|
55
descriptor.json.sample
Normal file
55
descriptor.json.sample
Normal file
|
@ -0,0 +1,55 @@
|
|||
{
|
||||
"baseUrl": "https://example.com/",
|
||||
"key": "maester",
|
||||
"lifecycle": {
|
||||
"installed": "/installed",
|
||||
"uninstalled": "/uninstalled"
|
||||
},
|
||||
"modules": {
|
||||
"chat:bot": [
|
||||
{
|
||||
"key": "bot-1",
|
||||
"mention": {
|
||||
"url": "/bot/mention"
|
||||
},
|
||||
"directMessage": {
|
||||
"url": "/bot/dm"
|
||||
}
|
||||
}
|
||||
],
|
||||
"chat:bot:messages": [
|
||||
{
|
||||
"key": "bot-2",
|
||||
"pattern": ".*",
|
||||
"url":"/bot/msg"
|
||||
}
|
||||
],
|
||||
"chat:glance": [
|
||||
{
|
||||
"key": "bot-glance",
|
||||
"name": {
|
||||
"value": "Reminders"
|
||||
},
|
||||
"label": {
|
||||
"value": "Reminders"
|
||||
},
|
||||
"icon": {
|
||||
"url": "/icon.png",
|
||||
"url@2x": "/icon.png"
|
||||
},
|
||||
"target": "bot-sidebar",
|
||||
"queryUrl": "/glance"
|
||||
}
|
||||
],
|
||||
"chat:sidebar": [
|
||||
{
|
||||
"key":"bot-sidebar",
|
||||
"name": {
|
||||
"value": "Reminders"
|
||||
},
|
||||
"url": "/sidebar",
|
||||
"authentication": "jwt"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
331
main.go
Normal file
331
main.go
Normal file
|
@ -0,0 +1,331 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"os"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
type echoHandler struct {
|
||||
Config Config
|
||||
}
|
||||
|
||||
type msgHandler struct {
|
||||
Counts map[string]int
|
||||
Config Config
|
||||
MessageSender chan simpleMessage
|
||||
}
|
||||
|
||||
type glanceHandler struct {
|
||||
Config Config
|
||||
|
||||
Glances map[glanceState]glanceState
|
||||
}
|
||||
type sidebarHandler struct {
|
||||
Config Config
|
||||
}
|
||||
|
||||
type conversation struct {
|
||||
ID string
|
||||
AvatarURL string
|
||||
IsArchived bool
|
||||
Name string
|
||||
Privacy string
|
||||
Topic string
|
||||
Type string
|
||||
Modified time.Time
|
||||
Created time.Time
|
||||
}
|
||||
|
||||
type messagePart struct {
|
||||
Type string
|
||||
Content []messagePart
|
||||
Text string
|
||||
}
|
||||
type sender struct {
|
||||
ID string
|
||||
}
|
||||
type message struct {
|
||||
ID string
|
||||
Body struct {
|
||||
messagePart
|
||||
Version int
|
||||
}
|
||||
Text string
|
||||
Sender sender
|
||||
Time time.Time `json:"ts"`
|
||||
}
|
||||
|
||||
type outgoingMessage struct {
|
||||
Body bodyType
|
||||
}
|
||||
|
||||
type bodyType struct {
|
||||
Version int
|
||||
Type string
|
||||
Content []messagePart
|
||||
Text string
|
||||
}
|
||||
|
||||
type incomingMessage struct {
|
||||
CloudID string
|
||||
Conversation conversation
|
||||
Message message
|
||||
Recipients []sender
|
||||
Sender sender
|
||||
Type string
|
||||
}
|
||||
|
||||
type glanceState struct {
|
||||
CloudID string
|
||||
ConversationID string
|
||||
}
|
||||
type glanceStateData struct {
|
||||
Label string `json:"label"`
|
||||
Metadata map[string]string `json:"metadata"`
|
||||
}
|
||||
|
||||
func (h *echoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
log.Println(r.Header)
|
||||
log.Println(*r.URL)
|
||||
|
||||
log.Println("MESSAGE:")
|
||||
_, err := io.Copy(os.Stdout, r.Body)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
fmt.Fprintf(w, "ok\n")
|
||||
}
|
||||
|
||||
type contextClaim struct {
|
||||
Context contextClaims `json:"context"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
type contextClaims struct {
|
||||
CloudID string `json:"cloudId"`
|
||||
ResourceID string `json:"resourceID"`
|
||||
ResourceType string `json:"resourceType"`
|
||||
}
|
||||
|
||||
func (h *glanceHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
tokenString := r.URL.Query().Get("jwt")
|
||||
|
||||
token, err := jwt.ParseWithClaims(tokenString, &contextClaim{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(h.Config.ClientSecret), nil
|
||||
})
|
||||
if err != nil {
|
||||
w.WriteHeader(401)
|
||||
fmt.Fprintf(w, "wrong jwt: %q", err)
|
||||
return
|
||||
}
|
||||
if claims, ok := token.Claims.(*contextClaim); ok && token.Valid {
|
||||
if _, e := h.Glances[glanceState{claims.Context.CloudID, claims.Context.ResourceID}]; !e {
|
||||
gs := glanceState{claims.Context.CloudID, claims.Context.ResourceID}
|
||||
h.Glances[gs] = gs
|
||||
}
|
||||
log.Println(h.Glances)
|
||||
} else {
|
||||
w.WriteHeader(401)
|
||||
fmt.Fprintf(w, "wrong jwt: %q", err)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Add("Content-Type", "application/json")
|
||||
fmt.Fprint(w, `{"label":"Value of the glance","metadata":{"count":"1"}}`)
|
||||
}
|
||||
|
||||
func (h *sidebarHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "text/html")
|
||||
fmt.Fprint(w, `
|
||||
<html>
|
||||
<head>
|
||||
<script src='https://dev-lib.stride.com/javascript/simple-xdm.js'></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello</h1>
|
||||
<script>
|
||||
AP.register({
|
||||
"message-received": function(data) {console.log(data);},
|
||||
"glance-update": function(data) {console.log(data);}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
}
|
||||
|
||||
var (
|
||||
karmaRegex = regexp.MustCompile(`(\S+)([+-]{2})`)
|
||||
karmaStatsRegex = regexp.MustCompile(`karma\s+(\S+)`)
|
||||
)
|
||||
|
||||
func (h *msgHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
log.Println(r.Header)
|
||||
log.Println(*r.URL)
|
||||
|
||||
var msg incomingMessage
|
||||
dec := json.NewDecoder(r.Body)
|
||||
err := dec.Decode(&msg)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
log.Printf("MESSAGE: %#v\n", msg)
|
||||
|
||||
if res := karmaRegex.FindAllStringSubmatch(msg.Message.Text, -1); len(res) >= 1 {
|
||||
for _, u := range res {
|
||||
count := 1
|
||||
username := u[1]
|
||||
if u[2] == "--" {
|
||||
count = -1
|
||||
}
|
||||
if c, e := h.Counts[username]; e {
|
||||
count += c
|
||||
}
|
||||
h.Counts[username] = count
|
||||
|
||||
// Reply in the same room, to the same user
|
||||
text := fmt.Sprintf("%s has %d karma\n", username, count)
|
||||
h.MessageSender <- simpleMessage{text, msg.CloudID, msg.Conversation.ID}
|
||||
|
||||
f, err := os.Create("counts.json")
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
json.NewEncoder(f).Encode(&h.Counts)
|
||||
}
|
||||
} else if matches := karmaStatsRegex.FindAllStringSubmatch(msg.Message.Text, -1); len(matches) >= 1 {
|
||||
go func(msg incomingMessage) {
|
||||
username := matches[0][1]
|
||||
var text string
|
||||
if c, e := h.Counts[username]; e {
|
||||
text = fmt.Sprintf("%s has %d karma\n", username, c)
|
||||
} else {
|
||||
text = fmt.Sprintf("%s has no karma, add with %s++\n", username, username)
|
||||
}
|
||||
h.MessageSender <- simpleMessage{text, msg.CloudID, msg.Conversation.ID}
|
||||
}(msg)
|
||||
}
|
||||
|
||||
fmt.Fprintf(w, "ok\n")
|
||||
}
|
||||
|
||||
type simpleMessage struct {
|
||||
Message string
|
||||
CloudID string
|
||||
ConversationID string
|
||||
}
|
||||
|
||||
func main() {
|
||||
config, err := loadConfig("config.json")
|
||||
if err != nil {
|
||||
log.Fatalf("error while loading config: %s", err)
|
||||
}
|
||||
|
||||
config.Token, err = getToken(config)
|
||||
if err != nil {
|
||||
log.Fatalf("error while getting token: %q", err)
|
||||
}
|
||||
|
||||
config.TokenCreated = time.Now()
|
||||
|
||||
saveConfig("config.json", config)
|
||||
|
||||
messageSender := make(chan simpleMessage)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case msg := <-messageSender:
|
||||
var b bytes.Buffer
|
||||
b.WriteString(msg.Message)
|
||||
sendMessage(&b, msg.CloudID, msg.ConversationID, config.Token.AccessToken)
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
eh := &echoHandler{Config: *config}
|
||||
mh := &msgHandler{Config: *config, MessageSender: messageSender}
|
||||
gh := &glanceHandler{Config: *config}
|
||||
sh := &sidebarHandler{Config: *config}
|
||||
|
||||
mh.Counts = make(map[string]int)
|
||||
|
||||
f, err := os.Open("counts.json")
|
||||
if err != nil {
|
||||
log.Fatalf("missing file: counts.json: %q", err)
|
||||
}
|
||||
|
||||
json.NewDecoder(f).Decode(&mh.Counts)
|
||||
|
||||
gh.Glances = make(map[glanceState]glanceState)
|
||||
|
||||
ticker := time.Tick(20 * time.Second)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker:
|
||||
client := http.Client{}
|
||||
for _, gs := range gh.Glances {
|
||||
log.Println("Updating glances")
|
||||
var b bytes.Buffer
|
||||
u := fmt.Sprintf(
|
||||
"https://api.atlassian.com/site/%s/conversation/%s/app/module/chat:glance/%s/state",
|
||||
gs.CloudID,
|
||||
gs.ConversationID,
|
||||
"bot-glance",
|
||||
)
|
||||
var data glanceStateData
|
||||
data.Label = fmt.Sprintf("%d items", rand.Intn(10)+1)
|
||||
data.Metadata = make(map[string]string)
|
||||
data.Metadata["count"] = "8"
|
||||
err = json.NewEncoder(&b).Encode(&data)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
req, err := http.NewRequest("PUT", u, &b)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", "Bearer "+config.Token.AccessToken)
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
io.Copy(os.Stderr, res.Body)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
http.Handle("/installed", eh)
|
||||
http.Handle("/uninstalled", eh)
|
||||
http.Handle("/bot/mention", mh)
|
||||
http.Handle("/bot/dm", mh)
|
||||
http.Handle("/bot/msg", mh)
|
||||
http.Handle("/glance", gh)
|
||||
http.Handle("/sidebar", sh)
|
||||
|
||||
http.Handle("/", http.FileServer(http.Dir(".")))
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
32
message.go
Normal file
32
message.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
func sendMessage(b io.Reader, cloudID, conversationID string, token string) {
|
||||
u := fmt.Sprintf("https://api.atlassian.com/site/%s/conversation/%s/message", cloudID, conversationID)
|
||||
client := http.Client{}
|
||||
|
||||
req, err := http.NewRequest("POST", u, b)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
req.Header.Add("Authorization", "Bearer "+token)
|
||||
req.Header.Add("Content-Type", "text/plain")
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
io.Copy(os.Stderr, res.Body)
|
||||
}
|
58
token.go
Normal file
58
token.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
type Token struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
Scope string `json:"scope"`
|
||||
TokenType string `json:"token_type"`
|
||||
}
|
||||
|
||||
type TokenRequest struct {
|
||||
GrantType string `json:"grant_type"`
|
||||
ClientID string `json:"client_id"`
|
||||
ClientSecret string `json:"client_secret"`
|
||||
}
|
||||
|
||||
func getToken(conf *Config) (Token, error) {
|
||||
var tok Token
|
||||
client := http.Client{}
|
||||
var b bytes.Buffer
|
||||
var tokReq TokenRequest
|
||||
tokReq.ClientID = conf.ClientID
|
||||
tokReq.ClientSecret = conf.ClientSecret
|
||||
tokReq.GrantType = "client_credentials"
|
||||
|
||||
err := json.NewEncoder(&b).Encode(&tokReq)
|
||||
if err != nil {
|
||||
return tok, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", "https://api.atlassian.com/oauth/token", &b)
|
||||
if err != nil {
|
||||
return tok, err
|
||||
}
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return tok, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
err = json.NewDecoder(io.TeeReader(res.Body, os.Stdout)).Decode(&tok)
|
||||
if err != nil {
|
||||
return tok, err
|
||||
}
|
||||
log.Println(tok)
|
||||
|
||||
return tok, nil
|
||||
}
|
Loading…
Reference in New Issue
Block a user