diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..62919a9cc --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +./docker/tinode/releases/* +*.gz \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..34059889c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "server/static"] + path = server/static + url = https://github.com/tinode/webapp diff --git a/build-custom.sh b/build-custom.sh new file mode 100755 index 000000000..7d244ba41 --- /dev/null +++ b/build-custom.sh @@ -0,0 +1,120 @@ +#!/usr/bin/env bash + +set -ex + +### builds custom image for makeomatic purposes +# usage "./build-custom.sh tag=v0.15.9" +goplat=( linux ) +goarc=( amd64 ) +dbtags=( rethinkdb ) +releasepath="./docker/tinode/releases" +repository="makeomatic" + +export GOPATH=`go env GOPATH` + +for line in $@; do + eval "$line" +done + +version=${tag#?} + +if [ -z "$version" ]; then + echo "Must provide tag as 'tag=v1.2.3'" + exit 1 +fi + +echo "Releasing $version" + +GOSRC=${GOPATH}/src/github.com/tinode +git submodule update --init --recursive + +# Prepare directory for the new release +rm -fR ${releasepath}/${version} +mkdir -p ${releasepath}/${version} + +if [[ ! -x "$GOPATH/bin/gox" ]]; then + go get github.com/mitchellh/gox +fi + +for plat in "${goplat[@]}" +do + for arc in "${goarc[@]}" + do + # Keygen is database-independent + # Remove previous build + rm -f $GOPATH/bin/keygen + # Build + $GOPATH/bin/gox -osarch="${plat}/${arc}" -ldflags "-s -w" -output $GOPATH/bin/keygen ./keygen > /dev/null + + for dbtag in "${dbtags[@]}" + do + echo "Building ${dbtag}-${plat}/${arc}..." + tmppath=`mktemp -d` + + # Remove previous builds + rm -f $GOPATH/bin/tinode + rm -f $GOPATH/bin/init-db + # Build tinode server and database initializer for RethinkDb and MySQL. + $GOPATH/bin/gox -osarch="${plat}/${arc}" \ + -ldflags "-s -w -X main.buildstamp=`git describe --tags`" \ + -tags ${dbtag} -output $GOPATH/bin/tinode ./server > /dev/null + $GOPATH/bin/gox -osarch="${plat}/${arc}" \ + -ldflags "-s -w" \ + -tags ${dbtag} -output $GOPATH/bin/init-db ./tinode-db > /dev/null + + # Tar on Mac is inflexible about directories. Let's just copy release files to + # one directory. + mkdir -p ${tmppath}/static/img + mkdir ${tmppath}/static/css + mkdir ${tmppath}/static/audio + mkdir ${tmppath}/static/src + mkdir ${tmppath}/static/umd + mkdir ${tmppath}/templ + + # Copy templates and database initialization files + cp ./server/tinode.conf ${tmppath} + cp ./server/templ/*.templ ${tmppath}/templ + cp ./server/static/img/*.png ${tmppath}/static/img + cp ./server/static/img/*.svg ${tmppath}/static/img + cp ./server/static/audio/*.mp3 ${tmppath}/static/audio + cp ./server/static/css/*.css ${tmppath}/static/css + cp ./server/static/index.html ${tmppath}/static + cp ./server/static/index-dev.html ${tmppath}/static + cp ./server/static/umd/*.js ${tmppath}/static/umd + cp ./server/static/manifest.json ${tmppath}/static + cp ./server/static/service-worker.js ${tmppath}/static + # Create empty FCM client-side config. + echo > ${tmppath}/static/firebase-init.js + cp ./tinode-db/data.json ${tmppath} + cp ./tinode-db/*.jpg ${tmppath} + cp ./tinode-db/credentials.sh ${tmppath} + + # Build archive. All platforms but Windows use tar for archiving. Windows uses zip. + plat2=$plat + # Rename 'darwin' tp 'mac' + if [ "$plat" = "darwin" ]; then + plat2=mac + fi + # Copy binaries + cp $GOPATH/bin/tinode ${tmppath} + cp $GOPATH/bin/init-db ${tmppath} + cp $GOPATH/bin/keygen ${tmppath} + + # Remove possibly existing archive. + rm -f ${releasepath}/${version}/tinode-${dbtag}."${plat2}-${arc}".tar.gz + # Generate a new one + tar -C ${tmppath} -zcf ${releasepath}/${version}/tinode-${dbtag}."${plat2}-${arc}".tar.gz . + + rm -fR ${tmppath} + done + done +done + +for dbtag in "${dbtags[@]}" +do + rmitags="${repository}/tinode-${dbtag}:${version}" + + docker rmi ${rmitags} -f + docker build --build-arg VERSION=$version --build-arg TARGET_DB=${dbtag} --tag ${rmitags} docker/tinode + docker push $rmitags +done \ No newline at end of file diff --git a/docker/tinode/Dockerfile b/docker/tinode/Dockerfile index ea54699ff..76a89dcff 100644 --- a/docker/tinode/Dockerfile +++ b/docker/tinode/Dockerfile @@ -86,11 +86,7 @@ RUN apk update && \ WORKDIR /opt/tinode # Get the desired Tinode build. -ADD https://github.com/tinode/chat/releases/download/v$VERSION/tinode-$TARGET_DB.linux-amd64.tar.gz . - -# Unpack the Tinode archive. -RUN tar -xzf tinode-$TARGET_DB.linux-amd64.tar.gz \ - && rm tinode-$TARGET_DB.linux-amd64.tar.gz +ADD ./releases/$VERSION/tinode-$TARGET_DB.linux-amd64.tar.gz . # Copy config template to the container. COPY config.template . diff --git a/docs/API.md b/docs/API.md index 985722d68..12f3b36a0 100644 --- a/docs/API.md +++ b/docs/API.md @@ -277,7 +277,7 @@ User-dependent topic properties: * given: access permissions given to this user * private: an application-defined object that is unique to the current user. -Topic usually have subscribers. One the the subscribers may be designated as topic owner (`O` access permission) with full access permissions. The list of subscribers can be queries with a `{get what="sub"}` message. The list of subscribers is returned in a `sub` section of a `{meta}` message. +Topic usually have subscribers. One of the subscribers may be designated as topic owner (`O` access permission) with full access permissions. The list of subscribers can be queries with a `{get what="sub"}` message. The list of subscribers is returned in a `sub` section of a `{meta}` message. ### `me` Topic diff --git a/docs/monitoring.md b/docs/monitoring.md index 7a079f927..f0d83ddef 100644 --- a/docs/monitoring.md +++ b/docs/monitoring.md @@ -11,4 +11,4 @@ As of the time of this writing the following stats are published: * `TotalSessions`: the count of all sessions which were created during server's life time. * `LiveSessions`: the number of sessions currently live, regardless of authentication status. * `TotalTopics`: the count of all topics activated during servers's life time. -* `LiveTpics`: the number of currently active topics. +* `LiveTopics`: the number of currently active topics. diff --git a/server/hdl_grpc.go b/server/hdl_grpc.go index bcdd6e186..063a4546a 100644 --- a/server/hdl_grpc.go +++ b/server/hdl_grpc.go @@ -19,6 +19,8 @@ import ( "github.com/tinode/chat/pbx" "google.golang.org/grpc" "google.golang.org/grpc/credentials" + "google.golang.org/grpc/reflection" + "google.golang.org/grpc/channelz/service" "google.golang.org/grpc/keepalive" ) @@ -143,6 +145,8 @@ func serveGrpc(addr string, kaEnabled bool, tlsConf *tls.Config) (*grpc.Server, } srv := grpc.NewServer(opts...) + reflection.Register(srv) + service.RegisterChannelzServiceToServer(srv) pbx.RegisterNodeServer(srv, &grpcNodeServer{}) log.Printf("gRPC/%s%s server is registered at [%s]", grpc.Version, secure, addr) diff --git a/server/main.go b/server/main.go index b0020033b..1cb70a6c6 100644 --- a/server/main.go +++ b/server/main.go @@ -42,6 +42,7 @@ import ( // Push notifications "github.com/tinode/chat/server/push" _ "github.com/tinode/chat/server/push/fcm" + _ "github.com/tinode/chat/server/push/http" _ "github.com/tinode/chat/server/push/stdout" "github.com/tinode/chat/server/store" diff --git a/server/push/http/push_http.go b/server/push/http/push_http.go new file mode 100644 index 000000000..9eb05f4e4 --- /dev/null +++ b/server/push/http/push_http.go @@ -0,0 +1,156 @@ +package http + +import ( + "bytes" + "encoding/json" + "errors" + "github.com/tinode/chat/server/drafty" + "github.com/tinode/chat/server/store" + "strconv" + "time" + + t "github.com/tinode/chat/server/store/types" + "log" + "net/http" + + "github.com/tinode/chat/server/push" +) + +var handler httpPush + +// How much to buffer the input channel. +const defaultBuffer = 32 + +type httpPush struct { + initialized bool + input chan *push.Receipt + stop chan bool +} + +type configType struct { + Enabled bool `json:"enabled"` + Buffer int `json:"buffer"` + Url string `json:"url"` +} + +// Init initializes the handler +func (httpPush) Init(jsonconf string) error { + log.Printf("Init HTTP push") + + // Check if the handler is already initialized + if handler.initialized { + return errors.New("already initialized") + } + + var config configType + if err := json.Unmarshal([]byte(jsonconf), &config); err != nil { + return errors.New("failed to parse config: " + err.Error()) + } + + handler.initialized = true + + if !config.Enabled { + return nil + } + + if config.Buffer <= 0 { + config.Buffer = defaultBuffer + } + + handler.input = make(chan *push.Receipt, config.Buffer) + handler.stop = make(chan bool, 1) + + go func() { + for { + select { + case msg := <-handler.input: + go sendPushToHttp(msg, config.Url) + case <-handler.stop: + return + } + } + }() + + log.Printf("Initialized HTTP push") + return nil +} + +func messagePayload(payload *push.Payload) map[string]string { + data := make(map[string]string) + data["topic"] = t.ParseUserId(payload.Topic).String() + data["from"] = t.ParseUserId(payload.From).String() + data["ts"] = payload.Timestamp.Format(time.RFC3339) + data["seq"] = strconv.Itoa(payload.SeqId) + data["mime"] = payload.ContentType + data["content"], _ = drafty.ToPlainText(payload.Content) + + return data +} + +func sendPushToHttp(msg *push.Receipt, url string) { + log.Print("Prepare to sent HTTP push from: ", msg.Payload.From) + + recipientsIds := make([]t.Uid, len(msg.To)) + for recipientId := range msg.To { + recipientsIds = append(recipientsIds, recipientId) + } + + /* + * Sender user data + */ + sender, _ := store.Users.Get(t.ParseUserId(msg.Payload.From)) + + /* + * Recipients list with user data, and conversation status + */ + recipientsList, _ := store.Users.GetAll(recipientsIds...) + recipients := map[string]map[string]interface{}{} + for _, r := range recipientsList { + user := map[string]interface{}{ + "user": r, + } + recipients[r.Id] = user + } + for uid, to := range msg.To { + recipients[uid.String()]["device"] = to + } + + /* + * Generate payload + */ + data := make(map[string]interface{}) + data["recipients"] = recipients + data["sender"] = sender + data["payload"] = messagePayload(&msg.Payload) + data["head"] = msg.Payload.Head + requestData, _ := json.Marshal(data) + + /* + * Send push through http + */ + log.Print("Sent HTTP push from: ", sender.Id, "to: ", recipientsIds) + _, err := http.Post(url, "application/json", bytes.NewBuffer(requestData)) + if err != nil { + log.Fatal("Http send push failed: ", err) + } +} + +// IsReady checks if the handler is initialized. +func (httpPush) IsReady() bool { + return handler.input != nil +} + +// Push returns a channel that the server will use to send messages to. +// If the adapter blocks, the message will be dropped. +func (httpPush) Push() chan<- *push.Receipt { + return handler.input +} + +// Stop terminates the handler's worker and stops sending pushes. +func (httpPush) Stop() { + handler.stop <- true +} + +func init() { + push.Register("http", &handler) +} diff --git a/server/push/push.go b/server/push/push.go index 222f01a31..4aefb82d5 100644 --- a/server/push/push.go +++ b/server/push/push.go @@ -41,6 +41,8 @@ type Payload struct { ContentType string `json:"mime"` // Actual Data.Content of the message, if requested Content interface{} `json:"content,omitempty"` + // Message head with custom parameters + Head map[string]interface{} `json:"head,omitempty"` } // Handler is an interface which must be implemented by handlers. diff --git a/server/static b/server/static new file mode 160000 index 000000000..44c5e30f4 --- /dev/null +++ b/server/static @@ -0,0 +1 @@ +Subproject commit 44c5e30f4d170bbed1f309398abf56f9e3c864f0 diff --git a/server/tinode.conf b/server/tinode.conf index 799f84b12..bc928bc0b 100644 --- a/server/tinode.conf +++ b/server/tinode.conf @@ -291,6 +291,15 @@ "enabled": false } }, + { + // Send push through http. Useful for handling push notifications by your service. + "name":"http", + "config": { + // Disabled. + "enabled": false, + "url": "http://localhost/tinode" + } + }, { // Google FCM notificator. "name":"fcm", diff --git a/server/topic.go b/server/topic.go index c8eded31b..38e5d0d1f 100644 --- a/server/topic.go +++ b/server/topic.go @@ -2556,7 +2556,9 @@ func (t *Topic) makePushReceipt(fromUid types.Uid, data *MsgServerData) *push.Re From: data.From, Timestamp: data.Timestamp, SeqId: data.SeqId, - Content: data.Content}} + Content: data.Content, + Head: data.Head, + }} for uid := range t.perUser { // Send only to those who have notifications enabled, exclude the originating user.