Skip to content

Commit

Permalink
Initial scaffolding from plume.chat
Browse files Browse the repository at this point in the history
  • Loading branch information
thesephist committed May 15, 2020
1 parent c9bacba commit 2cb956e
Show file tree
Hide file tree
Showing 15 changed files with 476 additions and 1 deletion.
21 changes: 20 additions & 1 deletion README.md
@@ -1,2 +1,21 @@
# draw
Real-time collaborative whiteboard on the web

draw is a tiny in-memory collaborative whiteboard. It's built on...

- [Gorilla WebSocket](https://github.com/gorilla/websocket) for initiating and managing WebSocket connections
- [Torus](https://github.com/thesephist/torus) as a light frontend UI library
- My own [blocks.css](https://thesephist.github.io/blocks.css/) to add some spice to the UI design

## Deploy

Deployment is managed by systemd. Copy the `draw.service` file to `/etc/systemd/system/draw.service` and update:

- replace `draw-user` with your Linux user
- replace `/home/draw-user/draw` with your working directory (path to repository or a copy of `static/`)

Then start draw as a service:

```sh
systemctl daemon-reload # reload systemd script
systemctl start draw # start draw server as a service
```
13 changes: 13 additions & 0 deletions cmd/draw.go
@@ -0,0 +1,13 @@
package main

import (
"fmt"

"github.com/thesephist/draw/pkg/draw"
)

func main() {
fmt.Println("Starting draw server...")

draw.StartServer()
}
13 changes: 13 additions & 0 deletions cmd/plume.go
@@ -0,0 +1,13 @@
package main

import (
"fmt"

"github.com/thesephist/draw/pkg/draw"
)

func main() {
fmt.Println("Starting draw server...")

draw.StartServer()
}
28 changes: 28 additions & 0 deletions draw.service
@@ -0,0 +1,28 @@
[Unit]
Description=draw server
ConditionPathExists=/home/draw-user/draw/draw
After=network.target

[Service]
Type=simple
User=draw-user
LimitNOFILE=256

Restart=on-failure
RestartSec=10
StartLimitIntervalSec=60

WorkingDirectory=/home/draw-user/draw/
ExecStart=/home/draw-user/draw/draw

# make sure log directory exists and owned by syslog
PermissionsStartOnly=true
ExecStartPre=/bin/mkdir -p /var/log/draw
ExecStartPre=/bin/chown syslog:adm /var/log/draw
ExecStartPre=/bin/chmod 755 /var/log/draw
StandardOutput=syslog
StandardError=syslog
SyslogIdentifier=draw

[Install]
WantedBy=multi-user.target
11 changes: 11 additions & 0 deletions go.mod
@@ -0,0 +1,11 @@
module github.com/thesephist/plume

go 1.13

require (
github.com/google/uuid v1.1.1 // indirect
github.com/gorilla/mux v1.7.3
github.com/gorilla/websocket v1.4.1
github.com/mailgun/mailgun-go/v3 v3.6.3 // indirect
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
)
24 changes: 24 additions & 0 deletions go.sum
@@ -0,0 +1,24 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51 h1:0JZ+dUmQeA8IIVUMzysrX4/AKuQwWhV2dYQuPZdvdSQ=
github.com/facebookgo/ensure v0.0.0-20160127193407-b4ab57deab51/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052 h1:JWuenKqqX8nojtoVVWjGfOF9635RETekkoH6Cc9SX0A=
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870 h1:E2s37DuLxFhQDg5gKsWoLBOB0n+ZW8s599zru8FJ2/Y=
github.com/facebookgo/subset v0.0.0-20150612182917-8dac2c3c4870/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
github.com/go-chi/chi v4.0.0+incompatible h1:SiLLEDyAkqNnw+T/uDTf3aFB9T4FTrwMpuYrgaRcnW4=
github.com/go-chi/chi v4.0.0+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/mailgun/mailgun-go/v3 v3.6.3 h1:dkPP10w15Igt0uM313CStG9+gd/XqdiqO0862zNAXbs=
github.com/mailgun/mailgun-go/v3 v3.6.3/go.mod h1:ZjVnH8S0dR2BLjvkZc/rxwerdcirzlA12LQDuGAadR0=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3bUBu+FXuk2pFbkN6tcwi/pjyaDic=
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
56 changes: 56 additions & 0 deletions pkg/draw/client.go
@@ -0,0 +1,56 @@
package draw

// Client represents an abstract client of a chat room
type Client struct {
User User
Room *Room
OnMessage func(Message)

receiver chan Message
}

// Send sends a new message to the room to which the client
// cl belongs, under the client user's name.
func (cl *Client) Send(text string) error {
if cl.Room == nil {
return Error{"client is not in a room yet"}
}

cl.Room.Broadcast(Message{
Type: msgText,
User: cl.User,
Text: text,
})

return nil
}

// Leave lets the client leave the room and cleans up.
func (cl *Client) Leave() error {
if cl.Room == nil {
return Error{"client is not in a room yet"}
}

delete(cl.Room.clientReceivers, cl)
close(cl.receiver)
cl.Room = nil

return nil
}

// StartListening enters an indefinite loop listening
// for new messages for the client and responds with cl.OnMessage.
func (cl *Client) StartListening() {
for {
msg, open := <-cl.receiver
if !open {
return
}

if cl.OnMessage == nil {
continue
}

cl.OnMessage(msg)
}
}
11 changes: 11 additions & 0 deletions pkg/draw/err.go
@@ -0,0 +1,11 @@
package draw

// Error represents any error originating
// from unexpected states in the draw server.
type Error struct {
reason string
}

func (err Error) Error() string {
return err.reason
}
30 changes: 30 additions & 0 deletions pkg/draw/message.go
@@ -0,0 +1,30 @@
package draw

const (
// msgHello is used to first connect and request authentication
msgHello = iota
// msgText is used for all normal text messages
msgText

// msgAuth represents an attempt to authenticate with a token
msgAuth
// msgAuthAck is sent by the server to approve authentication attempt
msgAuthAck
// msgAuthRst is sent by the server to reject authentication attemp
msgAuthRst

// msgMayNotEnter is sent by the server to reject entry attempt, usually
// means the username is taken
msgMayNotEnter

// In the future, we can support things like presence
// by using additional codes like MsgTypingStart/Stop
)

// Message represents any atomic communication between a draw client
// and server.
type Message struct {
Type int `json:"type"`
User User `json:"user"`
Text string `json:"text"`
}
61 changes: 61 additions & 0 deletions pkg/draw/room.go
@@ -0,0 +1,61 @@
package draw

import (
"strings"
)

// Room represents a collection of draw clients all
// sending each other messages.
type Room struct {
Sender chan<- Message
// map of usernames to emails
verifiedNames map[string]string
clientReceivers map[*Client]chan Message
}

// NewRoom allocates, creates, and returns a new Room
// ready to be used
func NewRoom() *Room {
return &Room{
Sender: make(chan Message),
verifiedNames: make(map[string]string),
clientReceivers: make(map[*Client]chan Message),
}
}

// Enter creates a new Client for a given user ready
// to be used
func (rm *Room) Enter(u User) *Client {
receiver := make(chan Message)
client := Client{
User: u,
Room: rm,
receiver: receiver,
}

rm.verifiedNames[strings.ToLower(u.Name)] = u.Email
rm.clientReceivers[&client] = receiver
go client.StartListening()

return &client
}

// CanEnter reports whether a user should be allowed in a room.
// A user may not enter a room if another user with a different email
// but a matching username is already inside.
func (rm *Room) CanEnter(u User) bool {
existingEmail, prs := rm.verifiedNames[strings.ToLower(u.Name)]
if prs {
return u.Email == existingEmail
}

return true
}

// Broadcast sends a new Message to every client
// in the Room
func (rm *Room) Broadcast(msg Message) {
for _, receiver := range rm.clientReceivers {
receiver <- msg
}
}

0 comments on commit 2cb956e

Please sign in to comment.