Skip to content

Commit

Permalink
feature: add matrix service (#113)
Browse files Browse the repository at this point in the history
* feat(matrix): add matrix service
* test(matrix): add some matrix tests
* fix(matrix): simplify client using stateless APIs
* fix(matrix): slim the public API and fix lint errors
* test(matrix): add test case
* test(matrix): add erroring test case
  • Loading branch information
piksel committed May 14, 2021
1 parent f72cbdc commit 7a60bc1
Show file tree
Hide file tree
Showing 8 changed files with 662 additions and 12 deletions.
38 changes: 38 additions & 0 deletions docs/services/matrix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Matrix

## URL Format

*matrix://__`user`__:__`password`__@__`host`__:__`port`__/[?rooms=__`!roomID1`__[,__`roomAlias2`__]][&disableTLS=yes]*

## Authentication

If no `user` is specified, the `password` is treated as the authentication token. This means that no matter what login
flow your server uses, if you can manually retrieve a token, then Shoutrrr can use it.

### Password Login Flow

If a `user` and `password` is supplied, the `m.login.password` login flow is attempted if the server supports it.

## Rooms

If `rooms` are *not* specified, the service will send the message to all the rooms that the user has currently joined.

Otherwise, the service will only send the message to the specified rooms. If the user is *not* in any of those rooms,
but have been invited to it, it will automatically accept that invite.

**Note**: The service will **not** join any rooms unless they are explicitly specified in `rooms`. If you need the user
to join those rooms, you can send a notification with `rooms` explicitly set once.

### Room Lookup

Rooms specified in `rooms` will be treated as room IDs if the start with a `!` and used directly to identify rooms. If
they have no such prefix (or use a *correctly escaped* `#`) they will instead be treated as aliases, and a directory
lookup will be used to resolve their corresponding IDs.

**Note**: Don't use unescaped `#` for the channel aliases as that will be treated as the `fragment` part of the URL.
Either omit them or URL encode them, I.E. `rooms=%23alias:server` or `rooms=alias:server`

### TLS

If you do not have TLS enabled on the server you can disable it by providing `disableTLS=yes`. This will effectively
use `http` intead of `https` for the API calls.
3 changes: 2 additions & 1 deletion docs/services/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ Click on the service for a more thorough explanation. <!-- @formatter:off -->
| [IFTTT](./ifttt.md) | *ifttt://__`key`__/?events=__`event1`__[,__`event2`__,...]&value1=__`value1`__&value2=__`value2`__&value3=__`value3`__* |
| [Join](./join.md) | *join://shoutrrr:__`api-key`__@join/?devices=__`device1`__[,__`device2`__, ...][&icon=__`icon`__][&title=__`title`__]* |
| [Mattermost](./mattermost.md) | *mattermost://[__`username`__@]__`mattermost-host`__/__`token`__[/__`channel`__]* |
| [Matrix](./matrix.md) | *matrix://__`username`__:__`password`__@__`host`__:__`port`__/[?rooms=__`!roomID1`__[,__`roomAlias2`__]]* |
| [OpsGenie](./opsgenie.md) | *opsgenie://__`host`__/token?responders=__`responder1`__[,__`responder2`__]* |
| [Pushbullet](./pushbullet.md) | *pushbullet://__`api-token`__[/__`device`__/#__`channel`__/__`email`__]* |
| [Pushover](./pushover.md) | *pushover://shoutrrr:__`apiToken`__@__`userKey`__/?devices=__`device1`__[,__`device2`__, ...]* |
Expand All @@ -25,4 +26,4 @@ Click on the service for a more thorough explanation. <!-- @formatter:off -->
| Service | Description |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| [Logger](./not-documented.md) | Writes notification to a configured go `log.Logger` |
| [Generic Webhook](./generic.md) | Sends notifications directly to a webhook |
| [Generic Webhook](./generic.md) | Sends notifications directly to a webhook |
24 changes: 13 additions & 11 deletions pkg/router/servicemap.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/containrrr/shoutrrr/pkg/services/ifttt"
"github.com/containrrr/shoutrrr/pkg/services/join"
"github.com/containrrr/shoutrrr/pkg/services/logger"
"github.com/containrrr/shoutrrr/pkg/services/matrix"
"github.com/containrrr/shoutrrr/pkg/services/mattermost"
"github.com/containrrr/shoutrrr/pkg/services/opsgenie"
"github.com/containrrr/shoutrrr/pkg/services/pushbullet"
Expand All @@ -24,21 +25,22 @@ import (

var serviceMap = map[string]func() t.Service{
"discord": func() t.Service { return &discord.Service{} },
"generic": func() t.Service { return &generic.Service{} },
"gotify": func() t.Service { return &gotify.Service{} },
"hangouts": func() t.Service { return &hangouts.Service{} },
"ifttt": func() t.Service { return &ifttt.Service{} },
"join": func() t.Service { return &join.Service{} },
"logger": func() t.Service { return &logger.Service{} },
"matrix": func() t.Service { return &matrix.Service{} },
"mattermost": func() t.Service { return &mattermost.Service{} },
"opsgenie": func() t.Service { return &opsgenie.Service{} },
"pushbullet": func() t.Service { return &pushbullet.Service{} },
"pushover": func() t.Service { return &pushover.Service{} },
"rocketchat": func() t.Service { return &rocketchat.Service{} },
"slack": func() t.Service { return &slack.Service{} },
"smtp": func() t.Service { return &smtp.Service{} },
"teams": func() t.Service { return &teams.Service{} },
"telegram": func() t.Service { return &telegram.Service{} },
"smtp": func() t.Service { return &smtp.Service{} },
"ifttt": func() t.Service { return &ifttt.Service{} },
"gotify": func() t.Service { return &gotify.Service{} },
"logger": func() t.Service { return &logger.Service{} },
"xmpp": func() t.Service { return &xmpp.Service{} },
"pushbullet": func() t.Service { return &pushbullet.Service{} },
"mattermost": func() t.Service { return &mattermost.Service{} },
"hangouts": func() t.Service { return &hangouts.Service{} },
"zulip": func() t.Service { return &zulip.Service{} },
"join": func() t.Service { return &join.Service{} },
"rocketchat": func() t.Service { return &rocketchat.Service{} },
"opsgenie": func() t.Service { return &opsgenie.Service{} },
"generic": func() t.Service { return &generic.Service{} },
}
59 changes: 59 additions & 0 deletions pkg/services/matrix/matrix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package matrix

import (
"fmt"
"github.com/containrrr/shoutrrr/pkg/format"
"github.com/containrrr/shoutrrr/pkg/services/standard"
t "github.com/containrrr/shoutrrr/pkg/types"
"log"
"net/url"
)

// Scheme is the identifying part of this service's configuration URL
const Scheme = "matrix"

// Service providing Matrix as a notification service
type Service struct {
standard.Standard
config *Config
client *client
pkr format.PropKeyResolver
}

// Initialize loads ServiceConfig from configURL and sets logger for this Service
func (s *Service) Initialize(configURL *url.URL, logger *log.Logger) error {
s.SetLogger(logger)
s.config = &Config{}

s.pkr = format.NewPropKeyResolver(s.config)
if err := s.config.setURL(&s.pkr, configURL); err != nil {
return err
}

s.client = newClient(s.config.Host, s.config.DisableTLS, logger)
if s.config.User != "" {
return s.client.login(s.config.User, s.config.Password)
}

s.client.useToken(s.config.Password)
return nil
}

// Send notification
func (s *Service) Send(message string, params *t.Params) error {
config := *s.config
if err := s.pkr.UpdateConfigFromParams(&config, params); err != nil {
return err
}

errors := s.client.sendMessage(message, s.config.Rooms)

if len(errors) > 0 {
for _, err := range errors {
s.Logf("error sending message: %v", err)
}
return fmt.Errorf("%v error(s) sending message, with initial error: %v", len(errors), errors[0])
}

return nil
}
80 changes: 80 additions & 0 deletions pkg/services/matrix/matrix_api.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package matrix

type messageType string
type flowType string
type identifierType string

const (
apiLogin = "/_matrix/client/r0/login"
apiRoomJoin = "/_matrix/client/r0/join/%s"
apiSendMessage = "/_matrix/client/r0/rooms/%s/send/m.room.message"
apiJoinedRooms = "/_matrix/client/r0/joined_rooms"

contentType = "application/json"

accessTokenKey = "access_token"

msgTypeText messageType = "m.text"
flowLoginPassword flowType = "m.login.password"
idTypeUser identifierType = "m.id.user"
)

type apiResLoginFlows struct {
Flows []flow `json:"flows"`
}

type apiReqLogin struct {
Type flowType `json:"type"`
Identifier *identifier `json:"identifier"`
Password string `json:"password,omitempty"`
Token string `json:"token,omitempty"`
}

type apiResLogin struct {
AccessToken string `json:"access_token"`
HomeServer string `json:"home_server"`
UserID string `json:"user_id"`
DeviceID string `json:"device_id"`
}

type apiReqSend struct {
MsgType messageType `json:"msgtype"`
Body string `json:"body"`
}

type apiResRoom struct {
RoomID string `json:"room_id"`
}

type apiResJoinedRooms struct {
Rooms []string `json:"joined_rooms"`
}

type apiResEvent struct {
EventID string `json:"event_id"`
}

type apiResError struct {
Message string `json:"error"`
Code string `json:"errcode"`
}

func (e *apiResError) Error() string {
return e.Message
}

type flow struct {
Type flowType `json:"type"`
}

type identifier struct {
Type identifierType `json:"type"`
User string `json:"user,omitempty"`
}

func newUserIdentifier(user string) (id *identifier) {
return &identifier{
Type: idTypeUser,
User: user,
}
}
Loading

0 comments on commit 7a60bc1

Please sign in to comment.