ttfm-socket 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +212 -0
  3. package/dist-client/actions/addDj.d.ts +26 -0
  4. package/dist-client/actions/joinRoom.d.ts +27 -0
  5. package/dist-client/actions/playOneTimeAnimation.d.ts +31 -0
  6. package/dist-client/actions/removeDj.d.ts +26 -0
  7. package/dist-client/actions/skipSong.d.ts +22 -0
  8. package/dist-client/actions/updateNextSong.d.ts +22 -0
  9. package/dist-client/actions/voteOnSong.d.ts +27 -0
  10. package/dist-client/client/ActionheroWebsocketClient.js +3638 -0
  11. package/dist-client/client/ActionheroWebsocketClient.js.map +1 -0
  12. package/dist-client/client/SocketClient.d.ts +51 -0
  13. package/dist-client/client/SocketClient.js +152 -0
  14. package/dist-client/client/SocketClient.js.map +1 -0
  15. package/dist-client/client/index.d.ts +14 -0
  16. package/dist-client/client/index.js +27 -0
  17. package/dist-client/client/index.js.map +1 -0
  18. package/dist-client/client/types.d.ts +23 -0
  19. package/dist-client/client/types.js +4 -0
  20. package/dist-client/client/types.js.map +1 -0
  21. package/dist-client/types/messages.d.ts +104 -0
  22. package/dist-client/types/messages.js +30 -0
  23. package/dist-client/types/messages.js.map +1 -0
  24. package/dist-client/types/names.d.ts +46 -0
  25. package/dist-client/types/names.js +55 -0
  26. package/dist-client/types/names.js.map +1 -0
  27. package/dist-client/types/services.d.ts +196 -0
  28. package/dist-client/types/services.js +18 -0
  29. package/dist-client/types/services.js.map +1 -0
  30. package/dist-client/types/state.d.ts +65 -0
  31. package/dist-client/types/state.js +3 -0
  32. package/dist-client/types/state.js.map +1 -0
  33. package/package.json +81 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 TTFM Labs, Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,212 @@
1
+ # Turntable LIVE Socket Client <!-- omit in toc -->
2
+
3
+ Welcome to the Turntable LIVE Socket Client! This package provides a TypeScript client to communicate with rooms on Turntable.
4
+
5
+ - [Installation](#installation)
6
+ - [Joining a Room](#joining-a-room)
7
+ - [Events](#events)
8
+ - [`serverMessage`](#servermessage)
9
+ - [`statefulMessage`](#statefulmessage)
10
+ - [`statelessMessage`](#statelessmessage)
11
+ - [`error`](#error)
12
+ - [Connection States](#connection-states)
13
+ - [Listening for Messages](#listening-for-messages)
14
+ - [Actions](#actions)
15
+ - [Messages](#messages)
16
+ - [Stateful Messages](#stateful-messages)
17
+ - [`userJoined`](#userjoined)
18
+ - [`userLeft`](#userleft)
19
+ - [`addedDj`](#addeddj)
20
+ - [`removedDj`](#removeddj)
21
+ - [`playedSong`](#playedsong)
22
+ - [`updatedNextSong`](#updatednextsong)
23
+ - [`votedOnSong`](#votedonsong)
24
+ - [`lookedUpSong`](#lookedupsong)
25
+ - [Stateless Messages](#stateless-messages)
26
+ - [`playedOneTimeAnimation`](#playedonetimeanimation)
27
+ - [`kickedFromRoom`](#kickedfromroom)
28
+ - [`roomReset`](#roomreset)
29
+ - [Examples](#examples)
30
+ - [Support](#support)
31
+
32
+ ## Installation
33
+
34
+ ```
35
+ npm install ttfm-socket
36
+ ```
37
+
38
+ ## Joining a Room
39
+
40
+ To join a room, you'll need to retrieve the UUID for that room using [this endpoint](https://rooms.prod.tt.fm/api/#/Rooms%20data/getRoom). Then create an instance of the `SocketClient` and run the `joinRoom` method:
41
+
42
+ ```ts
43
+ import { SocketClient } from "ttfm-socket";
44
+
45
+ const client = new SocketClient("https://socket.prod.tt.fm");
46
+ const token = "{your Turntable JWT}";
47
+
48
+ const { state } = await client.joinRoom(token, {
49
+ roomUuid: "{room uuid}",
50
+ password: "{optional room password}",
51
+ });
52
+ ```
53
+
54
+ Assuming you are allowed to join the room (e.g. it's not full, the password is correct, etc.), `joinRoom` will resolve with an object containing `state`, an object representing the current room state. If your join request is rejected for any reason, `joinRoom` will throw an error.
55
+
56
+ ### Events
57
+
58
+ When the `SocketClient` receives a message broadcast from the server, it emits a corresponding event, which can be listened to.
59
+
60
+ #### `serverMessage`
61
+
62
+ This is emitted on all messages broadcast from the server. The value emitted here is the _full_ message object, which contains both our custom `message` as well as other values such as `messageId` and `sentAt`.
63
+
64
+ #### `statefulMessage`
65
+
66
+ This is emitted when the `SocketClient` receives a server message that contains `statePatch`. The value emitted here is only our [custom message](#server-messages) without any of the extra values. The type of the message is narrowed so that the listener knows that `statePatch` will exist.
67
+
68
+ #### `statelessMessage`
69
+
70
+ This is emitted when the `SocketClient` receives a server message that contains `params` instead of `statePatch`. The value emitted here is only our [custom message](#server-messages) without any of the extra values. The type of the message is narrowed so that the listener knows that `params` will exist.
71
+
72
+ #### `error`
73
+
74
+ This is when the Primus connection has an error, or when the server returns an error after trying to run an Action. The value emitted here is a `string` error message, or potentially `undefined` if there is no error message available.
75
+
76
+ #### Connection States
77
+
78
+ There are four possible events here: `connected`, `disconnected`, `reconnecting`, and `timeout`. These are emitted when the connection state changes. There is no value emitted with these, so the listener will have 0 arguments.
79
+
80
+ ### Listening for Messages
81
+
82
+ Once a room has been joined, you should attach listeners to the `SocketClient` to handle incoming [messages](#mesages). To compute the state updates, we recommend the [fast-json-patch](https://www.npmjs.com/package/fast-json-patch) package.
83
+
84
+ ```ts
85
+ import { applyPatch } from "fast-json-patch";
86
+
87
+ client.on("statefulMessage", (message) => {
88
+ const newState = applyPatch(
89
+ prevState,
90
+ message.statePatch,
91
+ true,
92
+ false,
93
+ ).newDocument;
94
+ // Save the new state
95
+ });
96
+
97
+ client.on("statelessMessage", (message) => {
98
+ // Respond to the message based on its name and params
99
+ });
100
+ ```
101
+
102
+ Listeners can also be added for specific messages:
103
+
104
+ ```ts
105
+ client.on(ServerMessageName.kickedFromRoom, () => {
106
+ // Clear local state and destroy the client
107
+ });
108
+ ```
109
+
110
+ ## Actions
111
+
112
+ When a client wants to make some sort of update to the room, it can do so with the `action` method in the `SocketClient` class. Documentation for these actions can be found [here](https://socket.prod.tt.fm/swagger.html). For example, to take the stage in a room a client would run the `addDj` action:
113
+
114
+ ```ts
115
+ client.action<AddDjAction>(ActionName.addDj, { song: { ... } })
116
+ ```
117
+
118
+ In this case, `song` is optional but should match the type [MinimalCrateSongResDTO](https://playlists.prod.tt.fm/api/#model-MinimalCrateSongResDTO).
119
+
120
+ ## Messages
121
+
122
+ After a client or the server successfully runs an action in a room, the server will broadcast a message to every client connected to that room, including the one that ran the Action. As a convention, the names of these messages take the past-tense form of the name of the Action that spawned them. For example, the `addDj` Action correlates to the `addedDj` server message.
123
+
124
+ ### Stateful Messages
125
+
126
+ Most of the messages sent by the server come after some sort of room state update. For these, the message will take this shape:
127
+
128
+ ```ts
129
+ {
130
+ name: StatefulServerMessageNameType;
131
+ statePatch: Operation[];
132
+ }
133
+ ```
134
+
135
+ `statePatch` is a JSON-encoded series of operations for the client to perform in order to update its local state to stay in sync with the server. This adheres the [JSON Patch](https://jsonpatch.com/) format.
136
+
137
+ These are all of the stateful messages:
138
+
139
+ #### `userJoined`
140
+
141
+ Sent by the server to all clients when a user joins the room. This comes after a client successfully runs the `joinRoom` Action and is added to the Actoinhero `chatRoom`.
142
+
143
+ #### `userLeft`
144
+
145
+ Sent by the server to all clients when a user leaves the room. When a client sends the Actionhero `roomLeave` message, or when its connection is closed, we enqueue a 20-second delayed Task. If the user has no active connections after that delay, the server will broadcast this `userLeft` message.
146
+
147
+ #### `addedDj`
148
+
149
+ Sent by the server to all clients when a user has been successfully added to the DJ lineup. This comes after a client runs the `addDj` Action.
150
+
151
+ #### `removedDj`
152
+
153
+ Sent by the server to all clients when a user has been successfully removed from the DJ lineup. This comes after a client or server runs the `removeDj` Action.
154
+
155
+ #### `playedSong`
156
+
157
+ Sent by the server to all clients when `nowPlaying` changes. This can be triggered by add/removing DJs, skips, kicks, bans, and the current song ending. This same message is sent when there is no next song to play, in which case `nowPlaying` will be `null`. The only Action that broadcasts this message is `playSong`.
158
+
159
+ #### `updatedNextSong`
160
+
161
+ Sent by the server to all clients when a DJ updates their next song via the `updateNextSong` Action. This _might_ be followed by the `playedSong` message, but only if a DJ sends a non-`null` song and there isn't already something playing.
162
+
163
+ #### `votedOnSong`
164
+
165
+ Sent by the server to all clients when a user updates their song votes via the `voteOnSong` Action. This currently handles likes/dislikes and stars. The client state will have per-user voting information, as well as a computed `vibeMeter` value and an `allVotes` object that contains arrays of user UUIDs for each type of vote.
166
+
167
+ #### `lookedUpSong`
168
+
169
+ Sent by the server to all clients after a successful song lookup. This happens if a client sends a song placeholder with the `addDj` or `updateNextSong` Actions. It's unlikely the clients will need to do anything in response to this message, but it exists to keep the client state in sync with the server state to avoid errors on subsequent patches.
170
+
171
+ ### Stateless Messages
172
+
173
+ If a message does not accompany a room state update, the shape is slightly different:
174
+
175
+ ```ts
176
+ {
177
+ name: StatelessServerMessageNameType;
178
+ params?: StatelessServerMessageParams;
179
+ }
180
+ ```
181
+
182
+ These are all of the stateless messasges:
183
+
184
+ #### `playedOneTimeAnimation`
185
+
186
+ Sent by the server to all clients when a user sends a one-time animation. For this message, `params` takes the following shape:
187
+
188
+ ```ts
189
+ {
190
+ userUuid: string;
191
+ animation: OneTimeAnimationType;
192
+ emoji?: string;
193
+ }
194
+ ```
195
+
196
+ `emoji` here is only defined if `animation` is `OneTimeAnimation.emoji`, and will only ever be a string with a single emoji character in it.
197
+
198
+ #### `kickedFromRoom`
199
+
200
+ Sent by the server _only_ to the connection(s) for one specific user in a room. This indicates that the user was just kicked (or banned). Immediately following this message, the server will close the connection. This message has no `params`.
201
+
202
+ #### `roomReset`
203
+
204
+ Sent by the server after someone hits the `/resetRoom` endpoint for that room. This means that the room's state will be cleared and the server will close all connections that are currenly in the room. In response to this, the clients will need to open a new connection and rejoin the room via the `joinRoom` Action. This message has no `params`.
205
+
206
+ ## Examples
207
+
208
+ - [Discord Bot](https://github.com/TTFM-Labs/TTLIVE-discord-bot)
209
+
210
+ ## Support
211
+
212
+ If you have any questions or feedback about this package, please reach out to us on [Discord](https://discord.gg/kvAhYavVtW) or at help@turntabletlive.com.
@@ -0,0 +1,26 @@
1
+ import { Action, ParamsFrom } from "actionhero";
2
+ import { ActionName, MiddlewareName } from "../types/names";
3
+ export declare class AddDjAction extends Action {
4
+ name: ActionName.addDj;
5
+ description: string;
6
+ inputs: {
7
+ userUuid: {
8
+ required: true;
9
+ };
10
+ roomUuid: {
11
+ required: true;
12
+ };
13
+ tokenRole: {
14
+ required: true;
15
+ formatter: (tokenRole: import("../client").TokenRole) => import("../client").TokenRole;
16
+ };
17
+ song: {
18
+ formatter: (song: import("../client").MinimalCrateSongResDTO) => import("../client").MinimalCrateSongResDTO;
19
+ description: string;
20
+ };
21
+ };
22
+ middleware: MiddlewareName[];
23
+ run({ params: { roomUuid, userUuid, tokenRole, song }, }: {
24
+ params: ParamsFrom<AddDjAction>;
25
+ }): Promise<import("../types/state").ServerRoomState>;
26
+ }
@@ -0,0 +1,27 @@
1
+ import { Action, ParamsFrom, Connection } from "actionhero";
2
+ import { ActionName } from "../types/names";
3
+ export declare class JoinRoomAction extends Action {
4
+ name: ActionName.joinRoom;
5
+ description: string;
6
+ inputs: {
7
+ roomUuid: {
8
+ required: true;
9
+ };
10
+ password: {
11
+ description: string;
12
+ };
13
+ guestPassword: {
14
+ description: string;
15
+ };
16
+ };
17
+ run({ params: { roomUuid, password, guestPassword }, connection, }: {
18
+ params: ParamsFrom<JoinRoomAction>;
19
+ connection: Connection;
20
+ }): Promise<{
21
+ roomUuid: string;
22
+ allowedRooms: string[];
23
+ didCreate: boolean;
24
+ state: import("../client").RoomState;
25
+ serverTime: number;
26
+ }>;
27
+ }
@@ -0,0 +1,31 @@
1
+ import { Action, ParamsFrom } from "actionhero";
2
+ import { ActionName, MiddlewareName } from "../types/names";
3
+ import { OneTimeAnimationType } from "../types/messages";
4
+ export declare class PlayOneTimeAnimationAction extends Action {
5
+ name: ActionName.playOneTimeAnimation;
6
+ description: string;
7
+ inputs: {
8
+ roomUuid: {
9
+ required: true;
10
+ };
11
+ userUuid: {
12
+ required: true;
13
+ };
14
+ animation: {
15
+ required: true;
16
+ formatter: (animation: OneTimeAnimationType) => OneTimeAnimationType;
17
+ validator: (animation: OneTimeAnimationType) => void;
18
+ description: string;
19
+ };
20
+ emoji: {
21
+ validator: (emojiString: string) => void;
22
+ description: string;
23
+ };
24
+ };
25
+ middleware: MiddlewareName[];
26
+ private animationValidator;
27
+ private emojiValidator;
28
+ run({ params: { roomUuid, userUuid, animation, emoji }, }: {
29
+ params: ParamsFrom<PlayOneTimeAnimationAction>;
30
+ }): Promise<void>;
31
+ }
@@ -0,0 +1,26 @@
1
+ import { Action, ParamsFrom } from "actionhero";
2
+ import { ActionName, MiddlewareName } from "../types/names";
3
+ import { ServerRoomState } from "../types/state";
4
+ export declare class RemoveDjAction extends Action {
5
+ name: ActionName.removeDj;
6
+ description: string;
7
+ inputs: {
8
+ userUuid: {
9
+ required: true;
10
+ description: string;
11
+ };
12
+ roomUuid: {
13
+ required: true;
14
+ };
15
+ tokenRole: {
16
+ formatter: (tokenRole: import("../client").TokenRole) => import("../client").TokenRole;
17
+ };
18
+ djUuid: {
19
+ description: string;
20
+ };
21
+ };
22
+ middleware: MiddlewareName[];
23
+ run({ params: { roomUuid, userUuid, tokenRole, djUuid }, }: {
24
+ params: ParamsFrom<RemoveDjAction>;
25
+ }): Promise<ServerRoomState>;
26
+ }
@@ -0,0 +1,22 @@
1
+ import { Action, ParamsFrom } from "actionhero";
2
+ import { ActionName, MiddlewareName } from "../types/names";
3
+ export declare class SkipSongAction extends Action {
4
+ name: ActionName.skipSong;
5
+ description: string;
6
+ inputs: {
7
+ roomUuid: {
8
+ required: true;
9
+ };
10
+ tokenRole: {
11
+ required: true;
12
+ formatter: (tokenRole: import("../client").TokenRole) => import("../client").TokenRole;
13
+ };
14
+ userUuid: {
15
+ required: true;
16
+ };
17
+ };
18
+ middleware: MiddlewareName[];
19
+ run({ params: { roomUuid, userUuid, tokenRole }, }: {
20
+ params: ParamsFrom<SkipSongAction>;
21
+ }): Promise<void>;
22
+ }
@@ -0,0 +1,22 @@
1
+ import { Action, ParamsFrom } from "actionhero";
2
+ import { ActionName, MiddlewareName } from "../types/names";
3
+ export declare class UpdateNextSongAction extends Action {
4
+ name: ActionName.updateNextSong;
5
+ description: string;
6
+ inputs: {
7
+ roomUuid: {
8
+ required: true;
9
+ };
10
+ userUuid: {
11
+ required: true;
12
+ };
13
+ song: {
14
+ formatter: (song: import("../client").MinimalCrateSongResDTO | null) => import("../client").MinimalCrateSongResDTO | null;
15
+ description: string;
16
+ };
17
+ };
18
+ middleware: MiddlewareName[];
19
+ run({ params: { roomUuid, userUuid, song }, }: {
20
+ params: ParamsFrom<UpdateNextSongAction>;
21
+ }): Promise<import("../types/state").ServerRoomState>;
22
+ }
@@ -0,0 +1,27 @@
1
+ import { Action, ParamsFrom } from "actionhero";
2
+ import { ActionName, MiddlewareName } from "../types/names";
3
+ import { SongVotes } from "../types/state";
4
+ export declare class VoteOnSongAction extends Action {
5
+ name: ActionName.voteOnSong;
6
+ description: string;
7
+ inputs: {
8
+ roomUuid: {
9
+ required: true;
10
+ };
11
+ userUuid: {
12
+ required: true;
13
+ };
14
+ songVotes: {
15
+ required: true;
16
+ formatter: (voteParams: Partial<SongVotes>) => Partial<SongVotes>;
17
+ validator: (songVotes: Partial<SongVotes>) => void;
18
+ description: string;
19
+ };
20
+ };
21
+ middleware: MiddlewareName[];
22
+ private songVotesValidator;
23
+ private areVotesTheSame;
24
+ run({ params: { roomUuid, userUuid, songVotes }, }: {
25
+ params: ParamsFrom<VoteOnSongAction>;
26
+ }): Promise<import("../types/state").ServerRoomState | undefined>;
27
+ }