diff --git a/README.md b/README.md index bbea586..658d6ea 100644 --- a/README.md +++ b/README.md @@ -303,6 +303,21 @@ curl -X POST localhost:4050/admin/configureService --data-binary '{ }' ``` +## Starting a Giphy Service + +### Create a Giphy bot + +``` +curl -X POST localhost:4050/admin/configureService --data-binary '{ + "Type": "giphy", + "Id": "giphyid", + "UserID": "@goneb:localhost", + "Config": { + "APIKey": "YOUR_API_KEY" + } +}' +``` + # Developing on go-neb. diff --git a/src/github.com/matrix-org/go-neb/clients/clients.go b/src/github.com/matrix-org/go-neb/clients/clients.go index 0207a5f..3c9a5b6 100644 --- a/src/github.com/matrix-org/go-neb/clients/clients.go +++ b/src/github.com/matrix-org/go-neb/clients/clients.go @@ -151,7 +151,7 @@ func (c *Clients) newClient(config types.ClientConfig) (*matrix.Client, error) { } var plugins []plugin.Plugin for _, service := range services { - plugins = append(plugins, service.Plugin(event.RoomID)) + plugins = append(plugins, service.Plugin(client, event.RoomID)) } plugin.OnMessage(plugins, client, event) }) diff --git a/src/github.com/matrix-org/go-neb/goneb.go b/src/github.com/matrix-org/go-neb/goneb.go index a445a48..0d99e47 100644 --- a/src/github.com/matrix-org/go-neb/goneb.go +++ b/src/github.com/matrix-org/go-neb/goneb.go @@ -9,6 +9,7 @@ import ( _ "github.com/matrix-org/go-neb/realms/jira" "github.com/matrix-org/go-neb/server" _ "github.com/matrix-org/go-neb/services/echo" + _ "github.com/matrix-org/go-neb/services/giphy" _ "github.com/matrix-org/go-neb/services/github" _ "github.com/matrix-org/go-neb/services/jira" "github.com/matrix-org/go-neb/types" diff --git a/src/github.com/matrix-org/go-neb/matrix/matrix.go b/src/github.com/matrix-org/go-neb/matrix/matrix.go index ccf8544..9e59bb3 100644 --- a/src/github.com/matrix-org/go-neb/matrix/matrix.go +++ b/src/github.com/matrix-org/go-neb/matrix/matrix.go @@ -14,8 +14,10 @@ package matrix import ( "bytes" "encoding/json" + "fmt" log "github.com/Sirupsen/logrus" "github.com/matrix-org/go-neb/errors" + "io" "io/ioutil" "net/http" "net/url" @@ -44,9 +46,17 @@ type Client struct { } func (cli *Client) buildURL(urlPath ...string) string { + ps := []string{cli.Prefix} + for _, p := range urlPath { + ps = append(ps, p) + } + return cli.buildBaseURL(ps...) +} + +func (cli *Client) buildBaseURL(urlPath ...string) string { // copy the URL. Purposefully ignore error as the input is from a valid URL already hsURL, _ := url.Parse(cli.HomeserverURL.String()) - parts := []string{hsURL.Path, cli.Prefix} + parts := []string{hsURL.Path} parts = append(parts, urlPath...) hsURL.Path = path.Join(parts...) query := hsURL.Query() @@ -116,6 +126,52 @@ func (cli *Client) SendText(roomID, text string) (string, error) { TextMessage{"m.text", text}) } +// UploadLink uploads an HTTP URL and then returns an MXC URI. +func (cli *Client) UploadLink(link string) (string, error) { + res, err := http.Get(link) + if res != nil { + defer res.Body.Close() + } + if err != nil { + return "", err + } + log.Print("GOT GIF BODY ", link) + return cli.UploadToContentRepo(res.Body, res.Header.Get("Content-Type"), res.Header.Get("Content-Length")) +} + +// UploadToContentRepo uploads the given bytes to the content repository and returns an MXC URI. +func (cli *Client) UploadToContentRepo(content io.Reader, contentType, contentLength string) (string, error) { + req, err := http.NewRequest("POST", cli.buildBaseURL("_matrix/media/r0/upload"), content) + if err != nil { + return "", err + } + req.Header.Set("Content-Type", contentType) + cl, err := strconv.Atoi(contentLength) + if err != nil { + return "", err + } + req.ContentLength = int64(cl) + log.Print("Doing upload request to ", req.URL) + log.Print("Type=", contentType, "Length=", contentLength) + res, err := cli.httpClient.Do(req) + if res != nil { + defer res.Body.Close() + } + if err != nil { + return "", err + } + if res.StatusCode != 200 { + return "", fmt.Errorf("Upload request returned HTTP %d", res.StatusCode) + } + m := struct { + ContentURI string `json:"content_uri"` + }{} + if err := json.NewDecoder(res.Body).Decode(&m); err != nil { + return "", err + } + return m.ContentURI, nil +} + // Sync starts syncing with the provided Homeserver. This function will be invoked continually. // If Sync is called twice then the first sync will be stopped. func (cli *Client) Sync() { diff --git a/src/github.com/matrix-org/go-neb/matrix/types.go b/src/github.com/matrix-org/go-neb/matrix/types.go index 900b16a..1d1f20f 100644 --- a/src/github.com/matrix-org/go-neb/matrix/types.go +++ b/src/github.com/matrix-org/go-neb/matrix/types.go @@ -95,6 +95,21 @@ type TextMessage struct { Body string `json:"body"` } +type ImageInfo struct { + Height uint `json:"h"` + Width uint `json:"w"` + Mimetype string `json:"mimetype"` + Size uint `json:"size"` +} + +// ImageMessage is an m.image event +type ImageMessage struct { + MsgType string `json:"msgtype"` + Body string `json:"body"` + URL string `json:"url"` + Info ImageInfo `json:"info"` +} + // An HTMLMessage is the contents of a Matrix HTML formated message event. type HTMLMessage struct { Body string `json:"body"` diff --git a/src/github.com/matrix-org/go-neb/services/echo/echo.go b/src/github.com/matrix-org/go-neb/services/echo/echo.go index 83a2c74..f56beef 100644 --- a/src/github.com/matrix-org/go-neb/services/echo/echo.go +++ b/src/github.com/matrix-org/go-neb/services/echo/echo.go @@ -17,7 +17,7 @@ func (e *echoService) ServiceUserID() string func (e *echoService) ServiceID() string { return e.id } func (e *echoService) ServiceType() string { return "echo" } func (e *echoService) Register(oldService types.Service, client *matrix.Client) error { return nil } -func (e *echoService) Plugin(roomID string) plugin.Plugin { +func (e *echoService) Plugin(cli *matrix.Client, roomID string) plugin.Plugin { return plugin.Plugin{ Commands: []plugin.Command{ plugin.Command{ diff --git a/src/github.com/matrix-org/go-neb/services/giphy/giphy.go b/src/github.com/matrix-org/go-neb/services/giphy/giphy.go new file mode 100644 index 0000000..d3502ce --- /dev/null +++ b/src/github.com/matrix-org/go-neb/services/giphy/giphy.go @@ -0,0 +1,127 @@ +package services + +import ( + "encoding/json" + "errors" + log "github.com/Sirupsen/logrus" + "github.com/matrix-org/go-neb/matrix" + "github.com/matrix-org/go-neb/plugin" + "github.com/matrix-org/go-neb/types" + "net/http" + "net/url" + "strconv" + "strings" +) + +type result struct { + Slug string `json:"slug"` + Images struct { + Original struct { + URL string `json:"url"` + // Giphy returns ints as strings.. + Width string `json:"width"` + Height string `json:"height"` + Size string `json:"size"` + } `json:"original"` + } `json:"images"` +} + +type giphySearch struct { + Data []result +} + +type giphyService struct { + id string + serviceUserID string + APIKey string // beta key is dc6zaTOxFJmzC +} + +func (s *giphyService) ServiceUserID() string { return s.serviceUserID } +func (s *giphyService) ServiceID() string { return s.id } +func (s *giphyService) ServiceType() string { return "giphy" } +func (s *giphyService) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *matrix.Client) { +} +func (s *giphyService) Register(oldService types.Service, client *matrix.Client) error { return nil } + +func (s *giphyService) Plugin(client *matrix.Client, roomID string) plugin.Plugin { + return plugin.Plugin{ + Commands: []plugin.Command{ + plugin.Command{ + Path: []string{"giphy"}, + Command: func(roomID, userID string, args []string) (interface{}, error) { + return s.cmdGiphy(client, roomID, userID, args) + }, + }, + }, + } +} +func (s *giphyService) cmdGiphy(client *matrix.Client, roomID, userID string, args []string) (interface{}, error) { + // only 1 arg which is the text to search for. + query := strings.Join(args, " ") + gifResult, err := s.searchGiphy(query) + if err != nil { + return nil, err + } + log.Print("GOT ", gifResult) + mxc, err := client.UploadLink(gifResult.Images.Original.URL) + if err != nil { + return nil, err + } + log.Print("GOT MXC ", mxc) + + return matrix.ImageMessage{ + MsgType: "m.image", + Body: gifResult.Slug, + URL: mxc, + Info: matrix.ImageInfo{ + Height: asInt(gifResult.Images.Original.Height), + Width: asInt(gifResult.Images.Original.Width), + Mimetype: "image/gif", + Size: asInt(gifResult.Images.Original.Size), + }, + }, nil +} + +// searchGiphy returns info about a gif +func (s *giphyService) searchGiphy(query string) (*result, error) { + u, err := url.Parse("http://api.giphy.com/v1/gifs/search") + if err != nil { + return nil, err + } + q := u.Query() + q.Set("q", query) + q.Set("api_key", s.APIKey) + u.RawQuery = q.Encode() + res, err := http.Get(u.String()) + if res != nil { + defer res.Body.Close() + } + if err != nil { + return nil, err + } + var search giphySearch + if err := json.NewDecoder(res.Body).Decode(&search); err != nil { + return nil, err + } + if len(search.Data) == 0 { + return nil, errors.New("No results") + } + return &search.Data[0], nil +} + +func asInt(strInt string) uint { + u64, err := strconv.ParseUint(strInt, 10, 32) + if err != nil { + return 0 // default to 0 since these are all just hints to the client + } + return uint(u64) +} + +func init() { + types.RegisterService(func(serviceID, serviceUserID, webhookEndpointURL string) types.Service { + return &giphyService{ + id: serviceID, + serviceUserID: serviceUserID, + } + }) +} diff --git a/src/github.com/matrix-org/go-neb/services/github/github.go b/src/github.com/matrix-org/go-neb/services/github/github.go index b038174..d0c200c 100644 --- a/src/github.com/matrix-org/go-neb/services/github/github.go +++ b/src/github.com/matrix-org/go-neb/services/github/github.go @@ -131,7 +131,7 @@ func (s *githubService) expandIssue(roomID, userID, owner, repo string, issueNum } } -func (s *githubService) Plugin(roomID string) plugin.Plugin { +func (s *githubService) Plugin(cli *matrix.Client, roomID string) plugin.Plugin { return plugin.Plugin{ Commands: []plugin.Command{ plugin.Command{ diff --git a/src/github.com/matrix-org/go-neb/services/jira/jira.go b/src/github.com/matrix-org/go-neb/services/jira/jira.go index a7440f0..d466664 100644 --- a/src/github.com/matrix-org/go-neb/services/jira/jira.go +++ b/src/github.com/matrix-org/go-neb/services/jira/jira.go @@ -198,7 +198,7 @@ func (s *jiraService) expandIssue(roomID, userID string, issueKeyGroups []string ) } -func (s *jiraService) Plugin(roomID string) plugin.Plugin { +func (s *jiraService) Plugin(cli *matrix.Client, roomID string) plugin.Plugin { return plugin.Plugin{ Commands: []plugin.Command{ plugin.Command{ diff --git a/src/github.com/matrix-org/go-neb/types/types.go b/src/github.com/matrix-org/go-neb/types/types.go index 3640543..ae076f2 100644 --- a/src/github.com/matrix-org/go-neb/types/types.go +++ b/src/github.com/matrix-org/go-neb/types/types.go @@ -44,7 +44,7 @@ type Service interface { ServiceUserID() string ServiceID() string ServiceType() string - Plugin(roomID string) plugin.Plugin + Plugin(cli *matrix.Client, roomID string) plugin.Plugin OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *matrix.Client) Register(oldService Service, client *matrix.Client) error }