diff --git a/README.md b/README.md index fe303e6..c09fb40 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,15 @@ LIME - A lightweight messaging library ![Go](https://github.com/takenet/lime-go/workflows/Go/badge.svg?branch=master) -LIME allows you to build scalable, real-time messaging applications using a JSON-based [open protocol](http://limeprotocol.org). It's **fully asynchronous** and supports any persistent transport like TCP or Websockets. +LIME allows you to build scalable, real-time messaging applications using a JSON-based [open protocol](http://limeprotocol.org). +It's **fully asynchronous** and support persistent transports like TCP or Websockets. You can send and receive any type of document into the wire as long it can be represented as JSON or text (plain or encoded with base64) and it has a **MIME type** to allow the other party handle it in the right way. The connected nodes can send receipts to the other parties to notify events about messages (for instance, a message was received or the content invalid or not supported). -Besides that, there's a **REST capable** command interface with verbs (*get, set and delete*) and resource identifiers (URIs) to allow rich messaging scenarios. You can use that to provide services like on-band account registration or instance-messaging resources, like presence or roster management. +Besides that, there's a **REST capable** command interface with verbs (*get, set and delete*) and resource identifiers (URIs) to allow rich messaging scenarios. +You can use that to provide services like on-band account registration or instance-messaging resources, like presence or roster management. Finally, it has built-in support for authentication, transport encryption and compression. diff --git a/examples/ws-chat/client/dist/index.html b/examples/ws-chat/client/dist/index.html index fc91365..b4742e9 100644 --- a/examples/ws-chat/client/dist/index.html +++ b/examples/ws-chat/client/dist/index.html @@ -16,10 +16,7 @@
-
+
@@ -31,19 +28,24 @@ The available commands are:

  • - /name [new_nickname]: Sets a new nickname + /name [new_nickname]: Sets a new nickname.
  • - /to [nickname] [message]: Sends a private message to a nickname + /to [nickname] [message]: Sends a private message to a nickname. +
  • +
  • + /friends: Display your friends list. +
  • +
  • + /add [nickname]: Adds a nickname to your friends list. +
  • +
  • + /remove [nickname]: Removes a nickname from your friends list.
- -
- -
diff --git a/examples/ws-chat/client/src/index.js b/examples/ws-chat/client/src/index.js index 7adb91b..5677c1a 100644 --- a/examples/ws-chat/client/src/index.js +++ b/examples/ws-chat/client/src/index.js @@ -13,6 +13,7 @@ const BOT_IMG = "../images/bot.png"; let nickname = `guest`; let client = await createClient(); +let closing = false; // Notify other users await client.sendMessage({ @@ -30,6 +31,11 @@ async function createClient() { client.transport.onError = async function () { client = await connect(); } + client.transport.onClose = async function() { + if (!closing) { + client = await connect(); + } + } client.onMessage = (message) => { switch (message.type) { case 'application/x-chat-nickname+json': @@ -61,8 +67,10 @@ msgerForm.addEventListener("submit", async event => { const msgText = msgerInput.value; if (!msgText) return; + appendMessage(nickname, PERSON_IMG, "right", msgText); + msgerInput.value = ""; + if (await parseCommand(msgText)) { - msgerInput.value = ""; return; } @@ -77,28 +85,47 @@ async function sendMessage(content, type, to = null) { type: type ?? "text/plain", content: content, }); - - appendMessage(nickname, PERSON_IMG, "right", content); - msgerInput.value = ""; } + + async function parseCommand(input) { - if (input.startsWith("/name ")) { - let arg = input.split(" ")[1]; - if (arg) { - await setNickname(arg); - return true; - } - } + let args = input.split(' '); + + switch (args[0]) { + case '/name': + if (args.length > 1) { + await setNickname(args[1]); + return true; + } + break; + + case '/to': + if (args.length >= 2) { + let to = args[1]; + let content = args.slice(2).join(' '); + await sendMessage(content, 'text/plain', to); + return true; + } + break; - if (input.startsWith("/to")) { - let args = input.split(" "); - if (args.length >= 2) { - let to = args[1]; - let content = args.slice(2).join(" "); - await sendMessage(content, "text/plain", to); + case '/friends': + await getFriends(); return true; - } + + case '/add': + if (args.length > 1) { + await addFriend(args[1]); + return true; + } + break; + + case '/remove': + if (args.length > 1) { + await removeFriend(args[1]); + return true; + } + break; } return false; @@ -108,9 +135,13 @@ async function setNickname(newNickname) { let oldNickname = nickname; nickname = newNickname; + closing = true; + await client.sendFinishingSession(); client = await createClient(); + closing = false; + // Notify other users await client.sendMessage({ id: uuidv4(), @@ -146,6 +177,61 @@ function appendMessage(name, img, side, text) { msgerChat.scrollTop += 500; } +async function getFriends() { + let response = await client.processCommand({ + id: uuidv4(), + method: 'get', + uri: '/friends' + }); + + if (response.status !== 'success') { + appendMessage("BOT", BOT_IMG, "left", `Ops, an error occurred while retrieving your friends list: ${response.reason.description}`); + return; + } + + let responseMsg = 'Your friends are:
'; + for (let i = 0; i < response.resource.items.length; i++) { + let friend = response.resource.items[i]; + responseMsg += `- ${friend.nickname}${friend.online? ' (online)': '' }
`; + } + + appendMessage("BOT", BOT_IMG, "left", responseMsg); +} + +async function addFriend(nickname) { + let response = await client.processCommand({ + id: uuidv4(), + method: 'set', + uri: '/friends', + type: 'application/x-chat-friend+json', + resource: { + nickname: nickname, + } + }); + + if (response.status !== 'success') { + appendMessage("BOT", BOT_IMG, "left", `Ops, an error occurred while adding this friends: ${response.reason.description}`); + return; + } + + appendMessage("BOT", BOT_IMG, "left", `The nickname '${nickname}' was added to your friends list.`); +} + +async function removeFriend(nickname) { + let response = await client.processCommand({ + id: uuidv4(), + method: 'delete', + uri: `/friends/${nickname}` + }); + + if (response.status !== 'success') { + appendMessage("BOT", BOT_IMG, "left", `Ops, an error occurred while removing this friends: ${response.reason.description}`); + return; + } + + appendMessage("BOT", BOT_IMG, "left", `The nickname '${nickname}' was removed from your friends list.`); +} + // Utils function get(selector, root = document) { return root.querySelector(selector); diff --git a/examples/ws-chat/server/main.go b/examples/ws-chat/server/main.go index a07c8dd..184a505 100644 --- a/examples/ws-chat/server/main.go +++ b/examples/ws-chat/server/main.go @@ -2,6 +2,7 @@ package main import ( "context" + "fmt" "github.com/takenet/lime-go" "go.uber.org/multierr" "log" @@ -11,21 +12,18 @@ import ( "os" "os/signal" "strconv" + "strings" "sync" ) var channels = make(map[string]*lime.ServerChannel) var nodesToID = make(map[string]string) var mu sync.RWMutex +var nodeFriends = make(map[string][]string) func main() { - wsConfig := &lime.WebsocketConfig{ - CheckOrigin: func(r *http.Request) bool { - return true - }, - } - server := lime.NewServerBuilder(). + // Handler for registering new user sessions Register(func(ctx context.Context, candidate lime.Node, c *lime.ServerChannel) (lime.Node, error) { mu.Lock() defer mu.Unlock() @@ -45,6 +43,7 @@ func main() { nodesToID[candidate.Name] = sessionID return candidate, nil }). + // Callback for finished sessions, useful for updating our online users map Finished(func(sessionID string) { mu.Lock() defer mu.Unlock() @@ -54,8 +53,22 @@ func main() { delete(channels, sessionID) } }). - MessagesHandlerFunc(HandleMessage). - ListenWebsocket(&net.TCPAddr{Port: 8080}, wsConfig). + // Handler for all messages received by the server + MessagesHandlerFunc(handleMessage). + // Handler for commands with the "/friends" resource + CommandHandlerFunc( + func(cmd *lime.Command) bool { + uri := cmd.URI.ToURL() + return cmd.ID != "" && cmd.Status == "" && strings.HasPrefix(uri.Path, "/friends") + }, + handleFriendsCommand). + // Listen using the websocket transport in the 8080 port + ListenWebsocket( + &net.TCPAddr{Port: 8080}, + &lime.WebsocketConfig{ + CheckOrigin: func(r *http.Request) bool { + return true + }}). Build() sig := make(chan os.Signal) @@ -76,7 +89,7 @@ func main() { } } -func HandleMessage(ctx context.Context, msg *lime.Message, s lime.Sender) error { +func handleMessage(ctx context.Context, msg *lime.Message, s lime.Sender) error { mu.RLock() defer mu.RUnlock() @@ -99,3 +112,128 @@ func HandleMessage(ctx context.Context, msg *lime.Message, s lime.Sender) error } return err } + +func handleFriendsCommand(ctx context.Context, cmd *lime.Command, s lime.Sender) error { + node, _ := lime.ContextSessionRemoteNode(ctx) + + var respCmd *lime.Command + switch cmd.Method { + case lime.CommandMethodGet: + respCmd = getFriends(node, cmd) + case lime.CommandMethodSet: + respCmd = addFriend(cmd, node) + case lime.CommandMethodDelete: + respCmd = removeFriend(cmd, node) + default: + respCmd = cmd.FailureResponse(&lime.Reason{ + Code: 1, + Description: "Unsupported method", + }) + } + + return s.SendCommand(ctx, respCmd) +} + +func getFriends(node lime.Node, cmd *lime.Command) *lime.Command { + var respCmd *lime.Command + + if friends, ok := nodeFriends[node.Name]; ok { + items := make([]lime.Document, len(friends)) + for i, f := range friends { + _, online := nodesToID[f] + items[i] = &Friend{ + Nickname: f, + Online: online, + } + } + + respCmd = cmd.SuccessResponseWithResource( + &lime.DocumentCollection{ + Total: len(friends), + ItemType: friendMediaType, + Items: items, + }) + } else { + respCmd = cmd.FailureResponse(&lime.Reason{ + Code: 1, + Description: "No friends found", + }) + } + return respCmd +} + +func addFriend(cmd *lime.Command, node lime.Node) *lime.Command { + var respCmd *lime.Command + + if f, ok := cmd.Resource.(*Friend); ok { + friends := nodeFriends[node.Name] + friends = append(friends, f.Nickname) + nodeFriends[node.Name] = friends + respCmd = cmd.SuccessResponse() + + } else { + respCmd = cmd.FailureResponse(&lime.Reason{ + Code: 1, + Description: fmt.Sprintf("Unexpected resource type, should be '%v'", friendMediaType.String()), + }) + } + return respCmd +} + +func removeFriend(cmd *lime.Command, node lime.Node) *lime.Command { + var respCmd *lime.Command + + url := cmd.URI.ToURL() + + segments := strings.Split(url.Path, "/") + if len(segments) >= 2 { + friends := nodeFriends[node.Name] + toRemove := -1 + + for i, f := range friends { + if string(f) == segments[1] { + toRemove = i + break + } + } + + if toRemove >= 0 { + friends = append(friends[:toRemove], friends[toRemove+1:]...) + nodeFriends[node.Name] = friends + respCmd = cmd.SuccessResponse() + } else { + respCmd = cmd.FailureResponse(&lime.Reason{ + Code: 1, + Description: fmt.Sprintf("Friend '%v' not found", segments[1]), + }) + } + } else { + respCmd = cmd.FailureResponse(&lime.Reason{ + Code: 1, + Description: "Invalid URI, should be '/friends/'", + }) + } + + return respCmd +} + +var friendMediaType = lime.MediaType{ + Type: "application", + Subtype: "x-chat-friend", + Suffix: "json", +} + +type Friend struct { + Nickname string `json:"nickname"` + Online bool `json:"online,omitempty"` +} + +func (f *Friend) MediaType() lime.MediaType { + return friendMediaType +} + +func init() { + lime.RegisterDocumentFactory(func() lime.Document { + return &Friend{} + }) +}