├── .coveralls.yml ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── OWNERS ├── README.md ├── main.go ├── pkg ├── api │ ├── chat │ │ ├── chat.go │ │ ├── chat.pb.go │ │ ├── chat.proto │ │ ├── message.go │ │ └── router.go │ ├── client │ │ ├── client.go │ │ ├── client.pb.go │ │ ├── client.proto │ │ └── router.go │ ├── operator │ │ ├── operator.go │ │ ├── operator.pb.go │ │ ├── operator.proto │ │ └── router.go │ ├── person │ │ └── person.go │ └── webhook │ │ ├── event.go │ │ ├── router.go │ │ ├── webhook.go │ │ ├── webhook.pb.go │ │ └── webhook.proto ├── server │ ├── rest │ │ └── server.go │ └── socket │ │ ├── client.go │ │ └── server.go └── store │ └── store.go └── requirements.txt /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-pro 2 | repo_token: 5eZq1fZRGh5AALRVtUTYaZ7VVzffhSNj1 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 10 | 11 | - `go` version: 12 | 13 | Relevant code or config 14 | 15 | ```go 16 | 17 | ``` 18 | 19 | What you did: 20 | 21 | 22 | 23 | What happened: 24 | 25 | 26 | 27 | Reproduction repository: 28 | 29 | 33 | 34 | Problem description: 35 | 36 | 37 | 38 | Suggested solution: 39 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 16 | 17 | 18 | ## Description 19 | 20 | 21 | ### Motivation 22 | 23 | 24 | ### Changes 25 | 26 | - 27 | - 28 | 29 | 30 | 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | dist 10 | 11 | # Architecture specific extensions/prefixes 12 | *.[568vq] 13 | [568vq].out 14 | 15 | *.cgo1.go 16 | *.cgo2.c 17 | _cgo_defun.c 18 | _cgo_gotypes.go 19 | _cgo_export.* 20 | 21 | _testmain.go 22 | 23 | *.exe 24 | *.test 25 | *.prof 26 | 27 | # Coveralls coverage output 28 | coverage.out 29 | *.coverprofile 30 | 31 | *.swp 32 | *.swo 33 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - "1.10" 4 | install: 5 | - go get github.com/golang/lint/golint 6 | - go get github.com/julienschmidt/httprouter 7 | - go get github.com/minimalchat/go-socket.io 8 | - go get github.com/golang-plus/uuid 9 | - go get golang.org/x/tools/cmd/cover 10 | - go get github.com/go-playground/overalls 11 | - go get github.com/mattn/goveralls 12 | - go get github.com/golang/protobuf/jsonpb 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Minimal Chat Community Code of Conduct 2 | 3 | ## Contributor Code of Conduct 4 | 5 | ### Our Pledge 6 | 7 | In the interest of fostering an open and welcoming environment, we as 8 | contributors and maintainers pledge to making participation in our project and 9 | our community a harassment-free experience for everyone, regardless of age, body 10 | size, disability, ethnicity, gender identity and expression, level of experience, 11 | nationality, personal appearance, race, religion, or sexual identity and 12 | orientation. 13 | 14 | ### Our Standards 15 | 16 | Examples of behavior that contributes to creating a positive environment 17 | include: 18 | 19 | * Using welcoming and inclusive language 20 | * Being respectful of differing viewpoints and experiences 21 | * Gracefully accepting constructive criticism 22 | * Focusing on what is best for the community 23 | * Showing empathy towards other community members 24 | 25 | Examples of unacceptable behavior by participants include: 26 | 27 | * The use of sexualized language or imagery and unwelcome sexual attention or 28 | advances 29 | * Trolling, insulting/derogatory comments, and personal or political attacks 30 | * Public or private harassment 31 | * Publishing others' private information, such as a physical or electronic 32 | address, without explicit permission 33 | * Other conduct which could reasonably be considered inappropriate in a 34 | professional setting 35 | 36 | ### Our Responsibilities 37 | 38 | Project maintainers are responsible for clarifying the standards of acceptable 39 | behavior and are expected to take appropriate and fair corrective action in 40 | response to any instances of unacceptable behavior. 41 | 42 | Project maintainers have the right and responsibility to remove, edit, or 43 | reject comments, commits, code, wiki edits, issues, and other contributions 44 | that are not aligned to this Code of Conduct, or to ban temporarily or 45 | permanently any contributor for other behaviors that they deem inappropriate, 46 | threatening, offensive, or harmful. 47 | 48 | ### Scope 49 | 50 | This Code of Conduct applies both within project spaces and in public spaces 51 | when an individual is representing the project or its community. Examples of 52 | representing a project or community include using an official project e-mail 53 | address, posting via an official social media account, or acting as an appointed 54 | representative at an online or offline event. Representation of a project may be 55 | further defined and clarified by project maintainers. 56 | 57 | ### Enforcement 58 | 59 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 60 | reported by contacting the project team at contact@minimal.chat. All 61 | complaints will be reviewed and investigated and will result in a response that 62 | is deemed necessary and appropriate to the circumstances. The project team is 63 | obligated to maintain confidentiality with regard to the reporter of an incident. 64 | Further details of specific enforcement policies may be posted separately. 65 | 66 | Project maintainers who do not follow or enforce the Code of Conduct in good 67 | faith may face temporary or permanent repercussions as determined by other 68 | members of the project's leadership. 69 | 70 | ### Attribution 71 | 72 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 73 | available at [http://contributor-covenant.org/version/1/4][version] 74 | 75 | [homepage]: http://contributor-covenant.org 76 | [version]: http://contributor-covenant.org/version/1/4/ 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guidelines 2 | 3 | ## How to become a contributor and submit your own code 4 | 5 | ### Contributing A Patch 6 | 7 | 1. Submit an issue describing your proposed change to the repo in question. 8 | 1. The [repo owners](OWNERS) will respond to your issue promptly. 9 | 1. If instructed by the repo owners provide a short design document in a PR. 10 | 1. Fork the desired repo, develop and test your code changes. Unit tests are required for most PRs. 11 | 1. Submit a pull request. 12 | 13 | ## Bug reporting 14 | 15 | If you think you found a bug, please open a [new issue](https://github.com/minimalchat/daemon/issues/new) 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:xenial-20190515 2 | 3 | RUN mkdir -p /daemon 4 | WORKDIR /daemon 5 | 6 | RUN apt clean && cat /etc/apt/sources.list 7 | RUN apt update 8 | RUN apt install -y golang ca-certificates 9 | 10 | COPY dist/daemon /daemon/server 11 | 12 | ENTRYPOINT ["/daemon/server", "-host", "0.0.0.0"] 13 | CMD ["-port", "80"] 14 | 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Matthew Mihok 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its contributors 15 | may be used to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Commands 2 | GO_CMD = `which go` 3 | LINT_CMD = $(GOPATH)/bin/golint 4 | DOCKER_CMD = `which docker` 5 | 6 | # Directories 7 | PACKAGE = github.com/minimalchat/daemon 8 | SRC = $(GOPATH)/src/$(PACKAGE) 9 | DIST = $(SRC)/dist 10 | 11 | default: lint test coverage clean compile 12 | 13 | build: lint test clean compile 14 | 15 | run: lint test go 16 | 17 | dependencies: 18 | cat $(SRC)/requirements.txt | xargs -I \\# go get -u github.com/\\# 19 | 20 | lint: 21 | $(LINT_CMD) ./... 22 | 23 | test: 24 | cd $(SRC) 25 | $(GO_CMD) test -v ./... 26 | # $(GOPATH)/bin/goveralls -coverprofile=coverage.out -service=travis-ci # -repotoken $(COVERALLS_TOKEN) 27 | 28 | coverage: 29 | cd $(SRC) 30 | $(GOPATH)/bin/overalls -project=$(PACKAGE) -covermode=count 31 | $(GOPATH)/bin/goveralls -coverprofile=overalls.coverprofile -service=travis-ci 32 | 33 | clean: 34 | rm -rf $(DIST)/mnml-daemon 35 | 36 | protob-gen: 37 | protoc --plugin=protoc-gen-go=$(GOPATH)bin/protoc-gen-go --go_out=Mpkg/api/client/client.proto=github.com/minimalchat/daemon/pkg/api/client:. pkg/api/client/*.proto 38 | protoc --plugin=protoc-gen-go=$(GOPATH)bin/protoc-gen-go --go_out=Mpkg/api/client/client.proto=github.com/minimalchat/daemon/pkg/api/client:. pkg/api/chat/*.proto 39 | protoc --plugin=protoc-gen-go=$(GOPATH)bin/protoc-gen-go --go_out=Mpkg/api/client/client.proto=github.com/minimalchat/daemon/pkg/api/client:. pkg/api/operator/*.proto 40 | protoc --plugin=protoc-gen-go=$(GOPATH)bin/protoc-gen-go --go_out=Mpkg/api/client/client.proto=github.com/minimalchat/daemon/pkg/api/client,Mpkg/api/chat/chat.proto=github.com/minimalchat/daemon/pkg/api/chat,Mpkg/api/operator/operator.proto=github.com/minimalchat/daemon/pkg/api/operator:. pkg/api/webhook/*.proto 41 | 42 | 43 | 44 | compile: 45 | mkdir -p $(DIST) 46 | cd $(SRC) 47 | $(GO_CMD) build -o $(DIST)/daemon 48 | 49 | docker: compile 50 | $(DOCKER_CMD) build -t minimalchat/daemon $(SRC) 51 | 52 | go: 53 | cd $(SRC) 54 | $(GO_CMD) run main.go -cors 55 | -------------------------------------------------------------------------------- /OWNERS: -------------------------------------------------------------------------------- 1 | mihok 2 | teesloane 3 | broneks 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Minimal Chat daemon 2 | 3 | [![GoDoc](https://godoc.org/github.com/minimalchat/daemon?status.svg)](https://godoc.org/github.com/minimalchat/daemon) 4 | [![Build Status](https://travis-ci.org/minimalchat/daemon.svg?branch=master)](https://travis-ci.org/minimalchat/daemon) 5 | [![Coverage Status](https://coveralls.io/repos/github/minimalchat/daemon/badge.svg?branch=master)](https://coveralls.io/github/minimalchat/daemon?branch=master) 6 | 7 | --- 8 | 9 | Minimal Chat is an open source live chat system providing live one on one messaging to a website visitor and an operator. 10 | 11 | Minimal Chat is: 12 | - **minimal**: simple, lightweight, accessible 13 | - **extensible**: modular, pluggable, hookable, composable 14 | 15 | --- 16 | 17 | Minimal Chat daemon is the central server providing API endpoints for operator extensions like Slack, IRC, etc. It also provides the socket.io endpoints that the web clients connect to when on a Minimal Chat enabled website. 18 | 19 | We're glad you're interested in contributing, feel free to create an [issue](https://github.com/minimalchat/daemon/issues/new) or pick one up but first check out our [contributing doc](https://github.com/minimalchat/daemon/blob/master/CONTRIBUTING.md) and [code of conduct](https://github.com/minimalchat/daemon/blob/master/CODE_OF_CONDUCT.md). 20 | 21 | 22 | ### Installation 23 | 24 | Download the prebuilt binaries available in the [releases](https://github.com/minimalchat/daemon/releases) section or clone the repo and build using Go `>=1.6`. 25 | 26 | ``` 27 | > curl -L https://github.com/minimalchat/daemon/releases/download/v0.2.0/daemon-v0.2.0 -o daemon 28 | > chmod +x ./daemon 29 | > ./daemon -host 0.0.0.0 -port 8080 30 | ``` 31 | 32 | ### Usage 33 | 34 | ``` 35 | > daemon 36 | Minimal Chat live chat API/Socket daemon 37 | 38 | Find more information at https://github.com/minimalchat/daemon 39 | 40 | Flags: 41 | -cors 42 | Set if the daemon will handle CORS 43 | -cors-origin string 44 | Host to allow cross origin resource sharing (CORS) (default "http://localhost:3000") 45 | -h Get help 46 | -host string 47 | IP to serve http and websocket traffic on (default "localhost") 48 | -port int 49 | Port used to serve HTTP and websocket traffic on (default 8000) 50 | -ssl-cert string 51 | SSL Certificate Filepath 52 | -ssl-key string 53 | SSL Key Filepath 54 | -ssl-port int 55 | Port used to serve SSL HTTPS and websocket traffic on (default 4443) 56 | 57 | ``` 58 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "os" 9 | 10 | "github.com/minimalchat/daemon/pkg/server/rest" 11 | "github.com/minimalchat/daemon/pkg/store" 12 | ) 13 | 14 | // Log levels 15 | const ( 16 | DEBUG string = "DEBUG" 17 | INFO string = "INFO" 18 | WARNING string = "WARN" 19 | ERROR string = "ERROR" 20 | FATAL string = "FATAL" 21 | ) 22 | 23 | var config rest.Config 24 | var needHelp bool 25 | 26 | func help() { 27 | fmt.Println("Minimal Chat live chat API/Socket daemon") 28 | fmt.Println() 29 | fmt.Println("Find more information at https://github.com/minimalchat/daemon") 30 | fmt.Println() 31 | 32 | fmt.Println("Flags:") 33 | flag.PrintDefaults() 34 | } 35 | 36 | func init() { 37 | // Configuration 38 | flag.StringVar(&config.Host, "host", os.Getenv("HOST"), "IP to serve http and websocket traffic on") 39 | flag.StringVar(&config.Port, "port", os.Getenv("PORT"), "Port used to serve HTTP and websocket traffic on") 40 | flag.StringVar(&config.ID, "id", "", "A string used to identify the server in outbound HTTP requests") 41 | flag.StringVar(&config.SSLCertFile, "ssl-cert", "", "SSL Certificate Filepath") 42 | flag.StringVar(&config.SSLKeyFile, "ssl-key", "", "SSL Key Filepath") 43 | flag.IntVar(&config.SSLPort, "ssl-port", 4443, "Port used to serve SSL HTTPS and websocket traffic on") 44 | flag.StringVar(&config.CORSOrigins, "cors-origins", "http://localhost:3000", "Comma separated Hosts to allow cross origin resource sharing (CORS)") 45 | flag.BoolVar(&config.CORSEnabled, "cors", false, "Set if the daemon will handle CORS") 46 | flag.BoolVar(&needHelp, "h", false, "Get help") 47 | } 48 | 49 | func main() { 50 | // Create DataStore 51 | db := new(store.InMemory) 52 | 53 | // Configuration 54 | flag.Parse() 55 | 56 | if needHelp { 57 | help() 58 | 59 | return 60 | } 61 | 62 | // Server 63 | server := rest.Initialize(db, config) 64 | 65 | // Serve SSL/HTTPS if we can 66 | if config.SSLCertFile != "" && config.SSLKeyFile != "" { 67 | log.Println(INFO, "server:", fmt.Sprintf("Listening for SSL on %s:%d ...", config.Host, config.SSLPort)) 68 | go http.ListenAndServeTLS(fmt.Sprintf("%s:%d", config.Host, config.SSLPort), config.SSLCertFile, config.SSLKeyFile, server) 69 | } 70 | 71 | log.Println(INFO, "server:", fmt.Sprintf("Listening on %s:%s ...", config.Host, config.Port)) 72 | log.Fatal(http.ListenAndServe(fmt.Sprintf("%s:%s", config.Host, config.Port), server)) 73 | } 74 | -------------------------------------------------------------------------------- /pkg/api/chat/chat.go: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/golang/protobuf/jsonpb" 9 | timestamp "github.com/golang/protobuf/ptypes/timestamp" 10 | "github.com/minimalchat/daemon/pkg/api/client" 11 | ) 12 | 13 | /* 14 | Create takes a Client object and returns a new Chat session. */ 15 | func Create(cl *client.Client) *Chat { 16 | now := time.Now() 17 | 18 | // Get the unix timestamp (seconds since epoch) 19 | seconds := now.Unix() 20 | nanos := int32(now.Sub(time.Unix(seconds, 0))) 21 | 22 | ts := ×tamp.Timestamp{ 23 | Seconds: seconds, 24 | Nanos: nanos, 25 | } 26 | 27 | c := Chat{ 28 | CreationTime: ts, 29 | UpdatedTime: ts, 30 | Open: true, 31 | Uid: cl.Uid, 32 | Client: cl, 33 | } 34 | 35 | return &c 36 | } 37 | 38 | /* 39 | UnmarshalJSON converts a JSON string (as a byte array) into a Chat object. */ 40 | func (c *Chat) UnmarshalJSON(data []byte) error { 41 | u := jsonpb.Unmarshaler{} 42 | buf := bytes.NewBuffer(data) 43 | 44 | return u.Unmarshal(buf, &*c) 45 | } 46 | 47 | /* 48 | MarshalJSON converts a Chat object into a JSON string returned as a byte 49 | array. */ 50 | func (c Chat) MarshalJSON() ([]byte, error) { 51 | m := jsonpb.Marshaler{} 52 | var buf bytes.Buffer 53 | 54 | if err := m.Marshal(&buf, &c); err != nil { 55 | return nil, err 56 | } 57 | 58 | return buf.Bytes(), nil 59 | } 60 | 61 | /* 62 | Key implements the Keyer interface of the Store and returns a string used for 63 | storing the Webhook in memory. */ 64 | func (c Chat) Key() string { 65 | return fmt.Sprintf("chat.%s", c.Uid) 66 | } 67 | -------------------------------------------------------------------------------- /pkg/api/chat/chat.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: pkg/api/chat/chat.proto 3 | 4 | package chat 5 | 6 | import proto "github.com/golang/protobuf/proto" 7 | import fmt "fmt" 8 | import math "math" 9 | import timestamp "github.com/golang/protobuf/ptypes/timestamp" 10 | import client "github.com/minimalchat/daemon/pkg/api/client" 11 | 12 | // Reference imports to suppress errors if they are not otherwise used. 13 | var _ = proto.Marshal 14 | var _ = fmt.Errorf 15 | var _ = math.Inf 16 | 17 | // This is a compile-time assertion to ensure that this generated file 18 | // is compatible with the proto package it is being compiled against. 19 | // A compilation error at this line likely means your copy of the 20 | // proto package needs to be updated. 21 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 22 | 23 | type Chat struct { 24 | CreationTime *timestamp.Timestamp `protobuf:"bytes,1,opt,name=creation_time,proto3" json:"creation_time,omitempty"` 25 | UpdatedTime *timestamp.Timestamp `protobuf:"bytes,2,opt,name=updated_time,json=update_time,proto3" json:"updated_time,omitempty"` 26 | Open bool `protobuf:"varint,3,opt,name=open,proto3" json:"open,omitempty"` 27 | Uid string `protobuf:"bytes,4,opt,name=uid,json=id,proto3" json:"uid,omitempty"` 28 | Client *client.Client `protobuf:"bytes,5,opt,name=client,proto3" json:"client,omitempty"` 29 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 30 | XXX_unrecognized []byte `json:"-"` 31 | XXX_sizecache int32 `json:"-"` 32 | } 33 | 34 | func (m *Chat) Reset() { *m = Chat{} } 35 | func (m *Chat) String() string { return proto.CompactTextString(m) } 36 | func (*Chat) ProtoMessage() {} 37 | func (*Chat) Descriptor() ([]byte, []int) { 38 | return fileDescriptor_chat_c4d626344bc83633, []int{0} 39 | } 40 | func (m *Chat) XXX_Unmarshal(b []byte) error { 41 | return xxx_messageInfo_Chat.Unmarshal(m, b) 42 | } 43 | func (m *Chat) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 44 | return xxx_messageInfo_Chat.Marshal(b, m, deterministic) 45 | } 46 | func (dst *Chat) XXX_Merge(src proto.Message) { 47 | xxx_messageInfo_Chat.Merge(dst, src) 48 | } 49 | func (m *Chat) XXX_Size() int { 50 | return xxx_messageInfo_Chat.Size(m) 51 | } 52 | func (m *Chat) XXX_DiscardUnknown() { 53 | xxx_messageInfo_Chat.DiscardUnknown(m) 54 | } 55 | 56 | var xxx_messageInfo_Chat proto.InternalMessageInfo 57 | 58 | func (m *Chat) GetCreationTime() *timestamp.Timestamp { 59 | if m != nil { 60 | return m.CreationTime 61 | } 62 | return nil 63 | } 64 | 65 | func (m *Chat) GetUpdatedTime() *timestamp.Timestamp { 66 | if m != nil { 67 | return m.UpdatedTime 68 | } 69 | return nil 70 | } 71 | 72 | func (m *Chat) GetOpen() bool { 73 | if m != nil { 74 | return m.Open 75 | } 76 | return false 77 | } 78 | 79 | func (m *Chat) GetUid() string { 80 | if m != nil { 81 | return m.Uid 82 | } 83 | return "" 84 | } 85 | 86 | func (m *Chat) GetClient() *client.Client { 87 | if m != nil { 88 | return m.Client 89 | } 90 | return nil 91 | } 92 | 93 | type Message struct { 94 | Timestamp *timestamp.Timestamp `protobuf:"bytes,1,opt,name=timestamp,proto3" json:"timestamp,omitempty"` 95 | Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` 96 | Author string `protobuf:"bytes,3,opt,name=author,proto3" json:"author,omitempty"` 97 | Chat string `protobuf:"bytes,4,opt,name=chat,proto3" json:"chat,omitempty"` 98 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 99 | XXX_unrecognized []byte `json:"-"` 100 | XXX_sizecache int32 `json:"-"` 101 | } 102 | 103 | func (m *Message) Reset() { *m = Message{} } 104 | func (m *Message) String() string { return proto.CompactTextString(m) } 105 | func (*Message) ProtoMessage() {} 106 | func (*Message) Descriptor() ([]byte, []int) { 107 | return fileDescriptor_chat_c4d626344bc83633, []int{1} 108 | } 109 | func (m *Message) XXX_Unmarshal(b []byte) error { 110 | return xxx_messageInfo_Message.Unmarshal(m, b) 111 | } 112 | func (m *Message) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 113 | return xxx_messageInfo_Message.Marshal(b, m, deterministic) 114 | } 115 | func (dst *Message) XXX_Merge(src proto.Message) { 116 | xxx_messageInfo_Message.Merge(dst, src) 117 | } 118 | func (m *Message) XXX_Size() int { 119 | return xxx_messageInfo_Message.Size(m) 120 | } 121 | func (m *Message) XXX_DiscardUnknown() { 122 | xxx_messageInfo_Message.DiscardUnknown(m) 123 | } 124 | 125 | var xxx_messageInfo_Message proto.InternalMessageInfo 126 | 127 | func (m *Message) GetTimestamp() *timestamp.Timestamp { 128 | if m != nil { 129 | return m.Timestamp 130 | } 131 | return nil 132 | } 133 | 134 | func (m *Message) GetContent() string { 135 | if m != nil { 136 | return m.Content 137 | } 138 | return "" 139 | } 140 | 141 | func (m *Message) GetAuthor() string { 142 | if m != nil { 143 | return m.Author 144 | } 145 | return "" 146 | } 147 | 148 | func (m *Message) GetChat() string { 149 | if m != nil { 150 | return m.Chat 151 | } 152 | return "" 153 | } 154 | 155 | func init() { 156 | proto.RegisterType((*Chat)(nil), "chat.Chat") 157 | proto.RegisterType((*Message)(nil), "chat.Message") 158 | } 159 | 160 | func init() { proto.RegisterFile("pkg/api/chat/chat.proto", fileDescriptor_chat_c4d626344bc83633) } 161 | 162 | var fileDescriptor_chat_c4d626344bc83633 = []byte{ 163 | // 271 bytes of a gzipped FileDescriptorProto 164 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x91, 0xbd, 0x4e, 0xc3, 0x30, 165 | 0x10, 0xc7, 0xe5, 0x36, 0xa4, 0xe4, 0xca, 0x87, 0xe4, 0x01, 0xac, 0x30, 0x10, 0x75, 0x40, 0x99, 166 | 0x1c, 0x09, 0x16, 0x16, 0x24, 0xa4, 0xce, 0x2c, 0x16, 0x3b, 0x72, 0x13, 0x93, 0x58, 0xb4, 0xb1, 167 | 0x95, 0x5c, 0x1e, 0x82, 0xf7, 0xe3, 0x81, 0x90, 0x3f, 0x52, 0xc4, 0x04, 0x8b, 0xef, 0xce, 0x77, 168 | 0xf7, 0xbb, 0xff, 0xd9, 0x70, 0x6d, 0x3f, 0xda, 0x4a, 0x5a, 0x5d, 0xd5, 0x9d, 0x44, 0x7f, 0x70, 169 | 0x3b, 0x18, 0x34, 0x34, 0x71, 0x7e, 0x7e, 0xdb, 0x1a, 0xd3, 0xee, 0x55, 0xe5, 0xef, 0x76, 0xd3, 170 | 0x7b, 0x85, 0xfa, 0xa0, 0x46, 0x94, 0x07, 0x1b, 0xca, 0xf2, 0x9b, 0x63, 0xff, 0x5e, 0xab, 0x1e, 171 | 0xa3, 0x09, 0xc9, 0xcd, 0x17, 0x81, 0x64, 0xdb, 0x49, 0xa4, 0xcf, 0x70, 0x5e, 0x0f, 0x4a, 0xa2, 172 | 0x36, 0xfd, 0x9b, 0x23, 0x30, 0x52, 0x90, 0x72, 0x7d, 0x9f, 0xf3, 0x80, 0xe7, 0x33, 0x9e, 0xbf, 173 | 0xce, 0x78, 0xf1, 0xbb, 0x81, 0x3e, 0xc1, 0xd9, 0x64, 0x1b, 0x89, 0xaa, 0x09, 0x80, 0xc5, 0x9f, 174 | 0x80, 0x75, 0xa8, 0x0f, 0xed, 0x14, 0x12, 0x63, 0x55, 0xcf, 0x96, 0x05, 0x29, 0x4f, 0x85, 0xf7, 175 | 0xe9, 0x25, 0x2c, 0x27, 0xdd, 0xb0, 0xa4, 0x20, 0x65, 0x26, 0x16, 0xba, 0xa1, 0x77, 0x90, 0x06, 176 | 0xf9, 0xec, 0xc4, 0xd3, 0x2f, 0x78, 0xdc, 0x66, 0xeb, 0x8d, 0x88, 0xd9, 0xcd, 0x27, 0x81, 0xd5, 177 | 0x8b, 0x1a, 0x47, 0xd9, 0x2a, 0xfa, 0x08, 0xd9, 0xf1, 0x49, 0xfe, 0xb1, 0xd5, 0x4f, 0x31, 0x65, 178 | 0xb0, 0xaa, 0x4d, 0x8f, 0x6e, 0xdc, 0xc2, 0x4b, 0x98, 0x43, 0x7a, 0x05, 0xa9, 0x9c, 0xb0, 0x33, 179 | 0x83, 0x97, 0x9b, 0x89, 0x18, 0xb9, 0x25, 0xdc, 0xa7, 0x44, 0xc5, 0xde, 0xdf, 0xa5, 0x7e, 0xc8, 180 | 0xc3, 0x77, 0x00, 0x00, 0x00, 0xff, 0xff, 0x66, 0x8f, 0xac, 0x05, 0xc8, 0x01, 0x00, 0x00, 181 | } 182 | -------------------------------------------------------------------------------- /pkg/api/chat/chat.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package chat; 3 | 4 | import "google/protobuf/timestamp.proto"; 5 | import "pkg/api/client/client.proto"; 6 | 7 | message Chat { 8 | google.protobuf.Timestamp creation_time = 1 [json_name="creation_time"]; 9 | google.protobuf.Timestamp updated_time = 2 [json_name="update_time"]; 10 | 11 | bool open = 3; 12 | string uid = 4 [json_name="id"]; 13 | 14 | client.Client client = 5; 15 | } 16 | 17 | message Message { 18 | google.protobuf.Timestamp timestamp = 1; 19 | string content = 2; 20 | string author = 3; 21 | string chat = 4; 22 | } 23 | -------------------------------------------------------------------------------- /pkg/api/chat/message.go: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/golang/protobuf/jsonpb" 9 | timestamp "github.com/golang/protobuf/ptypes/timestamp" 10 | ) 11 | 12 | /* 13 | CreateMessage constructs a new `Message` with a default timestamp of now */ 14 | func CreateMessage() *Message { 15 | now := time.Now() 16 | 17 | // Get the unix timestamp (seconds since epoch) 18 | seconds := now.Unix() 19 | nanos := int32(now.Sub(time.Unix(seconds, 0))) 20 | 21 | ts := ×tamp.Timestamp{ 22 | Seconds: seconds, 23 | Nanos: nanos, 24 | } 25 | 26 | m := Message{ 27 | Timestamp: ts, 28 | } 29 | 30 | return &m 31 | } 32 | 33 | /* 34 | UnmarshalJSON converts a JSON string (as a byte array) into a Message object */ 35 | func (m *Message) UnmarshalJSON(data []byte) error { 36 | u := jsonpb.Unmarshaler{} 37 | buf := bytes.NewBuffer(data) 38 | 39 | return u.Unmarshal(buf, &*m) 40 | } 41 | 42 | /* 43 | MarshalJSON converts a Message object into a JSON string returned as a byte 44 | array */ 45 | func (m Message) MarshalJSON() ([]byte, error) { 46 | var buf bytes.Buffer 47 | 48 | mrsh := jsonpb.Marshaler{} 49 | 50 | if err := mrsh.Marshal(&buf, &m); err != nil { 51 | return nil, err 52 | } 53 | 54 | return buf.Bytes(), nil 55 | } 56 | 57 | /* 58 | Key implements the Keyer interface of the Store and returns a string used for 59 | storing the Message in memory. */ 60 | func (m Message) Key() string { 61 | return fmt.Sprintf("message.%s-%d", m.Chat, m.Timestamp.Seconds) 62 | } 63 | -------------------------------------------------------------------------------- /pkg/api/chat/router.go: -------------------------------------------------------------------------------- 1 | package chat 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/julienschmidt/httprouter" // Router 10 | 11 | "github.com/minimalchat/daemon/pkg/store" 12 | ) 13 | 14 | // Log levels 15 | const ( 16 | DEBUG string = "DEBUG" 17 | INFO string = "INFO" 18 | WARNING string = "WARN" 19 | ERROR string = "ERROR" 20 | FATAL string = "FATAL" 21 | ) 22 | 23 | /* 24 | Routes defines the Chat and Chat Message API routes */ 25 | func Routes(router *httprouter.Router, ds *store.InMemory) { 26 | 27 | // Chat 28 | router.GET("/api/chats", readChats(ds)) // Check 29 | router.GET("/api/chat", readChats(ds)) 30 | 31 | router.GET("/api/chat/:id", readChat(ds)) // Check 32 | 33 | router.POST("/api/chat", createOrUpdateChat(ds)) // Not Implement 34 | 35 | router.POST("/api/chat/", createOrUpdateChat(ds)) // Not Implement 36 | 37 | router.PUT("/api/chat/:id", createOrUpdateChat(ds)) // Not Implement 38 | 39 | router.PATCH("/api/chat/:id", createOrUpdateChat(ds)) // Not Implement 40 | 41 | router.DELETE("/api/chat/:id", deleteChat(ds)) // Not Implement 42 | 43 | // Chat Messages 44 | router.GET("/api/chat/:id/messages", readMessages(ds)) // Check 45 | router.GET("/api/chat/:id/message", readMessages(ds)) 46 | 47 | router.GET("/api/chat/:id/message/:mid", readMessage(ds)) // Not Implement 48 | 49 | router.POST("/api/chat/:id/message", createMessage(ds)) // Check 50 | 51 | router.POST("/api/chat/:id/message/", createMessage(ds)) // Check 52 | 53 | router.PUT("/api/chat/:id/message/:mid", updateMessage(ds)) // Not Implement 54 | 55 | router.PATCH("/api/chat/:id/message/:mid", updateMessage(ds)) // Not Implement 56 | 57 | router.DELETE("/api/chat/:id/message/:mid", deleteMessage(ds)) // Not Implement 58 | 59 | } 60 | 61 | /* 62 | notImplemented is a helper function for intentionally unimplemented routes */ 63 | func notImplemented(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 64 | resp.Header().Set("Content-Type", "text/plain; charset=UTF-8") 65 | resp.WriteHeader(http.StatusNotImplemented) 66 | 67 | fmt.Fprintf(resp, "Not Implemented") 68 | } 69 | 70 | // Chats 71 | 72 | /* 73 | GET /api/chat 74 | GET /api/chats */ 75 | func readChats(ds *store.InMemory) func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 76 | return func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 77 | chats, _ := ds.Search("chat.") 78 | result := make(map[string]interface{}) 79 | 80 | result["chats"] = chats 81 | 82 | log.Println(INFO, "api/chat:", "Reading chats", fmt.Sprintf("(%d records)", len(chats))) 83 | 84 | resp.Header().Set("Content-Type", "application/json; charset=UTF-8") 85 | resp.WriteHeader(http.StatusOK) 86 | if err := json.NewEncoder(resp).Encode(result); err != nil { 87 | panic(err) 88 | } 89 | } 90 | } 91 | 92 | // Chat 93 | 94 | /* 95 | GET /api/chat/:id */ 96 | func readChat(ds *store.InMemory) func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 97 | return func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 98 | ch, _ := ds.Get(fmt.Sprintf("chat.%s", params.ByName("id"))) 99 | 100 | log.Println(DEBUG, "api/chat:", "Reading chat", params.ByName("id")) 101 | 102 | resp.Header().Set("Content-Type", "application/json; charset=UTF-8") 103 | if ch != nil { 104 | resp.WriteHeader(http.StatusOK) 105 | if err := json.NewEncoder(resp).Encode(ch); err != nil { 106 | panic(err) 107 | } 108 | } else { 109 | resp.WriteHeader(http.StatusNotFound) 110 | 111 | fmt.Fprintf(resp, "Not Found") 112 | } 113 | } 114 | } 115 | 116 | /* 117 | POST / PUT / PATCH /api/chat/:id? */ 118 | func createOrUpdateChat(ds *store.InMemory) func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 119 | return notImplemented 120 | } 121 | 122 | /* 123 | DELETE /api/chat/:id? */ 124 | func deleteChat(ds *store.InMemory) func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 125 | return notImplemented 126 | } 127 | 128 | // Chat Messages 129 | 130 | /* 131 | GET /api/chat/:id/message 132 | GET /api/chat/:id/messages */ 133 | func readMessages(ds *store.InMemory) func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 134 | return func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 135 | messages, _ := ds.Search(fmt.Sprintf("message.%s-", params.ByName("id"))) 136 | result := make(map[string]interface{}) 137 | 138 | result["messages"] = messages 139 | 140 | log.Println(INFO, "api/message:", "Reading messages", fmt.Sprintf("(%d records)", len(messages))) 141 | 142 | resp.Header().Set("Content-Type", "application/json; charset=UTF-8") 143 | resp.WriteHeader(http.StatusOK) 144 | if err := json.NewEncoder(resp).Encode(result); err != nil { 145 | panic(err) 146 | } 147 | } 148 | } 149 | 150 | // Chat Message 151 | 152 | /* 153 | GET /api/chat/:id/message/:mid */ 154 | func readMessage(ds *store.InMemory) func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 155 | return notImplemented 156 | } 157 | 158 | /* 159 | POST / PUT /api/chat/:id/message */ 160 | func createMessage(ds *store.InMemory) func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 161 | return func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 162 | var msg *Message 163 | 164 | id := params.ByName("id") 165 | decoder := json.NewDecoder(req.Body) 166 | 167 | resp.Header().Set("Content-Type", "application/json; charset=UTF-8") 168 | 169 | if err := decoder.Decode(&msg); err != nil { 170 | log.Println(ERROR, "api/message:", "Bad Request", err) 171 | resp.WriteHeader(http.StatusBadRequest) 172 | 173 | fmt.Fprintf(resp, "Bad Request") 174 | return 175 | } 176 | 177 | if id == "" { 178 | log.Println(ERROR, "api/message:", "Bad Request ID", id) 179 | resp.WriteHeader(http.StatusBadRequest) 180 | 181 | fmt.Fprintf(resp, "Bad Request") 182 | return 183 | } 184 | 185 | result, _ := ds.Get(fmt.Sprintf("chat.%s", id)) 186 | 187 | if result == nil { 188 | log.Println(DEBUG, "api/message:", "Unknown Chat ID", id, result) 189 | resp.WriteHeader(http.StatusNotFound) 190 | 191 | fmt.Fprintf(resp, "Not Found") 192 | return 193 | } 194 | 195 | if ch, ok := result.(Chat); ok { 196 | log.Println(DEBUG, "api/operator:", msg.Content, ch.Uid) 197 | 198 | // Fix if missing in Message object 199 | if msg.Chat == "" { 200 | msg.Chat = id 201 | } 202 | 203 | ds.Put(msg) 204 | 205 | // TODO: Pass API post message to Operator via socket somehow 206 | // ch.Client.Socket.Emit("operator:message", msg.Content, nil) 207 | } else { 208 | log.Println(ERROR, "api/message:", "Could not cast store data to struct", ok, result.(Chat)) 209 | resp.WriteHeader(http.StatusInternalServerError) 210 | 211 | fmt.Fprintf(resp, "Bad Request") 212 | return 213 | } 214 | 215 | resp.WriteHeader(http.StatusOK) 216 | } 217 | } 218 | 219 | /* 220 | PATCH /api/chat/:id/message/:mid */ 221 | func updateMessage(ds *store.InMemory) func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 222 | return notImplemented 223 | } 224 | 225 | /* 226 | DELETE /api/chat/:id/message/:mid */ 227 | func deleteMessage(ds *store.InMemory) func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 228 | return notImplemented 229 | } 230 | -------------------------------------------------------------------------------- /pkg/api/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/golang-plus/uuid" // UUID (RFC 4122) 8 | "github.com/golang/protobuf/jsonpb" 9 | ) 10 | 11 | /* 12 | Create takes a Sid identifier string and returns a new Client. */ 13 | func Create(sid string) *Client { 14 | c := Client{ 15 | FirstName: "Site", 16 | LastName: "Visitor", 17 | Name: "Site Visitor", 18 | } 19 | 20 | // Generate Client UID 21 | uuid, _ := uuid.NewRandom() 22 | c.Uid = uuid.String() 23 | 24 | // Assign Client SID 25 | c.Sid = sid 26 | 27 | return &c 28 | } 29 | 30 | /* 31 | GetFullName returns the Client FirstName and LastName concatenated by a space. 32 | Implementing the Client as a Person. */ 33 | func (c *Client) GetFullName() string { 34 | if c != nil { 35 | return fmt.Sprintf("%s %s", c.GetFirstName(), c.GetLastName()) 36 | } 37 | return "" 38 | } 39 | 40 | /* 41 | UnmarshalJSON converts a JSON string (as a byte array) into a Client object. */ 42 | func (c *Client) UnmarshalJSON(data []byte) error { 43 | u := jsonpb.Unmarshaler{} 44 | buf := bytes.NewBuffer(data) 45 | 46 | return u.Unmarshal(buf, &*c) 47 | } 48 | 49 | /* 50 | MarshalJSON converts a Client object into a JSON string returned as a byte 51 | array. */ 52 | func (c Client) MarshalJSON() ([]byte, error) { 53 | var buf bytes.Buffer 54 | 55 | m := jsonpb.Marshaler{} 56 | 57 | if err := m.Marshal(&buf, &c); err != nil { 58 | return nil, err 59 | } 60 | 61 | return buf.Bytes(), nil 62 | } 63 | 64 | /* 65 | Key implements the Keyer interface of the Store and returns a string used for 66 | storing the Client in memory. */ 67 | func (c Client) Key() string { 68 | return fmt.Sprintf("client.%s", c.Uid) 69 | } 70 | -------------------------------------------------------------------------------- /pkg/api/client/client.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: pkg/api/client/client.proto 3 | 4 | package client 5 | 6 | import proto "github.com/golang/protobuf/proto" 7 | import fmt "fmt" 8 | import math "math" 9 | 10 | // Reference imports to suppress errors if they are not otherwise used. 11 | var _ = proto.Marshal 12 | var _ = fmt.Errorf 13 | var _ = math.Inf 14 | 15 | // This is a compile-time assertion to ensure that this generated file 16 | // is compatible with the proto package it is being compiled against. 17 | // A compilation error at this line likely means your copy of the 18 | // proto package needs to be updated. 19 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 20 | 21 | type Client struct { 22 | FirstName string `protobuf:"bytes,1,opt,name=first_name,proto3" json:"first_name,omitempty"` 23 | LastName string `protobuf:"bytes,2,opt,name=last_name,proto3" json:"last_name,omitempty"` 24 | Name string `protobuf:"bytes,3,opt,name=name,proto3" json:"name,omitempty"` 25 | Uid string `protobuf:"bytes,4,opt,name=uid,json=id,proto3" json:"uid,omitempty"` 26 | Sid string `protobuf:"bytes,5,opt,name=sid,proto3" json:"sid,omitempty"` 27 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 28 | XXX_unrecognized []byte `json:"-"` 29 | XXX_sizecache int32 `json:"-"` 30 | } 31 | 32 | func (m *Client) Reset() { *m = Client{} } 33 | func (m *Client) String() string { return proto.CompactTextString(m) } 34 | func (*Client) ProtoMessage() {} 35 | func (*Client) Descriptor() ([]byte, []int) { 36 | return fileDescriptor_client_9dcb2d1f509f7960, []int{0} 37 | } 38 | func (m *Client) XXX_Unmarshal(b []byte) error { 39 | return xxx_messageInfo_Client.Unmarshal(m, b) 40 | } 41 | func (m *Client) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 42 | return xxx_messageInfo_Client.Marshal(b, m, deterministic) 43 | } 44 | func (dst *Client) XXX_Merge(src proto.Message) { 45 | xxx_messageInfo_Client.Merge(dst, src) 46 | } 47 | func (m *Client) XXX_Size() int { 48 | return xxx_messageInfo_Client.Size(m) 49 | } 50 | func (m *Client) XXX_DiscardUnknown() { 51 | xxx_messageInfo_Client.DiscardUnknown(m) 52 | } 53 | 54 | var xxx_messageInfo_Client proto.InternalMessageInfo 55 | 56 | func (m *Client) GetFirstName() string { 57 | if m != nil { 58 | return m.FirstName 59 | } 60 | return "" 61 | } 62 | 63 | func (m *Client) GetLastName() string { 64 | if m != nil { 65 | return m.LastName 66 | } 67 | return "" 68 | } 69 | 70 | func (m *Client) GetName() string { 71 | if m != nil { 72 | return m.Name 73 | } 74 | return "" 75 | } 76 | 77 | func (m *Client) GetUid() string { 78 | if m != nil { 79 | return m.Uid 80 | } 81 | return "" 82 | } 83 | 84 | func (m *Client) GetSid() string { 85 | if m != nil { 86 | return m.Sid 87 | } 88 | return "" 89 | } 90 | 91 | func init() { 92 | proto.RegisterType((*Client)(nil), "client.Client") 93 | } 94 | 95 | func init() { proto.RegisterFile("pkg/api/client/client.proto", fileDescriptor_client_9dcb2d1f509f7960) } 96 | 97 | var fileDescriptor_client_9dcb2d1f509f7960 = []byte{ 98 | // 136 bytes of a gzipped FileDescriptorProto 99 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0x92, 0x2e, 0xc8, 0x4e, 0xd7, 100 | 0x4f, 0x2c, 0xc8, 0xd4, 0x4f, 0xce, 0xc9, 0x4c, 0xcd, 0x2b, 0x81, 0x52, 0x7a, 0x05, 0x45, 0xf9, 101 | 0x25, 0xf9, 0x42, 0x6c, 0x10, 0x9e, 0x52, 0x2d, 0x17, 0x9b, 0x33, 0x98, 0x25, 0x24, 0xc7, 0xc5, 102 | 0x95, 0x96, 0x59, 0x54, 0x5c, 0x12, 0x9f, 0x97, 0x98, 0x9b, 0x2a, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1, 103 | 0x19, 0x84, 0x24, 0x22, 0x24, 0xc3, 0xc5, 0x99, 0x93, 0x08, 0x93, 0x66, 0x02, 0x4b, 0x23, 0x04, 104 | 0x84, 0x84, 0xb8, 0x58, 0xc0, 0x12, 0xcc, 0x60, 0x09, 0x30, 0x5b, 0x88, 0x9f, 0x8b, 0xb9, 0x34, 105 | 0x33, 0x45, 0x82, 0x05, 0x2c, 0xc4, 0x94, 0x99, 0x22, 0x24, 0xc0, 0xc5, 0x5c, 0x9c, 0x99, 0x22, 106 | 0xc1, 0x0a, 0x16, 0x00, 0x31, 0x93, 0xd8, 0xc0, 0xae, 0x31, 0x06, 0x04, 0x00, 0x00, 0xff, 0xff, 107 | 0x77, 0x0c, 0x66, 0xc2, 0xac, 0x00, 0x00, 0x00, 108 | } 109 | -------------------------------------------------------------------------------- /pkg/api/client/client.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package client; 3 | 4 | message Client { 5 | string first_name = 1 [json_name="first_name"]; 6 | string last_name = 2 [json_name="last_name"]; 7 | string name = 3 [json_name="name"]; 8 | string uid = 4 [json_name="id"]; 9 | string sid = 5 [json_name="sid"]; 10 | } 11 | -------------------------------------------------------------------------------- /pkg/api/client/router.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/julienschmidt/httprouter" // Router 10 | 11 | "github.com/minimalchat/daemon/pkg/store" 12 | ) 13 | 14 | // Log levels 15 | const ( 16 | DEBUG string = "DEBUG" 17 | INFO string = "INFO" 18 | WARNING string = "WARN" 19 | ERROR string = "ERROR" 20 | FATAL string = "FATAL" 21 | ) 22 | 23 | /* 24 | Routes defines the Client API routes */ 25 | func Routes(router *httprouter.Router, ds *store.InMemory) { 26 | 27 | // Client 28 | router.GET("/api/clients", readClients(ds)) // Check 29 | router.GET("/api/client", readClients(ds)) 30 | 31 | router.GET("/api/client/:id", readClients(ds)) // Check 32 | 33 | router.POST("/api/client", createOrUpdateClient(ds)) // Not Implement 34 | 35 | router.POST("/api/client/", createOrUpdateClient(ds)) // Not Implement 36 | 37 | router.PUT("/api/client/:id", createOrUpdateClient(ds)) // Not Implement 38 | 39 | router.PATCH("/api/client/:id", createOrUpdateClient(ds)) // Not Implement 40 | 41 | router.DELETE("/api/client/:id", deleteClient(ds)) // Not Implement 42 | } 43 | 44 | /* 45 | notImplemented is a helper function for intentionally unimplemented routes */ 46 | func notImplemented(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 47 | resp.Header().Set("Content-Type", "text/plain; charset=UTF-8") 48 | resp.WriteHeader(http.StatusNotImplemented) 49 | 50 | fmt.Fprintf(resp, "Not Implemented") 51 | } 52 | 53 | // Clients 54 | 55 | /* 56 | GET /api/client 57 | GET /api/clients */ 58 | func readClients(ds *store.InMemory) func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 59 | return func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 60 | clients, _ := ds.Search("client.") 61 | result := make(map[string]interface{}) 62 | 63 | result["clients"] = clients 64 | 65 | log.Println(INFO, "api/client:", "Reading clients", fmt.Sprintf("(%d records)", len(clients))) 66 | 67 | resp.Header().Set("Content-Type", "application/json; charset=UTF-8") 68 | resp.WriteHeader(http.StatusOK) 69 | if err := json.NewEncoder(resp).Encode(result); err != nil { 70 | panic(err) 71 | } 72 | } 73 | } 74 | 75 | // Client 76 | 77 | /* 78 | GET /api/client/:id */ 79 | func readClient(ds *store.InMemory) func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 80 | return func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 81 | cl, _ := ds.Get(fmt.Sprintf("client.%s", params.ByName("id"))) 82 | 83 | log.Println(DEBUG, "api/client:", "Reading client", params.ByName("id")) 84 | 85 | resp.Header().Set("Content-Type", "application/json; charset=UTF-8") 86 | resp.WriteHeader(http.StatusOK) 87 | if err := json.NewEncoder(resp).Encode(cl); err != nil { 88 | panic(err) 89 | } 90 | } 91 | } 92 | 93 | /* 94 | POST / PUT / PATCH /api/chat/:id/message/:mid */ 95 | func createOrUpdateClient(ds *store.InMemory) func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 96 | return notImplemented 97 | } 98 | 99 | /* 100 | DELETE /api/chat/:id/message/:mid */ 101 | func deleteClient(ds *store.InMemory) func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 102 | return notImplemented 103 | } 104 | -------------------------------------------------------------------------------- /pkg/api/operator/operator.go: -------------------------------------------------------------------------------- 1 | package operator 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/golang-plus/uuid" // UUID (RFC 4122) 8 | "github.com/golang/protobuf/jsonpb" 9 | ) 10 | 11 | /* 12 | Create takes an optional Id string and returns a new Operator. */ 13 | func Create(id string) *Operator { 14 | o := Operator{} 15 | 16 | if id == "" { 17 | uuid, _ := uuid.NewRandom() 18 | o.Uid = uuid.String() 19 | } else { 20 | o.Uid = id 21 | } 22 | 23 | return &o 24 | } 25 | 26 | /* 27 | GetFullName returns the Operator FirstName and LastName concatenated by a 28 | space. Implementing the Operator as a Person. */ 29 | func (o *Operator) GetFullName() string { 30 | if o != nil { 31 | return fmt.Sprintf("%s %s", o.GetFirstName(), o.GetLastName()) 32 | } 33 | return "" 34 | } 35 | 36 | /* 37 | UnmarshalJSON converts a JSON string (as a byte array) into a Operator object. */ 38 | func (o *Operator) UnmarshalJSON(data []byte) error { 39 | u := jsonpb.Unmarshaler{} 40 | buf := bytes.NewBuffer(data) 41 | 42 | return u.Unmarshal(buf, &*o) 43 | } 44 | 45 | /* 46 | MarshalJSON converts a Operator object into a JSON string returned as a byte 47 | array. */ 48 | func (o Operator) MarshalJSON() ([]byte, error) { 49 | var buf bytes.Buffer 50 | 51 | m := jsonpb.Marshaler{} 52 | 53 | if err := m.Marshal(&buf, &o); err != nil { 54 | return nil, err 55 | } 56 | 57 | return buf.Bytes(), nil 58 | } 59 | 60 | /* 61 | Key implements the Keyer interface of the Store and returns a string used for 62 | storing the Operator in memory. */ 63 | func (o Operator) Key() string { 64 | return fmt.Sprintf("operator.%s", o.Aid) 65 | } 66 | -------------------------------------------------------------------------------- /pkg/api/operator/operator.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: pkg/api/operator/operator.proto 3 | 4 | package operator 5 | 6 | import proto "github.com/golang/protobuf/proto" 7 | import fmt "fmt" 8 | import math "math" 9 | 10 | // Reference imports to suppress errors if they are not otherwise used. 11 | var _ = proto.Marshal 12 | var _ = fmt.Errorf 13 | var _ = math.Inf 14 | 15 | // This is a compile-time assertion to ensure that this generated file 16 | // is compatible with the proto package it is being compiled against. 17 | // A compilation error at this line likely means your copy of the 18 | // proto package needs to be updated. 19 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 20 | 21 | type Operator struct { 22 | FirstName string `protobuf:"bytes,1,opt,name=firstName,json=first_name,proto3" json:"firstName,omitempty"` 23 | LastName string `protobuf:"bytes,2,opt,name=lastName,json=last_name,proto3" json:"lastName,omitempty"` 24 | // Access ID and Access Token will be used for enhanced security, 25 | // accessing the operator, etc 26 | Aid string `protobuf:"bytes,3,opt,name=aid,json=access_id,proto3" json:"aid,omitempty"` 27 | Atoken string `protobuf:"bytes,6,opt,name=atoken,json=access_token,proto3" json:"atoken,omitempty"` 28 | // Unique identifier 29 | Uid string `protobuf:"bytes,4,opt,name=uid,json=id,proto3" json:"uid,omitempty"` 30 | // URI to an avatar image, best results it should be 1:1 ratio 31 | Avatar string `protobuf:"bytes,5,opt,name=avatar,proto3" json:"avatar,omitempty"` 32 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 33 | XXX_unrecognized []byte `json:"-"` 34 | XXX_sizecache int32 `json:"-"` 35 | } 36 | 37 | func (m *Operator) Reset() { *m = Operator{} } 38 | func (m *Operator) String() string { return proto.CompactTextString(m) } 39 | func (*Operator) ProtoMessage() {} 40 | func (*Operator) Descriptor() ([]byte, []int) { 41 | return fileDescriptor_operator_d148b4eafdb2c445, []int{0} 42 | } 43 | func (m *Operator) XXX_Unmarshal(b []byte) error { 44 | return xxx_messageInfo_Operator.Unmarshal(m, b) 45 | } 46 | func (m *Operator) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 47 | return xxx_messageInfo_Operator.Marshal(b, m, deterministic) 48 | } 49 | func (dst *Operator) XXX_Merge(src proto.Message) { 50 | xxx_messageInfo_Operator.Merge(dst, src) 51 | } 52 | func (m *Operator) XXX_Size() int { 53 | return xxx_messageInfo_Operator.Size(m) 54 | } 55 | func (m *Operator) XXX_DiscardUnknown() { 56 | xxx_messageInfo_Operator.DiscardUnknown(m) 57 | } 58 | 59 | var xxx_messageInfo_Operator proto.InternalMessageInfo 60 | 61 | func (m *Operator) GetFirstName() string { 62 | if m != nil { 63 | return m.FirstName 64 | } 65 | return "" 66 | } 67 | 68 | func (m *Operator) GetLastName() string { 69 | if m != nil { 70 | return m.LastName 71 | } 72 | return "" 73 | } 74 | 75 | func (m *Operator) GetAid() string { 76 | if m != nil { 77 | return m.Aid 78 | } 79 | return "" 80 | } 81 | 82 | func (m *Operator) GetAtoken() string { 83 | if m != nil { 84 | return m.Atoken 85 | } 86 | return "" 87 | } 88 | 89 | func (m *Operator) GetUid() string { 90 | if m != nil { 91 | return m.Uid 92 | } 93 | return "" 94 | } 95 | 96 | func (m *Operator) GetAvatar() string { 97 | if m != nil { 98 | return m.Avatar 99 | } 100 | return "" 101 | } 102 | 103 | func init() { 104 | proto.RegisterType((*Operator)(nil), "operator.Operator") 105 | } 106 | 107 | func init() { 108 | proto.RegisterFile("pkg/api/operator/operator.proto", fileDescriptor_operator_d148b4eafdb2c445) 109 | } 110 | 111 | var fileDescriptor_operator_d148b4eafdb2c445 = []byte{ 112 | // 173 bytes of a gzipped FileDescriptorProto 113 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x3c, 0xce, 0x41, 0xaa, 0xc2, 0x30, 114 | 0x10, 0xc6, 0x71, 0xda, 0xbe, 0x17, 0xda, 0x41, 0x10, 0xb2, 0x28, 0x01, 0x15, 0xc5, 0x95, 0x2b, 115 | 0xbb, 0xf0, 0x1e, 0x0a, 0x5e, 0xa0, 0x8c, 0x4d, 0x94, 0xa1, 0xb6, 0x09, 0x49, 0xf4, 0x46, 0xde, 116 | 0x53, 0x9c, 0x46, 0x77, 0xf3, 0xfd, 0x7f, 0x9b, 0x81, 0xb5, 0xeb, 0x6f, 0x0d, 0x3a, 0x6a, 0xac, 117 | 0x33, 0x1e, 0xa3, 0xf5, 0xbf, 0x63, 0xef, 0xbc, 0x8d, 0x56, 0x96, 0xdf, 0xbd, 0x7d, 0x65, 0x50, 118 | 0x9e, 0xd2, 0x90, 0x2b, 0xa8, 0xae, 0xe4, 0x43, 0x3c, 0xe2, 0x60, 0x54, 0xb6, 0xc9, 0x76, 0xd5, 119 | 0x19, 0x38, 0xb4, 0x23, 0x0e, 0x46, 0x2e, 0xa0, 0xbc, 0x63, 0xd2, 0x9c, 0xb5, 0xfa, 0xec, 0x09, 120 | 0x6b, 0x28, 0x90, 0xb4, 0x2a, 0xa6, 0x8e, 0x5d, 0x67, 0x42, 0x68, 0x49, 0xcb, 0x25, 0x08, 0x8c, 121 | 0xb6, 0x37, 0xa3, 0x12, 0x4c, 0xb3, 0x44, 0xdc, 0xe4, 0x1c, 0x8a, 0x07, 0x69, 0xf5, 0xc7, 0x94, 122 | 0x93, 0x96, 0x35, 0x08, 0x7c, 0x62, 0x44, 0xaf, 0xfe, 0xb9, 0xa5, 0x75, 0x11, 0xfc, 0xf8, 0xe1, 123 | 0x1d, 0x00, 0x00, 0xff, 0xff, 0xe0, 0x58, 0x75, 0x63, 0xdb, 0x00, 0x00, 0x00, 124 | } 125 | -------------------------------------------------------------------------------- /pkg/api/operator/operator.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package operator; 3 | 4 | message Operator { 5 | string firstName = 1 [json_name="first_name"]; 6 | string lastName = 2 [json_name="last_name"]; 7 | 8 | // Access ID and Access Token will be used for enhanced security, 9 | // accessing the operator, etc 10 | string aid = 3 [json_name="access_id"]; 11 | string atoken = 6 [json_name="access_token"]; 12 | 13 | // Unique identifier 14 | string uid = 4 [json_name="id"]; 15 | 16 | // URI to an avatar image, best results it should be 1:1 ratio 17 | string avatar = 5 [json_name="avatar"]; 18 | } 19 | -------------------------------------------------------------------------------- /pkg/api/operator/router.go: -------------------------------------------------------------------------------- 1 | package operator 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | 9 | "github.com/julienschmidt/httprouter" // Router 10 | 11 | "github.com/minimalchat/daemon/pkg/store" 12 | ) 13 | 14 | // Log levels 15 | const ( 16 | DEBUG string = "DEBUG" 17 | INFO string = "INFO" 18 | WARNING string = "WARN" 19 | ERROR string = "ERROR" 20 | FATAL string = "FATAL" 21 | ) 22 | 23 | /* 24 | Routes defines the Operator API CRUD uris */ 25 | func Routes(router *httprouter.Router, ds *store.InMemory) { 26 | 27 | // Operator 28 | // Read 29 | router.GET("/api/operators", readOperators(ds)) // Check 30 | router.GET("/api/operator", readOperators(ds)) 31 | 32 | router.GET("/api/operator/:id", readOperator(ds)) // Check 33 | 34 | // Create / Update 35 | router.POST("/api/operator", createOrUpdateOperator(ds)) // Check 36 | router.POST("/api/operator/:id", createOrUpdateOperator(ds)) // Check 37 | router.PUT("/api/operator/", createOrUpdateOperator(ds)) // Check 38 | router.PATCH("/api/operator/:id", createOrUpdateOperator(ds)) // Check 39 | 40 | // Delete 41 | router.DELETE("/api/operator/:id", deleteOperator(ds)) // Check 42 | } 43 | 44 | // Operators 45 | 46 | /* 47 | GET /api/operator 48 | GET /api/operators */ 49 | func readOperators(ds *store.InMemory) func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 50 | return func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 51 | operators, _ := ds.Search("operator.") 52 | result := make(map[string]interface{}) 53 | 54 | result["operators"] = operators 55 | 56 | log.Println(INFO, "api/operator:", "Reading operators", fmt.Sprintf("(%d records)", len(operators))) 57 | 58 | resp.Header().Set("Content-Type", "application/json; charset=UTF-8") 59 | resp.WriteHeader(http.StatusOK) 60 | if err := json.NewEncoder(resp).Encode(result); err != nil { 61 | panic(err) 62 | } 63 | } 64 | } 65 | 66 | // Operator 67 | 68 | /* 69 | GET /api/operator/:id */ 70 | func readOperator(ds *store.InMemory) func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 71 | return func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 72 | id := params.ByName("id") 73 | op, _ := ds.Get(fmt.Sprintf("operator.%s", id)) 74 | 75 | log.Println(DEBUG, "api/operator:", "Reading operator", params.ByName("id")) 76 | 77 | resp.Header().Set("Content-Type", "application/json; charset=UTF-8") 78 | resp.WriteHeader(http.StatusOK) 79 | if err := json.NewEncoder(resp).Encode(op); err != nil { 80 | panic(err) 81 | } 82 | } 83 | } 84 | 85 | /* 86 | POST / PUT / PATCH /api/operator/:id? */ 87 | func createOrUpdateOperator(ds *store.InMemory) func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 88 | return func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 89 | var op *Operator 90 | 91 | id := params.ByName("id") 92 | decoder := json.NewDecoder(req.Body) 93 | 94 | resp.Header().Set("Content-Type", "application/json; charset=UTF-8") 95 | 96 | if err := decoder.Decode(&op); err != nil { 97 | log.Println(ERROR, "api/operator:", err) 98 | 99 | resp.WriteHeader(http.StatusBadRequest) 100 | 101 | fmt.Fprintf(resp, "Bad Request") 102 | return 103 | } 104 | 105 | if id == "" { // Create 106 | 107 | log.Println(DEBUG, "api/operator:", "Creating new operator", op) 108 | 109 | // Save new record 110 | ds.Put(op) 111 | 112 | } else { // Update 113 | 114 | // Get old record 115 | result, _ := ds.Get(fmt.Sprintf("operator.%s", id)) 116 | 117 | if result == nil { 118 | resp.WriteHeader(http.StatusNotFound) 119 | 120 | fmt.Fprintf(resp, "Not Found") 121 | return 122 | } 123 | 124 | if old, ok := result.(*Operator); ok { 125 | // Update fields of old record 126 | 127 | if op.GetAid() != "" { 128 | old.Aid = op.Aid 129 | } 130 | 131 | if op.GetAtoken() != "" { 132 | old.Atoken = op.Atoken 133 | } 134 | 135 | if op.GetAvatar() != "" { 136 | old.Avatar = op.Avatar 137 | } 138 | 139 | if op.GetFirstName() != "" { 140 | old.FirstName = op.FirstName 141 | } 142 | 143 | if op.GetLastName() != "" { 144 | old.LastName = op.LastName 145 | } 146 | 147 | log.Println(DEBUG, "api/operator:", "Updating operator", old) 148 | 149 | // Save old record 150 | ds.Put(old) 151 | } 152 | } 153 | 154 | resp.WriteHeader(http.StatusOK) 155 | } 156 | } 157 | 158 | /* 159 | DELETE /api/operator/:id */ 160 | func deleteOperator(ds *store.InMemory) func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 161 | return func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 162 | id := params.ByName("id") 163 | err := ds.Remove(fmt.Sprintf("operator.%s", id)) 164 | 165 | log.Println(DEBUG, "api/operator:", "Removing operator", id) 166 | 167 | resp.Header().Set("Content-Type", "application/json; charset=UTF-8") 168 | if err != nil { 169 | log.Println(ERROR, "api/operator:", err) 170 | 171 | resp.WriteHeader(http.StatusBadRequest) 172 | 173 | fmt.Fprintf(resp, "Bad Request") 174 | return 175 | } 176 | 177 | resp.WriteHeader(http.StatusOK) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /pkg/api/person/person.go: -------------------------------------------------------------------------------- 1 | package person 2 | 3 | /* 4 | Person interface defines a common function GetFullName that is required for 5 | anything to be considered a Person */ 6 | type Person interface { 7 | GetFullName() string 8 | } 9 | -------------------------------------------------------------------------------- /pkg/api/webhook/event.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/golang-plus/uuid" // UUID (RFC 4122) 9 | "github.com/golang/protobuf/jsonpb" 10 | timestamp "github.com/golang/protobuf/ptypes/timestamp" 11 | ) 12 | 13 | // Event Types 14 | const ( 15 | // EVENT_NEW_MESSAGE string = "chat:new_message" 16 | EventNewChat string = "chat:new" 17 | EventNewOperator string = "operator:new" 18 | // EVENT_NEW_OPERATOR_MESSAGE string = "operator:new_message" 19 | EventNewClient string = "client:new" 20 | EventNewClientMessage string = "client:message" 21 | ) 22 | 23 | /* 24 | CreateEvent a new Event object based on a given Type (t) string */ 25 | func CreateEvent(t string) *Event { 26 | e := Event{ 27 | Type: t, 28 | } 29 | 30 | // Generate ID 31 | uuid, _ := uuid.NewRandom() 32 | e.Id = uuid.String() 33 | 34 | // Set Create/Update timestamps 35 | now := time.Now() 36 | seconds := now.Unix() 37 | nanos := int32(now.Sub(time.Unix(seconds, 0))) 38 | 39 | ts := ×tamp.Timestamp{ 40 | Seconds: seconds, 41 | Nanos: nanos, 42 | } 43 | 44 | e.CreationTime = ts 45 | 46 | return &e 47 | } 48 | 49 | /* 50 | UnmarshalJSON converts a JSON string (as a byte array) into a Event object. */ 51 | func (e *Event) UnmarshalJSON(data []byte) error { 52 | u := jsonpb.Unmarshaler{} 53 | buf := bytes.NewBuffer(data) 54 | 55 | return u.Unmarshal(buf, &*e) 56 | } 57 | 58 | /* 59 | MarshalJSON converts a Event object into a JSON string returned as a byte 60 | array. */ 61 | func (e Event) MarshalJSON() ([]byte, error) { 62 | var buf bytes.Buffer 63 | 64 | m := jsonpb.Marshaler{} 65 | 66 | if err := m.Marshal(&buf, &e); err != nil { 67 | return nil, err 68 | } 69 | 70 | return buf.Bytes(), nil 71 | } 72 | 73 | /* 74 | Key implements the Keyer interface of the Store and returns a string used for 75 | storing the Webhook in memory. */ 76 | func (e Event) Key() string { 77 | return fmt.Sprintf("event.%d.%s", e.CreationTime.GetSeconds(), e.Id) 78 | } 79 | -------------------------------------------------------------------------------- /pkg/api/webhook/router.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "time" 9 | 10 | timestamp "github.com/golang/protobuf/ptypes/timestamp" 11 | "github.com/julienschmidt/httprouter" // Router 12 | 13 | "github.com/minimalchat/daemon/pkg/store" 14 | ) 15 | 16 | // Log levels 17 | const ( 18 | DEBUG string = "DEBUG" 19 | INFO string = "INFO" 20 | WARNING string = "WARN" 21 | ERROR string = "ERROR" 22 | FATAL string = "FATAL" 23 | ) 24 | 25 | /* 26 | Routes defines the Webhook API CRUD uris */ 27 | func Routes(router *httprouter.Router, ds *store.InMemory) { 28 | 29 | // Operator 30 | // Read 31 | // router.GET("/api/operators", readOperators(ds)) // Check 32 | // router.GET("/api/operator", readOperators(ds)) 33 | router.GET("/api/webhooks", readWebhooks(ds)) 34 | router.GET("/api/webhook", readWebhooks(ds)) 35 | 36 | router.GET("/api/webhook/:id", readWebhooks(ds)) 37 | 38 | // router.GET("/api/operator/:id", readOperator(ds)) // Check 39 | 40 | // Create / Update 41 | // router.POST("/api/operator", createOrUpdateOperator(ds)) // Check 42 | // router.POST("/api/operator/:id", createOrUpdateOperator(ds)) // Check 43 | // router.PUT("/api/operator/", createOrUpdateOperator(ds)) // Check 44 | // router.PATCH("/api/operator/:id", createOrUpdateOperator(ds)) // Check 45 | router.POST("/api/webhook", createOrUpdateWebhook(ds)) 46 | router.POST("/api/webhook/:id", createOrUpdateWebhook(ds)) 47 | router.PUT("/api/webhook/", createOrUpdateWebhook(ds)) 48 | router.PATCH("/api/webhook/:id", createOrUpdateWebhook(ds)) 49 | 50 | // Delete 51 | // router.DELETE("/api/operator/:id", deleteOperator(ds)) // Check 52 | router.DELETE("/api/webhook/:id", deleteWebhook(ds)) 53 | } 54 | 55 | /* 56 | GET /api/webhook 57 | GET /api/webhooks 58 | GET /api/webhook/:id */ 59 | func readWebhooks(ds *store.InMemory) func(http.ResponseWriter, *http.Request, httprouter.Params) { 60 | return func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 61 | id := params.ByName("id") 62 | 63 | if id == "" { 64 | ws, _ := ds.Search("webhook.") 65 | result := make(map[string]interface{}) 66 | 67 | result["webhooks"] = ws 68 | 69 | log.Println(INFO, "api/webhook:", "Reading webhooks", fmt.Sprintf("(%d records)", len(ws))) 70 | 71 | resp.Header().Set("Content-Type", "application/json; charset=UTF-8") 72 | resp.WriteHeader(http.StatusOK) 73 | if err := json.NewEncoder(resp).Encode(result); err != nil { 74 | panic(err) 75 | } 76 | } else { 77 | w, _ := ds.Get(fmt.Sprintf("webhook.%s", id)) 78 | 79 | log.Println(DEBUG, "api/webhook:", "Reading webhook", id) 80 | 81 | resp.Header().Set("Content-Type", "application/json; charset=UTF-8") 82 | if w != nil { 83 | resp.WriteHeader(http.StatusOK) 84 | if err := json.NewEncoder(resp).Encode(w); err != nil { 85 | panic(err) 86 | } 87 | } else { 88 | resp.WriteHeader(http.StatusNotFound) 89 | 90 | fmt.Fprintf(resp, "Not Found") 91 | } 92 | } 93 | } 94 | } 95 | 96 | /* 97 | POST /api/webhook/ 98 | POST /api/webhook/:id 99 | PUT /api/webhook/ 100 | PATCH /api/webhook/:id */ 101 | func createOrUpdateWebhook(ds *store.InMemory) func(http.ResponseWriter, *http.Request, httprouter.Params) { 102 | return func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 103 | var w *Webhook 104 | 105 | id := params.ByName("id") 106 | decoder := json.NewDecoder(req.Body) 107 | 108 | resp.Header().Set("Content-Type", "application/json; charset=UTF-8") 109 | 110 | if err := decoder.Decode(&w); err != nil { 111 | log.Println(ERROR, "api/webhook:", err) 112 | 113 | resp.WriteHeader(http.StatusBadRequest) 114 | 115 | fmt.Fprintf(resp, "Bad Request") 116 | return 117 | } 118 | 119 | if id == "" { // Create 120 | 121 | log.Println(DEBUG, "api/webhook:", "Creating new webhook", w) 122 | 123 | // Use our creation function to generate an ID and secret 124 | wh := CreateWebhook(w.Uri, w.EventTypes) 125 | 126 | if w.GetSecret() != "" { 127 | wh.Secret = w.Secret 128 | } 129 | 130 | // Save new record 131 | ds.Put(wh) 132 | 133 | } else { // Update 134 | 135 | // Get old record 136 | result, _ := ds.Get(fmt.Sprintf("webhook.%s", id)) 137 | 138 | if result == nil { 139 | resp.WriteHeader(http.StatusNotFound) 140 | 141 | fmt.Fprintf(resp, "Not Found") 142 | return 143 | } 144 | 145 | if old, ok := result.(*Webhook); ok { 146 | // Update fields of old record 147 | 148 | if w.GetUri() != "" { 149 | old.Uri = w.Uri 150 | } 151 | 152 | now := time.Now() 153 | seconds := now.Unix() 154 | nanos := int32(now.Sub(time.Unix(seconds, 0))) 155 | 156 | old.UpdatedTime = ×tamp.Timestamp{ 157 | Seconds: seconds, 158 | Nanos: nanos, 159 | } 160 | 161 | log.Println(DEBUG, "api/webhook:", "Updating webhook", old) 162 | 163 | // Save old record 164 | ds.Put(old) 165 | } 166 | } 167 | 168 | resp.WriteHeader(http.StatusOK) 169 | } 170 | } 171 | 172 | /* 173 | DELETE /api/webhook/:id */ 174 | func deleteWebhook(ds *store.InMemory) func(http.ResponseWriter, *http.Request, httprouter.Params) { 175 | return func(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 176 | id := params.ByName("id") 177 | err := ds.Remove(fmt.Sprintf("webhook.%s", id)) 178 | 179 | log.Println(DEBUG, "api/operator:", "Removing operator", id) 180 | 181 | resp.Header().Set("Content-Type", "application/json; charset=UTF-8") 182 | if err != nil { 183 | log.Println(ERROR, "api/webhook:", err) 184 | 185 | resp.WriteHeader(http.StatusBadRequest) 186 | 187 | fmt.Fprintf(resp, "Bad Request") 188 | return 189 | } 190 | 191 | resp.WriteHeader(http.StatusOK) 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /pkg/api/webhook/webhook.go: -------------------------------------------------------------------------------- 1 | package webhook 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | // "errors" 8 | "encoding/json" 9 | "fmt" 10 | "log" 11 | "net/http" 12 | "strings" 13 | "time" 14 | 15 | "github.com/golang-plus/uuid" // UUID (RFC 4122) 16 | "github.com/golang/protobuf/jsonpb" 17 | timestamp "github.com/golang/protobuf/ptypes/timestamp" 18 | 19 | "github.com/minimalchat/daemon/pkg/store" 20 | ) 21 | 22 | /* 23 | CreateWebhook a new Webhook object based on the given event strings. */ 24 | func CreateWebhook(uri string, events []string) *Webhook { 25 | w := Webhook{ 26 | EventTypes: events, 27 | Uri: uri, 28 | Enabled: true, 29 | } 30 | 31 | // Generate ID 32 | id, _ := uuid.NewRandom() 33 | w.Id = id.String() // fmt.Sprintf("wh-%s", id.String()) 34 | 35 | // // Generate Secret 36 | // h := sha256.New() 37 | // h.Write([]byte(w.Id)) 38 | 39 | t := time.Now() 40 | // b := make([]byte, 8) 41 | // binary.LittleEndian.PutUint64(b, uint64(t.Unix())) 42 | // h.Write(b) 43 | 44 | // h.Write([]byte(salt)) 45 | 46 | secret, _ := uuid.NewRandom() 47 | w.Secret = secret.String() // fmt.Sprintf("whsec-%s", secret.String()) 48 | 49 | // w.Secret = fmt.Sprintf("%x", h.Sum(nil)) 50 | 51 | // Set Create/Update timestamps 52 | seconds := t.Unix() 53 | nanos := int32(t.Sub(time.Unix(seconds, 0))) 54 | 55 | ts := ×tamp.Timestamp{ 56 | Seconds: seconds, 57 | Nanos: nanos, 58 | } 59 | 60 | w.CreationTime = ts 61 | w.UpdatedTime = ts 62 | 63 | return &w 64 | } 65 | 66 | /* 67 | GetByEventType returns one or more Webhooks that are for a specified event */ 68 | func GetByEventType(ds *store.InMemory, event string) ([]*Webhook, error) { 69 | var result []*Webhook 70 | // TODO: Figure out if this is the best place for this function 71 | 72 | // Get all webhooks 73 | ws, err := ds.Search("webhook.") 74 | if err != nil { 75 | return nil, err 76 | } 77 | 78 | // Iterate over each and check its EventTypes to see if there is a match 79 | // with event 80 | for i := 0; i < len(ws); i++ { 81 | eventTypes := ws[i].(*Webhook).EventTypes 82 | 83 | // TODO: Not sure if this is more/less expensive than looping 84 | // through each event type individually and doing a comparison. 85 | eventTypeString := strings.Join(eventTypes, "") 86 | 87 | if strings.Contains(eventTypeString, event) { 88 | result = append(result, ws[i].(*Webhook)) 89 | } 90 | } 91 | 92 | return result, nil 93 | // return nil, errors.New("not implemented") 94 | } 95 | 96 | /* 97 | Run executes the Webhook sending an HTTP request with an Event to the defined 98 | URI. We generate a Mnml-Signature header that looks something along the lines 99 | of: 100 | 101 | Mnml-Signature: t=1492774577, 102 | v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd 103 | 104 | Where t is the unix timestamp in seconds, and v1 is an HMAC signature using 105 | SHA-256. This can be used to validate that the signature is coming from the 106 | daemon service and not anywhere else. 107 | 108 | To manually verify the signature, take t and concat it with the JSON payload 109 | received, separated by a '.' dot. Use the Webhook's secret to sign the 110 | concatenated timestamp.payload and it should equal v1. */ 111 | func (w *Webhook) Run(t string, d []byte, n string) error { 112 | c := &http.Client{} 113 | 114 | e := CreateEvent(t) 115 | e.Data = string(d) 116 | e.SourceId = n 117 | 118 | b, err := json.Marshal(e) 119 | if err != nil { 120 | return err 121 | } 122 | 123 | req, err := http.NewRequest("POST", w.Uri, bytes.NewBuffer(b)) 124 | req.Header.Set("Content-Type", "application/json") 125 | 126 | sigPayload := fmt.Sprintf("%d.%s", e.CreationTime.GetSeconds(), b) 127 | 128 | // Generate Signature based off of the webhook secret 129 | hmac := hmac.New(sha256.New, []byte(w.Secret)) 130 | hmac.Write([]byte(sigPayload)) 131 | 132 | sig := fmt.Sprintf("t=%d,v1=%x", e.CreationTime.GetSeconds(), hmac.Sum(nil)) 133 | 134 | req.Header.Set("Mnml-Signature", sig) 135 | 136 | resp, err := c.Do(req) 137 | 138 | // TODO: Do something with the response, record the request? 139 | if resp != nil { 140 | log.Println(DEBUG, "webhook:", fmt.Sprintf("Sent event '%s' to %s and got: %v", t, w.Uri, resp.Status)) 141 | } 142 | 143 | return err 144 | } 145 | 146 | /* 147 | UnmarshalJSON converts a JSON string (as a byte array) into a Webhook object. */ 148 | func (w *Webhook) UnmarshalJSON(data []byte) error { 149 | u := jsonpb.Unmarshaler{} 150 | buf := bytes.NewBuffer(data) 151 | 152 | return u.Unmarshal(buf, &*w) 153 | } 154 | 155 | /* 156 | MarshalJSON converts a Webhook object into a JSON string returned as a byte 157 | array. */ 158 | func (w Webhook) MarshalJSON() ([]byte, error) { 159 | var buf bytes.Buffer 160 | 161 | m := jsonpb.Marshaler{} 162 | 163 | if err := m.Marshal(&buf, &w); err != nil { 164 | return nil, err 165 | } 166 | 167 | return buf.Bytes(), nil 168 | } 169 | 170 | /* 171 | Key implements the Keyer interface of the Store and returns a string used for 172 | storing the Webhook in memory. */ 173 | func (w Webhook) Key() string { 174 | return fmt.Sprintf("webhook.%s", w.Id) 175 | } 176 | -------------------------------------------------------------------------------- /pkg/api/webhook/webhook.pb.go: -------------------------------------------------------------------------------- 1 | // Code generated by protoc-gen-go. DO NOT EDIT. 2 | // source: pkg/api/webhook/webhook.proto 3 | 4 | package webhook 5 | 6 | import proto "github.com/golang/protobuf/proto" 7 | import fmt "fmt" 8 | import math "math" 9 | import timestamp "github.com/golang/protobuf/ptypes/timestamp" 10 | 11 | // Reference imports to suppress errors if they are not otherwise used. 12 | var _ = proto.Marshal 13 | var _ = fmt.Errorf 14 | var _ = math.Inf 15 | 16 | // This is a compile-time assertion to ensure that this generated file 17 | // is compatible with the proto package it is being compiled against. 18 | // A compilation error at this line likely means your copy of the 19 | // proto package needs to be updated. 20 | const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package 21 | 22 | type Event struct { 23 | Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` 24 | Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` 25 | // JSON string of data 26 | Data string `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` 27 | SourceId string `protobuf:"bytes,4,opt,name=source_id,proto3" json:"source_id,omitempty"` 28 | CreationTime *timestamp.Timestamp `protobuf:"bytes,5,opt,name=creation_time,proto3" json:"creation_time,omitempty"` 29 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 30 | XXX_unrecognized []byte `json:"-"` 31 | XXX_sizecache int32 `json:"-"` 32 | } 33 | 34 | func (m *Event) Reset() { *m = Event{} } 35 | func (m *Event) String() string { return proto.CompactTextString(m) } 36 | func (*Event) ProtoMessage() {} 37 | func (*Event) Descriptor() ([]byte, []int) { 38 | return fileDescriptor_webhook_dedb22006b451bde, []int{0} 39 | } 40 | func (m *Event) XXX_Unmarshal(b []byte) error { 41 | return xxx_messageInfo_Event.Unmarshal(m, b) 42 | } 43 | func (m *Event) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 44 | return xxx_messageInfo_Event.Marshal(b, m, deterministic) 45 | } 46 | func (dst *Event) XXX_Merge(src proto.Message) { 47 | xxx_messageInfo_Event.Merge(dst, src) 48 | } 49 | func (m *Event) XXX_Size() int { 50 | return xxx_messageInfo_Event.Size(m) 51 | } 52 | func (m *Event) XXX_DiscardUnknown() { 53 | xxx_messageInfo_Event.DiscardUnknown(m) 54 | } 55 | 56 | var xxx_messageInfo_Event proto.InternalMessageInfo 57 | 58 | func (m *Event) GetId() string { 59 | if m != nil { 60 | return m.Id 61 | } 62 | return "" 63 | } 64 | 65 | func (m *Event) GetType() string { 66 | if m != nil { 67 | return m.Type 68 | } 69 | return "" 70 | } 71 | 72 | func (m *Event) GetData() string { 73 | if m != nil { 74 | return m.Data 75 | } 76 | return "" 77 | } 78 | 79 | func (m *Event) GetSourceId() string { 80 | if m != nil { 81 | return m.SourceId 82 | } 83 | return "" 84 | } 85 | 86 | func (m *Event) GetCreationTime() *timestamp.Timestamp { 87 | if m != nil { 88 | return m.CreationTime 89 | } 90 | return nil 91 | } 92 | 93 | type Webhook struct { 94 | Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` 95 | EventTypes []string `protobuf:"bytes,2,rep,name=event_types,proto3" json:"event_types,omitempty"` 96 | Enabled bool `protobuf:"varint,3,opt,name=enabled,proto3" json:"enabled,omitempty"` 97 | Secret string `protobuf:"bytes,4,opt,name=secret,proto3" json:"secret,omitempty"` 98 | Uri string `protobuf:"bytes,5,opt,name=uri,proto3" json:"uri,omitempty"` 99 | CreationTime *timestamp.Timestamp `protobuf:"bytes,6,opt,name=creation_time,proto3" json:"creation_time,omitempty"` 100 | UpdatedTime *timestamp.Timestamp `protobuf:"bytes,7,opt,name=updated_time,json=update_time,proto3" json:"updated_time,omitempty"` 101 | XXX_NoUnkeyedLiteral struct{} `json:"-"` 102 | XXX_unrecognized []byte `json:"-"` 103 | XXX_sizecache int32 `json:"-"` 104 | } 105 | 106 | func (m *Webhook) Reset() { *m = Webhook{} } 107 | func (m *Webhook) String() string { return proto.CompactTextString(m) } 108 | func (*Webhook) ProtoMessage() {} 109 | func (*Webhook) Descriptor() ([]byte, []int) { 110 | return fileDescriptor_webhook_dedb22006b451bde, []int{1} 111 | } 112 | func (m *Webhook) XXX_Unmarshal(b []byte) error { 113 | return xxx_messageInfo_Webhook.Unmarshal(m, b) 114 | } 115 | func (m *Webhook) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { 116 | return xxx_messageInfo_Webhook.Marshal(b, m, deterministic) 117 | } 118 | func (dst *Webhook) XXX_Merge(src proto.Message) { 119 | xxx_messageInfo_Webhook.Merge(dst, src) 120 | } 121 | func (m *Webhook) XXX_Size() int { 122 | return xxx_messageInfo_Webhook.Size(m) 123 | } 124 | func (m *Webhook) XXX_DiscardUnknown() { 125 | xxx_messageInfo_Webhook.DiscardUnknown(m) 126 | } 127 | 128 | var xxx_messageInfo_Webhook proto.InternalMessageInfo 129 | 130 | func (m *Webhook) GetId() string { 131 | if m != nil { 132 | return m.Id 133 | } 134 | return "" 135 | } 136 | 137 | func (m *Webhook) GetEventTypes() []string { 138 | if m != nil { 139 | return m.EventTypes 140 | } 141 | return nil 142 | } 143 | 144 | func (m *Webhook) GetEnabled() bool { 145 | if m != nil { 146 | return m.Enabled 147 | } 148 | return false 149 | } 150 | 151 | func (m *Webhook) GetSecret() string { 152 | if m != nil { 153 | return m.Secret 154 | } 155 | return "" 156 | } 157 | 158 | func (m *Webhook) GetUri() string { 159 | if m != nil { 160 | return m.Uri 161 | } 162 | return "" 163 | } 164 | 165 | func (m *Webhook) GetCreationTime() *timestamp.Timestamp { 166 | if m != nil { 167 | return m.CreationTime 168 | } 169 | return nil 170 | } 171 | 172 | func (m *Webhook) GetUpdatedTime() *timestamp.Timestamp { 173 | if m != nil { 174 | return m.UpdatedTime 175 | } 176 | return nil 177 | } 178 | 179 | func init() { 180 | proto.RegisterType((*Event)(nil), "webhook.Event") 181 | proto.RegisterType((*Webhook)(nil), "webhook.Webhook") 182 | } 183 | 184 | func init() { 185 | proto.RegisterFile("pkg/api/webhook/webhook.proto", fileDescriptor_webhook_dedb22006b451bde) 186 | } 187 | 188 | var fileDescriptor_webhook_dedb22006b451bde = []byte{ 189 | // 280 bytes of a gzipped FileDescriptorProto 190 | 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x94, 0x8f, 0xc1, 0x4a, 0xf4, 0x30, 191 | 0x10, 0xc7, 0x69, 0xbb, 0xdb, 0x7e, 0x9d, 0x7e, 0x8a, 0xe4, 0x20, 0x61, 0x51, 0x2c, 0x7b, 0xea, 192 | 0xa9, 0x05, 0x3d, 0x0b, 0x5e, 0x7c, 0x81, 0x22, 0x78, 0x2c, 0x69, 0x33, 0xd6, 0xb0, 0xbb, 0x4d, 193 | 0x48, 0x53, 0xc5, 0x9b, 0x6f, 0xe2, 0xab, 0x4a, 0x92, 0x16, 0x95, 0x3d, 0x88, 0xa7, 0xcc, 0xfc, 194 | 0xf2, 0xcf, 0xe4, 0x37, 0x70, 0xa9, 0x76, 0x7d, 0xc5, 0x94, 0xa8, 0x5e, 0xb1, 0x7d, 0x96, 0x72, 195 | 0xb7, 0x9c, 0xa5, 0xd2, 0xd2, 0x48, 0x92, 0xcc, 0xed, 0xe6, 0xaa, 0x97, 0xb2, 0xdf, 0x63, 0xe5, 196 | 0x70, 0x3b, 0x3d, 0x55, 0x46, 0x1c, 0x70, 0x34, 0xec, 0xa0, 0x7c, 0x72, 0xfb, 0x11, 0xc0, 0xfa, 197 | 0xfe, 0x05, 0x07, 0x43, 0x4e, 0x21, 0x14, 0x9c, 0x06, 0x79, 0x50, 0xa4, 0x75, 0x28, 0x38, 0x21, 198 | 0xb0, 0x32, 0x6f, 0x0a, 0x69, 0xe8, 0x88, 0xab, 0x2d, 0xe3, 0xcc, 0x30, 0x1a, 0x79, 0x66, 0x6b, 199 | 0x72, 0x01, 0xe9, 0x28, 0x27, 0xdd, 0x61, 0x23, 0x38, 0x5d, 0xb9, 0x8b, 0x2f, 0x40, 0xee, 0xe0, 200 | 0xa4, 0xd3, 0xc8, 0x8c, 0x90, 0x43, 0x63, 0xff, 0xa6, 0xeb, 0x3c, 0x28, 0xb2, 0xeb, 0x4d, 0xe9, 201 | 0xc5, 0xca, 0x45, 0xac, 0x7c, 0x58, 0xc4, 0xea, 0x9f, 0x0f, 0xb6, 0xef, 0x21, 0x24, 0x8f, 0x7e, 202 | 0x9d, 0x23, 0xc7, 0x1c, 0x32, 0xb4, 0xf2, 0x8d, 0xb5, 0x1b, 0x69, 0x98, 0x47, 0x45, 0x5a, 0x7f, 203 | 0x47, 0x84, 0x42, 0x82, 0x03, 0x6b, 0xf7, 0xc8, 0x9d, 0xf4, 0xbf, 0x7a, 0x69, 0xc9, 0x39, 0xc4, 204 | 0x23, 0x76, 0x1a, 0xcd, 0x2c, 0x3d, 0x77, 0xe4, 0x0c, 0xa2, 0x49, 0x0b, 0xe7, 0x99, 0xd6, 0xb6, 205 | 0x3c, 0xde, 0x21, 0xfe, 0xe3, 0x0e, 0xe4, 0x16, 0xfe, 0x4f, 0x8a, 0x33, 0x83, 0xdc, 0x0f, 0x48, 206 | 0x7e, 0x1d, 0x90, 0xf9, 0xbc, 0x8b, 0xb7, 0xb1, 0x0b, 0xdc, 0x7c, 0x06, 0x00, 0x00, 0xff, 0xff, 207 | 0x79, 0x10, 0x6a, 0xa5, 0xf6, 0x01, 0x00, 0x00, 208 | } 209 | -------------------------------------------------------------------------------- /pkg/api/webhook/webhook.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | package webhook; 3 | 4 | import "google/protobuf/timestamp.proto"; 5 | // import "pkg/api/client/client.proto"; 6 | // import "pkg/api/operator/operator.proto"; 7 | // import "pkg/api/chat/chat.proto"; 8 | 9 | message Event { 10 | string id = 1; 11 | string type = 2; 12 | // JSON string of data 13 | string data = 3; 14 | string source_id = 4 [json_name="source_id"]; 15 | google.protobuf.Timestamp creation_time = 5 [json_name="creation_time"]; 16 | } 17 | 18 | message Webhook { 19 | string id = 1; 20 | repeated string event_types = 2 [json_name="event_types"]; 21 | bool enabled = 3; 22 | string secret = 4; 23 | string uri = 5; 24 | 25 | google.protobuf.Timestamp creation_time = 6 [json_name="creation_time"]; 26 | google.protobuf.Timestamp updated_time = 7 [json_name="update_time"]; 27 | } 28 | -------------------------------------------------------------------------------- /pkg/server/rest/server.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "regexp" 8 | "strings" 9 | 10 | "github.com/julienschmidt/httprouter" // Http router 11 | 12 | "github.com/minimalchat/daemon/pkg/api/chat" 13 | "github.com/minimalchat/daemon/pkg/api/client" 14 | "github.com/minimalchat/daemon/pkg/api/operator" 15 | "github.com/minimalchat/daemon/pkg/api/webhook" 16 | "github.com/minimalchat/daemon/pkg/server/socket" 17 | "github.com/minimalchat/daemon/pkg/store" // InMemory store 18 | ) 19 | 20 | // Log levels 21 | const ( 22 | DEBUG string = "DEBUG" 23 | INFO string = "INFO" 24 | WARNING string = "WARN" 25 | ERROR string = "ERROR" 26 | FATAL string = "FATAL" 27 | ) 28 | 29 | /* 30 | Server is the REST API server for Minimal Chat */ 31 | type Server struct { 32 | Router *httprouter.Router 33 | Config 34 | } 35 | 36 | /* 37 | Config holds all the necessary configuration for our REST API server */ 38 | type Config struct { 39 | Protocol string 40 | Port string 41 | Host string 42 | 43 | ID string 44 | 45 | SSLCertFile string 46 | SSLKeyFile string 47 | SSLPort int 48 | 49 | CORSOrigins string 50 | CORSEnabled bool 51 | } 52 | 53 | /* 54 | Initialize takes a Store and ServerConfig starts listening on port and host 55 | provided by a ServerConfig */ 56 | func Initialize(ds *store.InMemory, c Config) *Server { 57 | s := Server{ 58 | Router: httprouter.New(), 59 | Config: c, 60 | } 61 | 62 | if s.Config.CORSEnabled { 63 | log.Println(DEBUG, "server:", fmt.Sprintf("Setting CORS origin to %s", c.CORSOrigins)) 64 | } 65 | 66 | // 404 67 | s.Router.NotFound = http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 68 | resp.Header().Set("Content-Type", "text/plain; charset=UTF-8") 69 | resp.WriteHeader(http.StatusNotFound) 70 | 71 | fmt.Fprintf(resp, "Not Found") 72 | }) 73 | 74 | // 405 75 | s.Router.HandleMethodNotAllowed = true 76 | s.Router.MethodNotAllowed = http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 77 | resp.Header().Set("Content-Type", "text/plain; charset=UTF-8") 78 | resp.WriteHeader(http.StatusMethodNotAllowed) 79 | 80 | fmt.Fprintf(resp, "Method Not Allowed") 81 | }) 82 | 83 | // Default Routes 84 | s.Router.GET("/", defaultRedirectRoute) 85 | s.Router.GET("/api", defaultRedirectRoute) 86 | s.Router.GET("/api/", defaultRoute) 87 | 88 | // Socket.io 89 | sock, err := socket.Create(ds) 90 | sock.ID = c.ID 91 | 92 | if err != nil { 93 | log.Fatal(err) 94 | } 95 | 96 | go sock.Listen() 97 | 98 | s.Router.HandlerFunc("GET", "/socket.io/", func(w http.ResponseWriter, r *http.Request) { 99 | if s.Config.CORSEnabled { 100 | regx := regexp.MustCompile(`https?:\/\/`) 101 | pro := r.Header.Get("Origin") 102 | ro := regx.ReplaceAllString(pro, "") 103 | if len(ro) > 0 { 104 | log.Println(DEBUG, "server:", fmt.Sprintf("Comparing incoming request host %s, with CORS Origins (%s)", ro, s.Config.CORSOrigins)) 105 | po := strings.Split(s.Config.CORSOrigins, ",") 106 | for i := 0; i < len(po); i++ { 107 | o := regx.ReplaceAllString(po[i], "") 108 | if strings.Contains(strings.Trim(ro, " "), strings.Trim(o, " ")) { 109 | log.Println(DEBUG, "server:", fmt.Sprintf("Sending CORS Access-Control-Allow-Origin for %s", po[i])) 110 | w.Header().Set("Access-Control-Allow-Origin", pro) 111 | 112 | break 113 | } 114 | } 115 | } 116 | w.Header().Set("Access-Control-Allow-Credentials", "true") 117 | // resp.Header().Set("Access-Control-Allow-Headers", "X-Socket-Type") 118 | } 119 | 120 | sock.ServeHTTP(w, r) 121 | }) 122 | 123 | // Operators API 124 | operator.Routes(s.Router, ds) 125 | 126 | // Clients API 127 | client.Routes(s.Router, ds) 128 | 129 | // Chats API 130 | chat.Routes(s.Router, ds) 131 | 132 | // Webhook API 133 | webhook.Routes(s.Router, ds) 134 | 135 | return &s 136 | } 137 | 138 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 139 | if s.Config.CORSEnabled { 140 | regx := regexp.MustCompile(`https?:\/\/`) 141 | pro := r.Header.Get("Origin") 142 | ro := regx.ReplaceAllString(pro, "") 143 | if len(ro) > 0 { 144 | log.Println(DEBUG, "server:", fmt.Sprintf("Comparing incoming request host %s, with CORS Origins (%s)", ro, s.Config.CORSOrigins)) 145 | po := strings.Split(s.Config.CORSOrigins, ",") 146 | for i := 0; i < len(po); i++ { 147 | o := regx.ReplaceAllString(po[i], "") 148 | if strings.Contains(strings.Trim(ro, " "), strings.Trim(o, " ")) { 149 | log.Println(DEBUG, "server:", fmt.Sprintf("Sending CORS Access-Control-Allow-Origin for %s", po[i])) 150 | w.Header().Set("Access-Control-Allow-Origin", pro) 151 | break 152 | } 153 | } 154 | } 155 | w.Header().Set("Access-Control-Allow-Credentials", "true") 156 | // resp.Header().Set("Access-Control-Allow-Headers", "X-Socket-Type") 157 | } 158 | 159 | s.Router.ServeHTTP(w, r) 160 | } 161 | 162 | // GET / 163 | func defaultRedirectRoute(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 164 | // resp.Header().Set("Content-Type", "text/html; charset=UTF-8") 165 | // resp.WriteHeader(http.StatusMovedPermanently) 166 | http.Redirect(resp, req, "/api/", http.StatusMovedPermanently) 167 | } 168 | 169 | // GET /api/ 170 | func defaultRoute(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { 171 | resp.Header().Set("Content-Type", "application/json; charset=UTF-8") 172 | resp.WriteHeader(http.StatusOK) 173 | // TODO: Make this less hacky? 174 | fmt.Fprint(resp, "{\"clients\": \"/api/clients\", \"client\": \"/api/client/:id\", \"chats\":\"/api/chats\", \"chat\":\"/api/chat/:id\", \"messages\":\"/api/chat/:id/messages\", \"message\":\"/api/chat/:id/message/:mid\", \"operators\":\"/api/operators\", \"operators\":\"/api/operators\", \"operator\":\"/api/operator/:id\", \"webhooks\":\"/api/webhooks\", \"webhook\":\"/api/webhook/:id\"}") 175 | } 176 | -------------------------------------------------------------------------------- /pkg/server/socket/client.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | 9 | // TODO: Move away from this socket library, it is no longer maintained 10 | "github.com/minimalchat/go-socket.io" // Socket 11 | // "github.com/googollee/go-socket.io" // Socket 12 | 13 | "github.com/minimalchat/daemon/pkg/api/chat" 14 | "github.com/minimalchat/daemon/pkg/api/client" 15 | "github.com/minimalchat/daemon/pkg/api/operator" 16 | "github.com/minimalchat/daemon/pkg/api/webhook" 17 | "github.com/minimalchat/daemon/pkg/store" // InMemory Database 18 | ) 19 | 20 | /* 21 | Category defines the type of socket connection. */ 22 | type Category string 23 | 24 | /* 25 | Categories currently allowed */ 26 | const ( 27 | OPERATOR Category = "operator" 28 | CLIENT Category = "client" 29 | ) 30 | 31 | /* 32 | Conn holds the basic information for a socket connection. */ 33 | type Conn struct { 34 | server *Server 35 | 36 | // Socketio connection 37 | raw socketio.Socket 38 | category Category 39 | 40 | send chan *Message 41 | } 42 | 43 | /* 44 | Message provides all the details to send or receive a message from a socket 45 | connection. */ 46 | type Message struct { 47 | event string 48 | message string 49 | target string 50 | } 51 | 52 | /* 53 | Listen waits for an incoming message to send out through the connection. */ 54 | func (c Conn) Listen() { 55 | // Defer closing the socket 56 | defer func() { 57 | log.Println(DEBUG, "socket:", fmt.Sprintf("%s disconnected", c.raw.Id())) 58 | 59 | c.raw.Disconnect() 60 | }() 61 | 62 | // Listen for send channel messages and emit them 63 | for { 64 | select { 65 | case data, ok := <-c.send: 66 | if !ok { 67 | log.Println(WARNING, "socket:", fmt.Sprintf("Server closed %s channel", c.raw.Id())) 68 | return 69 | } 70 | 71 | if data.message == "" { 72 | log.Println(DEBUG, "socket:", fmt.Sprintf("Emitting '%s' to %s", data.event, c.raw.Id())) 73 | 74 | } else { 75 | log.Println(DEBUG, "socket:", fmt.Sprintf("Emitting '%s' to %s '%s'", data.event, c.raw.Id(), data.message)) 76 | } 77 | 78 | c.raw.Emit(data.event, data.message) 79 | } 80 | } 81 | } 82 | 83 | func (c Conn) runWebhooks(e []string, d []byte) { 84 | for i := 0; i < len(e); i++ { 85 | w, err := webhook.GetByEventType(c.server.store, e[i]) 86 | if err != nil { 87 | log.Println(WARNING, "webhooks:", err) 88 | continue 89 | } else { 90 | log.Println(DEBUG, "webhooks:", fmt.Sprintf("Processing webhooks for '%s' (%d found)", e[i], len(w))) 91 | 92 | for j := 0; j < len(w); j++ { 93 | // Run the Webhook, sending event and data along to the 94 | // Webhook's defined endpoint 95 | err := w[j].Run(e[i], d, c.server.ID) 96 | if err != nil { 97 | log.Println(WARNING, "webhooks:", fmt.Sprintf("%s:", e[i]), err) 98 | } 99 | } 100 | } 101 | } 102 | } 103 | 104 | // Client Functions 105 | 106 | func (c Conn) onClientConnection(sid string) { 107 | 108 | var cl *client.Client 109 | var ch *chat.Chat 110 | var storeBuffer store.Keyer 111 | var event string 112 | 113 | // Get all Clients 114 | storeBuffer, _ = c.server.store.Get(fmt.Sprintf("client.%s", sid)) 115 | 116 | if storeBuffer != nil { 117 | // Hijack Client object with new Socket ID 118 | cl = &client.Client{ 119 | FirstName: storeBuffer.(*client.Client).FirstName, 120 | LastName: storeBuffer.(*client.Client).LastName, 121 | Name: storeBuffer.(*client.Client).Name, 122 | Uid: storeBuffer.(*client.Client).Uid, 123 | Sid: c.raw.Id(), 124 | } 125 | } 126 | 127 | if cl == nil { 128 | // Create Client Object 129 | cl = client.Create(c.raw.Id()) 130 | 131 | // Save Client Object to Data Store 132 | c.server.store.Put(cl) 133 | 134 | // Create Chat Object 135 | ch = chat.Create(cl) 136 | 137 | // Save Chat Object to Data Store 138 | c.server.store.Put(ch) 139 | 140 | event = "chat:new" 141 | 142 | // Call any webhooks if they exist 143 | b, err := json.Marshal(ch) 144 | if err != nil { 145 | log.Println(WARNING, "client", err) 146 | } else { 147 | c.runWebhooks([]string{ 148 | webhook.EventNewChat, 149 | webhook.EventNewClient, 150 | }, b) 151 | } 152 | } else { 153 | // Save Client Object to Data Store with updated Sid 154 | c.server.store.Put(cl) 155 | 156 | // Get Chat Object 157 | storeBuffer, _ = c.server.store.Get(fmt.Sprintf("chat.%s", cl.Uid)) 158 | 159 | // Hijack the Chat Object 160 | ch = &chat.Chat{ 161 | CreationTime: storeBuffer.(*chat.Chat).CreationTime, 162 | // TODO: Update UpdatedTime to now 163 | UpdatedTime: storeBuffer.(*chat.Chat).UpdatedTime, 164 | Open: storeBuffer.(*chat.Chat).Open, 165 | Uid: storeBuffer.(*chat.Chat).Uid, 166 | Client: cl, 167 | } 168 | 169 | // Save Chat Object to Data Store with updated Client 170 | c.server.store.Put(ch) 171 | 172 | event = "chat:existing" 173 | } 174 | 175 | // Convert Chat to JSON 176 | chJSON, _ := json.Marshal(ch) 177 | var buffer bytes.Buffer 178 | buffer.Write(chJSON) 179 | // buffer.WriteString("\n") 180 | 181 | m := Message{ 182 | event: event, 183 | message: buffer.String(), 184 | target: "", 185 | } 186 | 187 | // Broadcast Chat to Operators 188 | c.server.broadcastToOperators <- &m 189 | 190 | // Send Chat back to Client 191 | c.send <- &m 192 | } 193 | 194 | func (c Conn) onClientMessage(m string) { 195 | // Create message from JSON 196 | var msg chat.Message 197 | 198 | json.Unmarshal([]byte(m), &msg) 199 | 200 | log.Println(DEBUG, "client", fmt.Sprintf("%s: %s", c.raw.Id(), msg.Content)) 201 | 202 | // Save Message to Data Store 203 | c.server.store.Put(msg) 204 | 205 | // TODO: 206 | // Update Chat Object 207 | // Save Chat Object to Data Store? 208 | // Run any webhooks if they exist 209 | c.runWebhooks([]string{webhook.EventNewClientMessage}, []byte(m)) 210 | 211 | // Broadcast to Operators 212 | c.server.broadcastToOperators <- &Message{ 213 | event: "client:message", 214 | message: m, 215 | target: "", 216 | } 217 | } 218 | 219 | func (c Conn) onClientTyping(m string) { 220 | // Create message from JSON 221 | var msg chat.Message 222 | 223 | json.Unmarshal([]byte(m), &msg) 224 | 225 | log.Println(DEBUG, "client", fmt.Sprintf("%s: typing ...", c.raw.Id())) 226 | 227 | c.server.broadcastToOperators <- &Message{ 228 | event: "client:typing", 229 | message: m, 230 | target: "", 231 | } 232 | } 233 | 234 | // Operator Functions 235 | 236 | func (c Conn) onOperatorConnection(id string, t string) { 237 | 238 | var o *operator.Operator 239 | // TODO: Is this the best way to go about providing access controls? 240 | // Is id set? 241 | // Is token set? 242 | // Get operator with these variables 243 | // TODO: What should we do if there is no id/token (access ID, 244 | // access token)? 245 | // TODO: This is the only way we can find the operator right now, we 246 | // need to improve the InMemory store to handle querying 247 | operators, err := c.server.store.Search("operator.") 248 | if err != nil { 249 | // TODO: What should happen here? 250 | log.Println(ERROR, "operator", fmt.Sprintf("Something unexpected happened")) 251 | } 252 | 253 | for _, op := range operators { 254 | log.Println(DEBUG, "operator", "Does operator match", fmt.Sprintf("(%s == %s)", id, op.(*operator.Operator).Aid)) 255 | if op.(*operator.Operator).Aid == id && 256 | op.(*operator.Operator).Atoken == t { 257 | o = op.(*operator.Operator) 258 | break 259 | } 260 | } 261 | 262 | if o != nil { 263 | // TODO: Currently the whole apparatus works off of the Uid.. 264 | // So this feels weird. 265 | // Update the Uid.. 266 | o.Uid = c.raw.Id() 267 | } else { 268 | // If there is no result from the store, create new Operator Object 269 | o = operator.Create(c.raw.Id()) 270 | 271 | b, err := json.Marshal(o) 272 | if err != nil { 273 | log.Println(ERROR, "operator", err) 274 | } else { 275 | c.runWebhooks([]string{webhook.EventNewOperator}, b) 276 | } 277 | } 278 | 279 | // Save Operator Object to Data Store 280 | c.server.store.Put(o) 281 | 282 | b, err := json.Marshal(o) 283 | if err != nil { 284 | log.Println(ERROR, "operator", err) 285 | } 286 | 287 | // Broadcast the new Operator to all Operators 288 | c.server.broadcastToOperators <- &Message{ 289 | event: "operator:new", 290 | message: string(b), 291 | } 292 | } 293 | 294 | func (c Conn) onOperatorMessage(m string) { 295 | // Create message from JSON 296 | var msg chat.Message 297 | 298 | json.Unmarshal([]byte(m), &msg) 299 | 300 | log.Println(DEBUG, "operator", fmt.Sprintf("%s: %s", c.raw.Id(), msg.Content)) 301 | 302 | // Save Message to Data Store 303 | c.server.store.Put(msg) 304 | 305 | // TODO: Update Chat Object? 306 | // TODO: Save Chat Object to Data Store? 307 | 308 | storeBuffer, _ := c.server.store.Get(fmt.Sprintf("client.%s", msg.Chat)) 309 | 310 | if storeBuffer == nil { 311 | log.Println(ERROR, "operator:", fmt.Sprintf("Client %s does not exist!", msg.Chat)) 312 | return 313 | } 314 | 315 | // TODO: This could be better, seems kinda hacky 316 | // Hijack the Client object we need to message to 317 | cl := &client.Client{ 318 | FirstName: storeBuffer.(*client.Client).FirstName, 319 | LastName: storeBuffer.(*client.Client).LastName, 320 | Name: storeBuffer.(*client.Client).Name, 321 | Uid: storeBuffer.(*client.Client).Uid, 322 | Sid: storeBuffer.(*client.Client).Sid, 323 | } 324 | 325 | c.server.broadcastToClient <- &Message{ 326 | event: "operator:message", 327 | message: m, 328 | target: cl.Sid, 329 | } 330 | } 331 | 332 | func (c Conn) onOperatorTyping(m string) { 333 | // Create message from JSON 334 | var msg chat.Message 335 | 336 | json.Unmarshal([]byte(m), &msg) 337 | 338 | log.Println(DEBUG, "operator", fmt.Sprintf("%s: typing ...", c.raw.Id())) 339 | 340 | storeBuffer, _ := c.server.store.Get(fmt.Sprintf("client.%s", msg.Chat)) 341 | 342 | if storeBuffer == nil { 343 | log.Println(ERROR, "operator:", fmt.Sprintf("Client %s does not exist!", msg.Chat)) 344 | return 345 | } 346 | 347 | // TODO: This could be better, seems kinda hacky 348 | // Hijack the Client object we need to message to 349 | cl := &client.Client{ 350 | FirstName: storeBuffer.(*client.Client).FirstName, 351 | LastName: storeBuffer.(*client.Client).LastName, 352 | Name: storeBuffer.(*client.Client).Name, 353 | Uid: storeBuffer.(*client.Client).Uid, 354 | Sid: storeBuffer.(*client.Client).Sid, 355 | } 356 | 357 | c.server.broadcastToClient <- &Message{ 358 | event: "operator:typing", 359 | message: m, 360 | target: cl.Sid, 361 | } 362 | } 363 | -------------------------------------------------------------------------------- /pkg/server/socket/server.go: -------------------------------------------------------------------------------- 1 | package socket 2 | 3 | import ( 4 | // "bytes" 5 | // "encoding/json" 6 | "fmt" 7 | "log" 8 | "net/http" 9 | "time" 10 | 11 | // TODO: Move away from this library 12 | "github.com/minimalchat/go-socket.io" // Socket 13 | // "github.com/googollee/go-socket.io" // Socket 14 | 15 | // "github.com/minimalchat/daemon/chat" 16 | // "github.com/minimalchat/daemon/client" 17 | // "github.com/minimalchat/daemon/operator" 18 | // "github.com/minimalchat/daemon/person" 19 | "github.com/minimalchat/daemon/pkg/store" // InMemory store 20 | ) 21 | 22 | // Log levels 23 | const ( 24 | DEBUG string = "DEBUG" 25 | INFO string = "INFO" 26 | WARNING string = "WARN" 27 | ERROR string = "ERROR" 28 | FATAL string = "FATAL" 29 | ) 30 | 31 | /* 32 | Server is the socket.io abstraction for Minimal Chat */ 33 | type Server struct { 34 | ID string 35 | store *store.InMemory 36 | sock *socketio.Server 37 | 38 | sockets map[*Conn]bool 39 | 40 | registerClient chan *Conn 41 | registerOperator chan *Conn 42 | unregister chan *Conn 43 | broadcastToOperators chan *Message 44 | broadcastToClient chan *Message 45 | } 46 | 47 | /* 48 | Create takes a data store and returns a new socket server */ 49 | func Create(ds *store.InMemory) (*Server, error) { 50 | log.Println(DEBUG, "socket:", "Starting WebSocket server ...") 51 | 52 | ping, _ := time.ParseDuration("5s") 53 | 54 | srv := &Server{ 55 | store: ds, 56 | 57 | registerClient: make(chan *Conn), 58 | registerOperator: make(chan *Conn), 59 | 60 | unregister: make(chan *Conn), 61 | 62 | broadcastToOperators: make(chan *Message), 63 | broadcastToClient: make(chan *Message), 64 | 65 | sockets: make(map[*Conn]bool), 66 | } 67 | 68 | sock, err := socketio.NewServer(nil) 69 | 70 | if err != nil { 71 | return nil, err 72 | } 73 | 74 | srv.sock = sock 75 | srv.sock.SetPingInterval(ping) 76 | 77 | srv.sock.On("connection", func(s socketio.Socket) { 78 | go srv.onConnect(s) 79 | }) 80 | 81 | return srv, nil 82 | } 83 | 84 | /* 85 | Listen creates a new Server instance and begins listening for ws:// 86 | connections. */ 87 | func (s Server) Listen() { 88 | 89 | for { 90 | select { 91 | case data := <-s.broadcastToClient: 92 | for c := range s.sockets { 93 | if c.raw.Id() == data.target { 94 | select { 95 | case c.send <- data: 96 | default: 97 | close(c.send) 98 | delete(s.sockets, c) 99 | } 100 | } 101 | } 102 | case data := <-s.broadcastToOperators: 103 | for c := range s.sockets { 104 | if c.category == OPERATOR { 105 | select { 106 | case c.send <- data: 107 | default: 108 | log.Println(DEBUG, "socket:", fmt.Sprintf("%s send channel not available, closing ..", c.raw.Id())) 109 | close(c.send) 110 | delete(s.sockets, c) 111 | } 112 | } 113 | } 114 | case c := <-s.registerOperator: 115 | s.sockets[c] = true 116 | case c := <-s.registerClient: 117 | s.sockets[c] = true 118 | case c := <-s.unregister: 119 | if _, ok := s.sockets[c]; ok { 120 | delete(s.sockets, c) 121 | close(c.send) 122 | } 123 | } 124 | } 125 | } 126 | 127 | func (s *Server) onConnect(raw socketio.Socket) { 128 | 129 | var cat Category 130 | 131 | query := raw.Request().URL.Query() 132 | 133 | connType := query.Get("type") 134 | accessID := query.Get("accessId") 135 | accessToken := query.Get("accessToken") 136 | sessionID := query.Get("sessionId") 137 | 138 | // Identify the connection type 139 | switch connType { 140 | case "client": 141 | cat = CLIENT 142 | break 143 | case "operator": 144 | cat = OPERATOR 145 | break 146 | default: 147 | log.Println(WARNING, "socket:", "Unknown connection type, dropping ...") 148 | return 149 | } 150 | 151 | log.Println(INFO, "socket:", fmt.Sprintf("Incoming %s connection %s", cat, raw.Id())) 152 | 153 | // Create a Socket Connection 154 | conn := Conn{ 155 | server: s, 156 | 157 | raw: raw, 158 | category: cat, 159 | 160 | send: make(chan *Message), 161 | } 162 | 163 | // Start listening for channel messages 164 | go conn.Listen() 165 | 166 | // Register event types 167 | // TODO: Do I really need to listen for both on every socket? 168 | 169 | conn.raw.On("client:message", func(data string) { 170 | go conn.onClientMessage(data) 171 | }) 172 | 173 | conn.raw.On("client:typing", func(data string) { 174 | go conn.onClientTyping(data) 175 | }) 176 | 177 | conn.raw.On("operator:message", func(data string) { 178 | go conn.onOperatorMessage(data) 179 | }) 180 | 181 | conn.raw.On("operator:typing", func(data string) { 182 | go conn.onOperatorTyping(data) 183 | }) 184 | 185 | conn.raw.On("disconnection", func() { 186 | s.unregister <- &conn 187 | }) 188 | 189 | // Register the new client, depending on connection type 190 | switch conn.category { 191 | case OPERATOR: 192 | // Register the new Socket with the server as an Operator 193 | s.registerOperator <- &conn 194 | 195 | // TODO: This may not be the right name for this func now 196 | go conn.onOperatorConnection(accessID, accessToken) 197 | 198 | break 199 | case CLIENT: 200 | // Register the new Socket with the server as a Client 201 | s.registerClient <- &conn 202 | 203 | // TODO: This may not be the right name for this func now 204 | go conn.onClientConnection(sessionID) 205 | 206 | break 207 | default: 208 | log.Println(ERROR, "socket:", "Unknown connection type specified") 209 | } 210 | } 211 | 212 | /* 213 | ServeHTTP serves the socket.io client script */ 214 | func (s Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { 215 | s.sock.ServeHTTP(w, r) 216 | } 217 | -------------------------------------------------------------------------------- /pkg/store/store.go: -------------------------------------------------------------------------------- 1 | // Package store provides an in-memory key-value store. 2 | package store 3 | 4 | import ( 5 | "fmt" 6 | "log" 7 | "strings" 8 | "sync" 9 | ) 10 | 11 | /* 12 | Log Levels */ 13 | const ( 14 | DEBUG string = "DEBUG" 15 | INFO string = "INFO" 16 | WARNING string = "WARN" 17 | ERROR string = "ERROR" 18 | FATAL string = "FATAL" 19 | ) 20 | 21 | /* 22 | Keyer is an object that can be kept in an InMemory store */ 23 | type Keyer interface { 24 | Key() string 25 | } 26 | 27 | /* 28 | InMemory store is thread-safe and handles any object implemented as a Keyer */ 29 | type InMemory struct { 30 | sync.RWMutex 31 | data map[string]Keyer 32 | } 33 | 34 | /* 35 | Put stores a value */ 36 | func (db *InMemory) Put(v Keyer) error { 37 | db.Lock() 38 | defer db.Unlock() 39 | 40 | if db.data == nil { 41 | db.data = make(map[string]Keyer) 42 | } 43 | 44 | log.Println(DEBUG, "store:", fmt.Sprintf("Storing %s ...", v.Key())) 45 | 46 | db.data[v.Key()] = v 47 | return nil 48 | } 49 | 50 | /* 51 | Get retrieves a value */ 52 | func (db *InMemory) Get(k string) (Keyer, error) { 53 | db.RLock() 54 | defer db.RUnlock() 55 | 56 | log.Println(DEBUG, "store:", fmt.Sprintf("Getting %s ...", k)) 57 | 58 | return db.data[k], nil 59 | } 60 | 61 | /* 62 | Remove a value */ 63 | func (db *InMemory) Remove(k string) error { 64 | db.Lock() 65 | defer db.Unlock() 66 | 67 | if db.data == nil { 68 | db.data = make(map[string]Keyer) 69 | } 70 | 71 | log.Println(DEBUG, "store:", fmt.Sprintf("Deleting %s ...", k)) 72 | 73 | delete(db.data, k) 74 | return nil 75 | } 76 | 77 | /* 78 | Search on keys */ 79 | func (db *InMemory) Search(q string) ([]Keyer, error) { 80 | var result []Keyer 81 | 82 | log.Println(DEBUG, "store:", fmt.Sprintf("Searching for %s ...", q)) 83 | 84 | for key := range db.data { 85 | if strings.Contains(key, q) { 86 | value, _ := db.Get(key) 87 | result = append(result, value) 88 | } 89 | } 90 | 91 | return result, nil 92 | } 93 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | golang/protobuf/jsonpb 2 | github.com/golang/lint/golint 3 | github.com/julienschmidt/httprouter 4 | github.com/minimalchat/go-socket.io 5 | github.com/golang-plus/uuid 6 | github.com/go-playground/overalls 7 | github.com/mattn/goveralls 8 | github.com/golang/protobuf/proto 9 | github.com/golang/protobuf/protoc-gen-go 10 | --------------------------------------------------------------------------------