mirror of
https://github.com/pushbits/server.git
synced 2025-05-29 00:36:35 +02:00
Merge branch 'master' into testing
This commit is contained in:
commit
5f5d941bc4
18 changed files with 333 additions and 41 deletions
31
.github/workflows/main.yml
vendored
31
.github/workflows/main.yml
vendored
|
@ -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
2
.gitignore
vendored
|
@ -1,4 +1,4 @@
|
|||
app
|
||||
out/
|
||||
*.db
|
||||
config.yml
|
||||
|
||||
|
|
|
@ -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 \
|
||||
|
|
10
Makefile
10
Makefile
|
@ -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:
|
||||
|
|
12
README.md
12
README.md
|
@ -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/).
|
||||
|
|
|
@ -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
2
go.mod
|
@ -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
32
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
5
internal/api/errors.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
package api
|
||||
|
||||
import "errors"
|
||||
|
||||
var ErrorMessageNotFound = errors.New("message not found")
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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, ¬ification)); !success {
|
||||
messageID, err := h.DP.SendNotification(application, ¬ification)
|
||||
|
||||
if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success {
|
||||
return
|
||||
}
|
||||
|
||||
notification.ID = messageID
|
||||
notification.UrlEncodedID = url.QueryEscape(messageID)
|
||||
|
||||
ctx.JSON(http.StatusOK, ¬ification)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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, "", "")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue