Merge branch 'master' into testing

This commit is contained in:
Cubicroot 2021-06-20 17:24:14 +02:00 committed by GitHub
commit 5f5d941bc4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 333 additions and 41 deletions

View file

@ -1,25 +1,34 @@
name: Main
on: push
on: [push, pull_request]
jobs:
test_publish:
name: Test and publish
test_build_publish:
name: Test, build, and publish
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Checkout code
uses: actions/checkout@v2
- name: Install dependencies
run: make setup
- name: Export GOBIN
uses: actions/setup-go@v2
with:
go-version: 1.16
- name: Run tests
run: |
export PATH="$PATH:$(go env GOPATH)/bin" # Currently the path needs to be set manually.
make test
run: make test
- name: Build image
run: make build_image
- name: Get Branch # Needed to evaluate env.BRANCH.
if: ${{ startsWith(github.ref, 'refs/tags/v') && github.event_name == 'push' }} # Otherwise will fail on pull requests.
run: |
raw=$(git branch -r --contains ${{ github.ref }})
branch=${raw##*/}
echo "BRANCH=$branch" >> $GITHUB_ENV
- name: Login to Docker Hub
if: ${{ startsWith(github.ref, 'refs/tags/v') && github.event_name == 'push' && env.BRANCH == 'master' }} # Only login for tagged commits pushed to master.
uses: docker/login-action@v1
with:
with: # Secrets are not exposed to pull request contexts.
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Publish image
if: startsWith(github.ref, 'refs/tags/') # Only publish for tagged commits.
run: |
make push_image
if: ${{ startsWith(github.ref, 'refs/tags/v') && github.event_name == 'push' && env.BRANCH == 'master' }} # Only publish for tagged commits pushed to master.
run: make push_image

2
.gitignore vendored
View file

@ -1,4 +1,4 @@
app
out/
*.db
config.yml

View file

@ -9,7 +9,7 @@ RUN set -ex \
&& go mod download \
&& go mod verify \
&& make build \
&& chmod +x /build/app
&& chmod +x /build/out/pushbits
FROM alpine
@ -22,7 +22,7 @@ EXPOSE 8080
WORKDIR /app
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /build/app ./run
COPY --from=builder /build/out/pushbits ./run
RUN set -ex \
&& apk add --no-cache ca-certificates curl \

View file

@ -2,7 +2,8 @@ IMAGE := eikendev/pushbits
.PHONY: build
build:
go build -ldflags="-w -s" -o app ./cmd/pushbits
mkdir -p ./out
go build -ldflags="-w -s" -o ./out/pushbits ./cmd/pushbits
.PHONY: test
test:
@ -12,16 +13,13 @@ test:
fi
go vet ./...
gocyclo -over 10 $(shell find . -iname '*.go' -type f)
staticcheck ./...
go test -v -cover ./...
stdout=$$(golint ./... 2>&1); \
if [ "$$stdout" ]; then \
exit 1; \
fi
.PHONY: setup
setup:
go get -u github.com/fzipp/gocyclo/cmd/gocyclo
go get -u golang.org/x/lint/golint
go get -u honnef.co/go/tools/cmd/staticcheck
.PHONY: build_image
build_image:

View file

@ -142,6 +142,18 @@ curl \
HTML-Content might not be fully rendered in your Matrix-Client - see the corresponding [Matrix specs](https://spec.matrix.org/unstable/client-server-api/#mroommessage-msgtypes). This also holds for Markdown, as it is transfered to the corresponding HTML-syntax.
### Deleting a Message
You can delete a message, this will send a notification in response to the original message informing you that the message is "deleted".
You need the message ID for deleting a message. As it might contain characters not valid in uris we provide an additional `id_url_encoded` field for messages, use that value for deleting a message.
```bash
curl \
--request DELETE \
"https://pushbits.example.com/message/${MESSAGE_ID}?token=$PB_TOKEN"
```
## 👮 Acknowledgments
The idea for this software and most parts of the initial source are heavily inspired by [Gotify](https://gotify.net/).

View file

@ -47,7 +47,7 @@ func main() {
log.Fatal(err)
}
dp, err := dispatcher.Create(db, c.Matrix.Homeserver, c.Matrix.Username, c.Matrix.Password, c.Formatting)
dp, err := dispatcher.Create(c.Matrix.Homeserver, c.Matrix.Username, c.Matrix.Password, c.Formatting)
if err != nil {
log.Fatal(err)
}

2
go.mod
View file

@ -4,6 +4,7 @@ go 1.14
require (
github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b
github.com/fzipp/gocyclo v0.3.1 // indirect
github.com/gin-contrib/location v0.0.2
github.com/gin-gonic/gin v1.6.3
github.com/go-playground/validator/v10 v10.3.0 // indirect
@ -25,4 +26,5 @@ require (
gorm.io/driver/mysql v1.0.4
gorm.io/driver/sqlite v1.1.4
gorm.io/gorm v1.20.12
honnef.co/go/tools v0.2.0 // indirect
)

32
go.sum
View file

@ -5,6 +5,8 @@ github.com/alexedwards/argon2id v0.0.0-20201228115903-cf543ebc1f7b/go.mod h1:Kmn
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fzipp/gocyclo v0.3.1 h1:A9UeX3HJSXTBzvHzhqoYVuE0eAhe+aM8XBCCwsPMZOc=
github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E=
github.com/gin-contrib/location v0.0.2 h1:QZKh1+K/LLR4KG/61eIO3b7MLuKi8tytQhV6texLgP4=
github.com/gin-contrib/location v0.0.2/go.mod h1:NGoidiRlf0BlA/VKSVp+g3cuSMeTmip/63PhEjRhUAc=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
@ -77,20 +79,36 @@ github.com/ugorji/go v1.2.4/go.mod h1:EuaSCk8iZMdIspsu6HXH7X2UGKw1ezO4wCfGszGmmo
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.4 h1:C5VurWRRCKjuENsbM6GYVw8W++WVW9rSxoACKIvxzz8=
github.com/ugorji/go/codec v1.2.4/go.mod h1:bWBu1+kIRWcF8uMklKaJrR6fTWQOwAlrIzX22pHwryA=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/dl v0.0.0-20190829154251-82a15e2f2ead/go.mod h1:IUMfjQLJQd4UTqG1Z90tenwKoCX93Gn3MAQJMOSBsDQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210608053332-aa57babbf139 h1:C+AwYEtBp/VQwoLntUmQ/yx3MS9vmZaKNdw5eOpoQe8=
golang.org/x/sys v0.0.0-20210608053332-aa57babbf139/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c h1:VwygUrnw9jn88c4u8GD3rZQbqrP/tgas88tPUbBxQrk=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -116,3 +134,5 @@ gorm.io/driver/sqlite v1.1.4/go.mod h1:mJCeTFr7+crvS+TRnWc5Z3UvwxUN1BGBLMrf5LA9D
gorm.io/gorm v1.20.7/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
gorm.io/gorm v1.20.12 h1:ebZ5KrSHzet+sqOCVdH9mTjW91L298nX3v5lVxAzSUY=
gorm.io/gorm v1.20.12/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw=
honnef.co/go/tools v0.2.0 h1:ws8AfbgTX3oIczLPNPCu5166oBg9ST2vNs0rcht+mDE=
honnef.co/go/tools v0.2.0/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=

View file

@ -20,6 +20,17 @@ func getID(ctx *gin.Context) (uint, error) {
return id, nil
}
func getMessageID(ctx *gin.Context) (string, error) {
id, ok := ctx.MustGet("messageid").(string)
if !ok {
err := errors.New("an error occured while retrieving messageID from context")
ctx.AbortWithError(http.StatusInternalServerError, err)
return "", err
}
return id, nil
}
func getApplication(ctx *gin.Context, db Database) (*model.Application, error) {
id, err := getID(ctx)
if err != nil {

5
internal/api/errors.go Normal file
View file

@ -0,0 +1,5 @@
package api
import "errors"
var ErrorMessageNotFound = errors.New("message not found")

View file

@ -8,6 +8,10 @@ type idInURI struct {
ID uint `uri:"id" binding:"required"`
}
type messageIdInURI struct {
MessageID string `uri:"messageid" binding:"required"`
}
// RequireIDInURI returns a Gin middleware which requires an ID to be supplied in the URI of the request.
func RequireIDInURI() gin.HandlerFunc {
return func(ctx *gin.Context) {
@ -20,3 +24,16 @@ func RequireIDInURI() gin.HandlerFunc {
ctx.Set("id", requestModel.ID)
}
}
// RequireMessageIDInURI returns a Gin middleware which requires an messageID to be supplied in the URI of the request.
func RequireMessageIDInURI() gin.HandlerFunc {
return func(ctx *gin.Context) {
var requestModel messageIdInURI
if err := ctx.BindUri(&requestModel); err != nil {
return
}
ctx.Set("messageid", requestModel.MessageID)
}
}

View file

@ -3,6 +3,7 @@ package api
import (
"log"
"net/http"
"net/url"
"strings"
"time"
@ -18,7 +19,8 @@ type NotificationDatabase interface {
// The NotificationDispatcher interface for relaying notifications.
type NotificationDispatcher interface {
SendNotification(a *model.Application, n *model.Notification) error
SendNotification(a *model.Application, n *model.Notification) (id string, err error)
DeleteNotification(a *model.Application, n *model.DeleteNotification) error
}
// NotificationHandler holds information for processing requests about notifications.
@ -38,16 +40,41 @@ func (h *NotificationHandler) CreateNotification(ctx *gin.Context) {
application := authentication.GetApplication(ctx)
log.Printf("Sending notification for application %s.", application.Name)
notification.ID = 0
notification.ApplicationID = application.ID
if strings.TrimSpace(notification.Title) == "" {
notification.Title = application.Name
}
notification.Date = time.Now()
if success := successOrAbort(ctx, http.StatusInternalServerError, h.DP.SendNotification(application, &notification)); !success {
messageID, err := h.DP.SendNotification(application, &notification)
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
return
}
notification.ID = messageID
notification.UrlEncodedID = url.QueryEscape(messageID)
ctx.JSON(http.StatusOK, &notification)
}
// DeleteNotification is used to delete (or mark as deleted) a notification for a user
func (h *NotificationHandler) DeleteNotification(ctx *gin.Context) {
application := authentication.GetApplication(ctx)
id, err := getMessageID(ctx)
if success := successOrAbort(ctx, http.StatusUnprocessableEntity, err); !success {
return
}
n := model.DeleteNotification{
ID: id,
Date: time.Now(),
}
if success := successOrAbort(ctx, http.StatusInternalServerError, h.DP.DeleteNotification(application, &n)); !success {
return
}
ctx.Status(http.StatusOK)
}

View file

@ -11,7 +11,13 @@ import (
func successOrAbort(ctx *gin.Context, code int, err error) bool {
if err != nil {
ctx.AbortWithError(code, err)
// If we know the error force error code
switch err {
case ErrorMessageNotFound:
ctx.AbortWithError(http.StatusNotFound, err)
default:
ctx.AbortWithError(code, err)
}
}
return err == nil

View file

@ -14,7 +14,7 @@ func (m *Manager) CreatePasswordHash(password string) ([]byte, error) {
if err != nil {
return []byte{}, errors.New("HIBP is not available, please wait until service is available again")
} else if pwned {
return []byte{}, errors.New("Password is pwned, please choose another one")
return []byte{}, errors.New("password is pwned, please choose another one")
}
}

View file

@ -11,19 +11,14 @@ var (
loginType = "m.login.password"
)
// The Database interface for encapsulating database access.
type Database interface {
}
// Dispatcher holds information for sending notifications to clients.
type Dispatcher struct {
db Database
client *gomatrix.Client
formatting configuration.Formatting
}
// Create instanciates a dispatcher connection.
func Create(db Database, homeserver, username, password string, formatting configuration.Formatting) (*Dispatcher, error) {
func Create(homeserver, username, password string, formatting configuration.Formatting) (*Dispatcher, error) {
log.Println("Setting up dispatcher.")
client, err := gomatrix.NewClient(homeserver, "", "")

View file

@ -7,11 +7,50 @@ import (
"strings"
"github.com/gomarkdown/markdown"
"github.com/matrix-org/gomatrix"
"github.com/pushbits/server/internal/api"
"github.com/pushbits/server/internal/model"
)
// MessageFormat is a matrix message format
type MessageFormat string
// MsgType is a matrix msgtype
type MsgType string
// Define matrix constants
const (
MessageFormatHTML = MessageFormat("org.matrix.custom.html")
MsgTypeText = MsgType("m.text")
)
// MessageEvent is the content of a matrix message event
type MessageEvent struct {
Body string `json:"body"`
FormattedBody string `json:"formatted_body"`
MsgType MsgType `json:"msgtype"`
RelatesTo RelatesTo `json:"m.relates_to,omitempty"`
Format MessageFormat `json:"format"`
NewContent NewContent `json:"m.new_content,omitempty"`
}
// RelatesTo holds information about relations to other message events
type RelatesTo struct {
InReplyTo map[string]string `json:"m.in_reply_to,omitempty"`
RelType string `json:"rel_type,omitempty"`
EventID string `json:"event_id,omitempty"`
}
// NewContent holds information about an updated message event
type NewContent struct {
Body string `json:"body"`
FormattedBody string `json:"formatted_body"`
MsgType MsgType `json:"msgtype"`
Format MessageFormat `json:"format"`
}
// SendNotification sends a notification to the specified user.
func (d *Dispatcher) SendNotification(a *model.Application, n *model.Notification) error {
func (d *Dispatcher) SendNotification(a *model.Application, n *model.Notification) (id string, err error) {
log.Printf("Sending notification to room %s.", a.MatrixID)
plainMessage := strings.TrimSpace(n.Message)
@ -22,7 +61,42 @@ func (d *Dispatcher) SendNotification(a *model.Application, n *model.Notificatio
text := fmt.Sprintf("%s\n\n%s", plainTitle, plainMessage)
formattedText := fmt.Sprintf("%s %s", title, message)
_, err := d.client.SendFormattedText(a.MatrixID, text, formattedText)
respSendEvent, err := d.client.SendFormattedText(a.MatrixID, text, formattedText)
return respSendEvent.EventID, err
}
// DeleteNotification sends a notification to the specified user that another notificaion is deleted
func (d *Dispatcher) DeleteNotification(a *model.Application, n *model.DeleteNotification) error {
log.Printf("Sending delete notification to room %s", a.MatrixID)
var oldFormattedBody string
var oldBody string
// get the message we want to delete
deleteMessage, err := d.getMessage(a, n.ID)
if err != nil {
log.Println(err)
return api.ErrorMessageNotFound
}
oldBody, oldFormattedBody, err = bodiesFromMessage(deleteMessage)
if err != nil {
return err
}
// update the message with strikethrough
newBody := fmt.Sprintf("<del>%s</del>\n- deleted", oldBody)
newFormattedBody := fmt.Sprintf("<del>%s</del><br>- deleted", oldFormattedBody)
_, err = d.replaceMessage(a, newBody, newFormattedBody, deleteMessage.ID, oldBody, oldFormattedBody)
if err != nil {
return err
}
_, err = d.respondToMessage(a, "This message got deleted", "<i>This message got deleted.</i>", deleteMessage)
return err
}
@ -92,3 +166,111 @@ func (d *Dispatcher) coloredText(color string, text string) string {
return "<font data-mx-color='" + color + "'>" + text + "</font>"
}
// Searches in the messages list for the given id
func (d *Dispatcher) getMessage(a *model.Application, id string) (gomatrix.Event, error) {
start := ""
end := ""
maxPages := 10 // maximum pages to request (10 messages per page)
for i := 0; i < maxPages; i++ {
messages, _ := d.client.Messages(a.MatrixID, start, end, 'b', 10)
for _, event := range messages.Chunk {
if event.ID == id {
return event, nil
}
}
start = messages.End
}
return gomatrix.Event{}, api.ErrorMessageNotFound
}
// Replaces the content of a matrix message
func (d *Dispatcher) replaceMessage(a *model.Application, newBody, newFormattedBody string, messageID string, oldBody, oldFormattedBody string) (*gomatrix.RespSendEvent, error) {
newMessage := NewContent{
Body: newBody,
FormattedBody: newFormattedBody,
MsgType: MsgTypeText,
Format: MessageFormatHTML,
}
replaceRelation := RelatesTo{
RelType: "m.replace",
EventID: messageID,
}
replaceEvent := MessageEvent{
Body: oldBody,
FormattedBody: oldFormattedBody,
MsgType: MsgTypeText,
NewContent: newMessage,
RelatesTo: replaceRelation,
Format: MessageFormatHTML,
}
sendEvent, err := d.client.SendMessageEvent(a.MatrixID, "m.room.message", replaceEvent)
if err != nil {
log.Println(err)
return nil, err
}
return sendEvent, nil
}
// Sends a notification in response to another matrix message event
func (d *Dispatcher) respondToMessage(a *model.Application, body, formattedBody string, respondMessage gomatrix.Event) (*gomatrix.RespSendEvent, error) {
oldBody, oldFormattedBody, err := bodiesFromMessage(respondMessage)
if err != nil {
return nil, err
}
// formatting according to https://matrix.org/docs/spec/client_server/latest#fallbacks-and-event-representation
newFormattedBody := fmt.Sprintf("<mx-reply><blockquote><a href='https://matrix.to/#/%s/%s'>In reply to</a> <a href='https://matrix.to/#/%s'>%s</a><br />%s</blockquote>\n</mx-reply>%s", respondMessage.RoomID, respondMessage.ID, respondMessage.Sender, respondMessage.Sender, oldFormattedBody, formattedBody)
newBody := fmt.Sprintf("> <%s>%s\n\n%s", respondMessage.Sender, oldBody, body)
notificationEvent := MessageEvent{
FormattedBody: newFormattedBody,
Body: newBody,
MsgType: MsgTypeText,
Format: MessageFormatHTML,
}
notificationReply := make(map[string]string)
notificationReply["event_id"] = respondMessage.ID
notificationRelation := RelatesTo{
InReplyTo: notificationReply,
}
notificationEvent.RelatesTo = notificationRelation
return d.client.SendMessageEvent(a.MatrixID, "m.room.message", notificationEvent)
}
// Extracts body and formatted body from a matrix message event
func bodiesFromMessage(message gomatrix.Event) (body, formattedBody string, err error) {
if val, ok := message.Content["body"]; ok {
body, ok := val.(string)
if !ok {
return "", "", api.ErrorMessageNotFound
}
formattedBody = body
} else {
return "", "", api.ErrorMessageNotFound
}
if val, ok := message.Content["formatted_body"]; ok {
body, ok := val.(string)
if !ok {
return "", "", api.ErrorMessageNotFound
}
formattedBody = body
}
return body, formattedBody, nil
}

View file

@ -6,7 +6,8 @@ import (
// Notification holds information like the message, the title, and the priority of a notification.
type Notification struct {
ID uint `json:"id"`
ID string `json:"id"`
UrlEncodedID string `json:"id_url_encoded"`
ApplicationID uint `json:"appid"`
Message string `json:"message" form:"message" query:"message" binding:"required"`
Title string `json:"title" form:"title" query:"title"`
@ -14,3 +15,9 @@ type Notification struct {
Extras map[string]interface{} `json:"extras,omitempty" form:"-" query:"-"`
Date time.Time `json:"date"`
}
// DeleteNotification holds information like the message ID of a deletion notification.
type DeleteNotification struct {
ID string `json:"id" form:"id"`
Date time.Time `json:"date"`
}

View file

@ -46,6 +46,7 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp
r.GET("/health", healthHandler.Health)
r.POST("/message", auth.RequireApplicationToken(), notificationHandler.CreateNotification)
r.DELETE("/message/:messageid", api.RequireMessageIDInURI(), auth.RequireApplicationToken(), notificationHandler.DeleteNotification)
userGroup := r.Group("/user")
userGroup.Use(auth.RequireAdmin())