mirror of
https://github.com/stoatchat/javascript-client-sdk.git
synced 2026-07-20 20:16:06 -04:00
Add websocket.
This commit is contained in:
+22
-7
@@ -2,15 +2,20 @@ import Axios, { AxiosInstance } from 'axios';
|
||||
import { defaultsDeep } from 'lodash';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { defaultConfig } from '.';
|
||||
import { Core } from './api/core';
|
||||
import { Auth } from './api/auth';
|
||||
import { Onboarding } from './api/onboarding';
|
||||
import { defaultConfig } from '.';
|
||||
import { runInThisContext } from 'vm';
|
||||
import { WebSocketClient } from './websocket/client';
|
||||
|
||||
import User from './objects/User';
|
||||
import { Users } from './api/users';
|
||||
|
||||
export interface ClientOptions {
|
||||
apiURL: string,
|
||||
wsURL: string
|
||||
wsURL: string,
|
||||
|
||||
autoReconnect: boolean
|
||||
}
|
||||
|
||||
export declare interface Client {
|
||||
@@ -23,13 +28,21 @@ export declare interface Client {
|
||||
export class Client extends EventEmitter {
|
||||
Axios: AxiosInstance;
|
||||
options: ClientOptions;
|
||||
websocket: WebSocketClient;
|
||||
configuration?: Core.RevoltNodeConfiguration;
|
||||
session?: Auth.Session;
|
||||
|
||||
users: Map<string, User>;
|
||||
user?: User;
|
||||
|
||||
constructor(options: Partial<ClientOptions> = {}) {
|
||||
super();
|
||||
|
||||
this.options = defaultsDeep(options, defaultConfig);
|
||||
this.Axios = Axios.create({ baseURL: this.options.apiURL });
|
||||
this.websocket = new WebSocketClient(this);
|
||||
|
||||
this.users = new Map();
|
||||
}
|
||||
|
||||
// Stage 1: Connect to Revolt.
|
||||
@@ -37,12 +50,12 @@ export class Client extends EventEmitter {
|
||||
this.configuration = (await this.Axios.get('/')).data;
|
||||
}
|
||||
|
||||
$checkConfiguration() {
|
||||
private $checkConfiguration() {
|
||||
if (typeof this.configuration === 'undefined')
|
||||
throw new Error("No configuration synced from Revolt node yet. Use client.connect();");
|
||||
}
|
||||
|
||||
$generateHeaders(session: Auth.Session | undefined = this.session) {
|
||||
private $generateHeaders(session: Auth.Session | undefined = this.session) {
|
||||
return {
|
||||
'x-user-id': session?.user_id,
|
||||
'x-session-token': session?.session_token
|
||||
@@ -51,26 +64,28 @@ export class Client extends EventEmitter {
|
||||
|
||||
// Login to Revolt.
|
||||
async login(details: Auth.LoginRequest) {
|
||||
this.$checkConfiguration();
|
||||
this.session = (await this.Axios.post('/auth/login', details)).data;
|
||||
return await this.$connect();
|
||||
}
|
||||
|
||||
// Use an existing session to log into Revolt.
|
||||
async useExistingSession(session: Auth.Session) {
|
||||
this.$checkConfiguration();
|
||||
await this.Axios.get('/auth/check', { headers: this.$generateHeaders(session) });
|
||||
this.session = session;
|
||||
return await this.$connect();
|
||||
}
|
||||
|
||||
// Check onboarding status and connect to notifications service.
|
||||
async $connect() {
|
||||
private async $connect() {
|
||||
this.Axios.defaults.headers = this.$generateHeaders();
|
||||
let { onboarding } = (await this.Axios.get('/onboard/hello')).data;
|
||||
if (onboarding) {
|
||||
return (username: string) => this.completeOnboarding({ username });
|
||||
}
|
||||
|
||||
console.log((await this.Axios.get('/users/' + this.session?.user_id)).data);
|
||||
await this.websocket.connect();
|
||||
}
|
||||
|
||||
// Complete onboarding if required.
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
export namespace Account {
|
||||
// POST /create
|
||||
export interface CreateRequest {
|
||||
username: string,
|
||||
password: string,
|
||||
email: string,
|
||||
captcha?: string,
|
||||
}
|
||||
|
||||
export interface CreateResponse {
|
||||
email_sent?: boolean,
|
||||
}
|
||||
|
||||
// POST /login
|
||||
export interface LoginRequest {
|
||||
email: string,
|
||||
password: string,
|
||||
captcha?: string,
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
access_token: string,
|
||||
id: string,
|
||||
}
|
||||
|
||||
// POST /resend
|
||||
export interface ResendRequest {
|
||||
email: string,
|
||||
captcha?: string,
|
||||
}
|
||||
|
||||
export interface ResendResponse { }
|
||||
|
||||
// POST /token
|
||||
export interface TokenRequest {
|
||||
token: string,
|
||||
}
|
||||
|
||||
export interface TokenResponse {
|
||||
id: string,
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
export enum ChannelType {
|
||||
DM = 0,
|
||||
GROUPDM = 1,
|
||||
GUILDCHANNEL = 2,
|
||||
}
|
||||
|
||||
export interface LastMessage {
|
||||
id: string,
|
||||
user_id: string,
|
||||
short_content: string
|
||||
}
|
||||
|
||||
export type RawChannel = {
|
||||
id: string
|
||||
} & (
|
||||
({
|
||||
type: ChannelType.DM | ChannelType.GROUPDM,
|
||||
recipients: string[],
|
||||
last_message: LastMessage
|
||||
} & (
|
||||
{ type: ChannelType.DM } |
|
||||
{
|
||||
type: ChannelType.GROUPDM,
|
||||
recipients: string[],
|
||||
owner: string,
|
||||
name: string,
|
||||
description: string,
|
||||
}
|
||||
))
|
||||
|
|
||||
{
|
||||
type: ChannelType.GUILDCHANNEL,
|
||||
guild: string,
|
||||
name: string,
|
||||
description: string
|
||||
}
|
||||
);
|
||||
|
||||
export type RawMessage = {
|
||||
id: string,
|
||||
nonce?: string,
|
||||
author: string,
|
||||
content: string,
|
||||
edited: number | null,
|
||||
};
|
||||
|
||||
export namespace Channels {
|
||||
// GET /:id
|
||||
export type ChannelResponse = RawChannel;
|
||||
|
||||
// DELETE /:id
|
||||
export interface DeleteChannelResponse { };
|
||||
|
||||
// GET /:id/messages
|
||||
export type MessagesResponse = RawMessage[];
|
||||
|
||||
// POST /:id/messages
|
||||
export interface SendMessageRequest {
|
||||
content: string,
|
||||
nonce: string,
|
||||
}
|
||||
|
||||
export interface SendMessageResponse {
|
||||
id: string,
|
||||
}
|
||||
|
||||
// GET /:id/messages/:mid
|
||||
export type MessageResponse = RawMessage;
|
||||
|
||||
// PATCH /:id/messages/:mid
|
||||
export interface EditMessageRequest {
|
||||
content: string,
|
||||
}
|
||||
|
||||
export interface EditMessageResponse { }
|
||||
|
||||
// DELETE /:id/messages/:mid
|
||||
export interface DeleteMessageResponse { }
|
||||
|
||||
// POST /create
|
||||
export interface CreateGroupRequest {
|
||||
name: string,
|
||||
nonce: string,
|
||||
users: string[]
|
||||
}
|
||||
|
||||
export interface CreateGroupResponse { }
|
||||
|
||||
// PUT /:id/recipients/:uid
|
||||
export interface AddToGroupResponse { }
|
||||
|
||||
// DELETE /:id/recipients/:uid
|
||||
export interface RemoveFromGroupResponse { }
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import { RawChannel } from "./channels";
|
||||
|
||||
export interface CoreGuild {
|
||||
id: string,
|
||||
name: string,
|
||||
description: string,
|
||||
owner: string,
|
||||
channels: RawChannel[]
|
||||
}
|
||||
|
||||
export namespace Guild {
|
||||
// GET /@me
|
||||
export type GuildsResponse = CoreGuild[];
|
||||
|
||||
// GET /:id
|
||||
export type GuildResponse = CoreGuild;
|
||||
|
||||
// DELETE /:id
|
||||
export interface GuildDeleteResponse { }
|
||||
|
||||
// POST /create
|
||||
export interface CreateGuildRequest {
|
||||
name: string,
|
||||
description?: string,
|
||||
nonce: string
|
||||
}
|
||||
|
||||
export interface CreateGuildResponse {
|
||||
id: string
|
||||
}
|
||||
|
||||
// POST /:id/channels
|
||||
export interface CreateChannelRequest {
|
||||
name: string,
|
||||
description?: string,
|
||||
nonce: string
|
||||
}
|
||||
|
||||
export interface CreateChannelResponse {
|
||||
id: string
|
||||
}
|
||||
|
||||
// GET /:id/members
|
||||
export type MembersResponse = MemberResponse[];
|
||||
|
||||
// GET /:id/members/:uid
|
||||
export interface MemberResponse {
|
||||
id: string,
|
||||
nickname?: string
|
||||
}
|
||||
|
||||
// DELETE /:id/members/:uid
|
||||
export interface KickMemberResponse { }
|
||||
|
||||
// PUT /:id/members/:uid/ban
|
||||
export interface BanMemberResponse { }
|
||||
|
||||
// DELETE /:id/members/:uid/ban
|
||||
export interface UnbanMemberResponse { }
|
||||
|
||||
// POST /:id/channels/:cid/invite
|
||||
export interface CreateInviteRequest { }
|
||||
|
||||
export interface CreateInviteResponse {
|
||||
code: string
|
||||
}
|
||||
|
||||
// GET /join/:code
|
||||
export interface InvitePreviewResponse {
|
||||
guild: {
|
||||
id: string,
|
||||
name: string
|
||||
},
|
||||
channel: {
|
||||
id: string,
|
||||
name: string
|
||||
}
|
||||
}
|
||||
|
||||
// POST /join/:code
|
||||
export interface AcceptInviteResponse {
|
||||
guild: string,
|
||||
channel: string
|
||||
}
|
||||
|
||||
// GET /:id/invites
|
||||
export type InvitesResponse = {
|
||||
code: string,
|
||||
creator: string,
|
||||
channel: string
|
||||
}[]
|
||||
|
||||
// DELETE /:id/invites/:code
|
||||
export interface DeleteInviteResponse { }
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import { Relationship, Users } from './users';
|
||||
import { RawChannel } from './channels';
|
||||
import { CoreGuild } from './guild';
|
||||
|
||||
export * from './account';
|
||||
export * from './users';
|
||||
export * from './channels';
|
||||
export * from './guild';
|
||||
|
||||
export namespace WebsocketPackets {
|
||||
export interface ready {
|
||||
channels: RawChannel[],
|
||||
guilds: CoreGuild[],
|
||||
user: Users.UserResponse,
|
||||
users: Users.UserResponse[]
|
||||
}
|
||||
|
||||
export interface message_create {
|
||||
id: string,
|
||||
nonce?: string,
|
||||
channel: string,
|
||||
author: string,
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface message_edit {
|
||||
id: string,
|
||||
channel: string,
|
||||
author: string,
|
||||
content: string
|
||||
}
|
||||
|
||||
export interface message_delete {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface group_user_join {
|
||||
id: string,
|
||||
user: string
|
||||
}
|
||||
|
||||
export interface group_user_leave {
|
||||
id: string,
|
||||
user: string
|
||||
}
|
||||
|
||||
export interface guild_user_join {
|
||||
id: string,
|
||||
user: string
|
||||
}
|
||||
|
||||
export interface guild_user_leave {
|
||||
id: string,
|
||||
user: string
|
||||
}
|
||||
|
||||
export interface guild_channel_create {
|
||||
id: string,
|
||||
channel: string,
|
||||
name: string,
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface guild_channel_delete {
|
||||
id: string,
|
||||
channel: string
|
||||
}
|
||||
|
||||
export interface guild_delete {
|
||||
id: string
|
||||
}
|
||||
|
||||
export interface user_friend_status {
|
||||
id: string,
|
||||
user: string,
|
||||
status: Relationship
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { RawChannel } from "./channels";
|
||||
|
||||
export enum Relationship {
|
||||
FRIEND = 0,
|
||||
OUTGOING = 1,
|
||||
INCOMING = 2,
|
||||
BLOCKED = 3,
|
||||
BLOCKED_OTHER = 4,
|
||||
NONE = 5,
|
||||
SELF = 6,
|
||||
}
|
||||
|
||||
export namespace Users {
|
||||
// GET /@me or /:id
|
||||
export interface UserResponse {
|
||||
id: string,
|
||||
username: string,
|
||||
display_name: string,
|
||||
email?: string,
|
||||
verified?: boolean,
|
||||
relationship?: Relationship
|
||||
}
|
||||
|
||||
// POST /query
|
||||
export interface QueryRequest {
|
||||
username: string,
|
||||
}
|
||||
|
||||
export type QueryResponse = UserResponse;
|
||||
|
||||
// GET /@me/dms
|
||||
export type DMsResponse = RawChannel[];
|
||||
|
||||
// GET /:id/dm
|
||||
export interface OpenDMResponse {
|
||||
id: string
|
||||
}
|
||||
|
||||
// GET /@me/friend
|
||||
export type FriendsResponse = FriendResponse[];
|
||||
|
||||
// GET /:id/friend
|
||||
export interface FriendResponse {
|
||||
id: string,
|
||||
status: Relationship,
|
||||
}
|
||||
|
||||
// PUT /:id/friend
|
||||
export interface AddFriendResponse {
|
||||
status: Relationship,
|
||||
}
|
||||
|
||||
// DELETE /:id/friend
|
||||
export interface RemoveFriendResponse {
|
||||
status: Relationship,
|
||||
}
|
||||
|
||||
// PUT /:id/block
|
||||
export interface BlockUserResponse {
|
||||
status: Relationship,
|
||||
}
|
||||
|
||||
// DELETE /:id/block
|
||||
export interface UnblockUserResponse {
|
||||
status: Relationship,
|
||||
}
|
||||
}
|
||||
@@ -7,3 +7,14 @@ export enum Relationship {
|
||||
Blocked = "Blocked",
|
||||
BlockedOther = "BlockedOther",
|
||||
}
|
||||
|
||||
export type Relationships = { _id: string, status: Relationship }[];
|
||||
|
||||
export namespace Users {
|
||||
// GET /:id
|
||||
export interface User {
|
||||
_id: string,
|
||||
username: string,
|
||||
relations?: Relationships
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -16,5 +16,7 @@ export const LIBRARY_VERSION = {
|
||||
|
||||
export const defaultConfig = {
|
||||
apiURL: 'https://api.revolt.chat',
|
||||
wsURL: 'wss://api.revolt.chat/ws'
|
||||
wsURL: 'wss://ws.revolt.chat',
|
||||
|
||||
autoReconnect: true
|
||||
};
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Client } from '..';
|
||||
import { Users, Relationships } from '../api/users';
|
||||
|
||||
export default class User {
|
||||
client: Client;
|
||||
id: string;
|
||||
username: string;
|
||||
|
||||
_relations?: Relationships;
|
||||
|
||||
constructor(client: Client, data: Users.User) {
|
||||
this.client = client;
|
||||
this.id = data._id;
|
||||
this.username = data.username;
|
||||
|
||||
this._relations = data.relations;
|
||||
}
|
||||
|
||||
static async fetch(client: Client, id: string, data?: Users.User): Promise<User> {
|
||||
let existing;
|
||||
if (existing = client.users.get(id)) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
let user = new User(client, data ?? (await client.Axios.get(`/users/${id}`)).data);
|
||||
client.users.set(id, user);
|
||||
|
||||
return user;
|
||||
}
|
||||
}
|
||||
+12
-4
@@ -4,13 +4,21 @@ config();
|
||||
import { Client } from "./Client";
|
||||
let client = new Client();
|
||||
|
||||
client.on('ready', () => {
|
||||
console.log(`Logged in as @${client.user?.username}`);
|
||||
});
|
||||
|
||||
(async () => {
|
||||
console.log('Start:', new Date());
|
||||
|
||||
await client.connect();
|
||||
let onboarding = await client.login({ email: 'mink3@insrt.uk', password: 'password', device_name: 'aaa' });
|
||||
if (onboarding) {
|
||||
await onboarding("poggers");
|
||||
try {
|
||||
await client.connect();
|
||||
let onboarding = await client.login({ email: 'mink@insrt.uk', password: 'password', device_name: 'aaa' });
|
||||
if (onboarding) {
|
||||
await onboarding("username");
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
|
||||
console.log('End: ', new Date());
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import WebSocket from 'isomorphic-ws';
|
||||
|
||||
import { Client } from '..';
|
||||
import { Auth } from '../api/auth';
|
||||
import { ServerboundNotification, ClientboundNotification } from './notifications';
|
||||
|
||||
import User from '../objects/User';
|
||||
|
||||
export class WebSocketClient {
|
||||
client: Client;
|
||||
ws?: WebSocket;
|
||||
|
||||
constructor(client: Client) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
connect(): Promise<void> {
|
||||
return new Promise((resolve, $reject) => {
|
||||
let thrown = false;
|
||||
const reject = (err: any) => {
|
||||
if (!thrown) {
|
||||
thrown = true;
|
||||
$reject(err);
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof this.ws !== 'undefined' && this.ws.readyState === WebSocket.OPEN) {
|
||||
this.ws.close();
|
||||
}
|
||||
|
||||
if (typeof this.client.session === 'undefined') {
|
||||
throw new Error("Attempted to open WebSocket without valid session.");
|
||||
}
|
||||
|
||||
let ws = new WebSocket(this.client.options.wsURL);
|
||||
const send = (notification: ServerboundNotification) => ws.send(JSON.stringify(notification));
|
||||
|
||||
ws.onopen = () => {
|
||||
send({ type: 'Authenticate', ...this.client.session as Auth.Session });
|
||||
};
|
||||
|
||||
ws.onmessage = async (msg) => {
|
||||
let data = msg.data;
|
||||
if (typeof data !== 'string') return;
|
||||
|
||||
let packet = JSON.parse(data) as ClientboundNotification;
|
||||
switch (packet.type) {
|
||||
case 'Error': reject(packet.error); break;
|
||||
case 'Authenticated': this.client.emit('connected'); break;
|
||||
case 'Ready': {
|
||||
this.client.user = await User.fetch(this.client, packet.user._id, packet.user);
|
||||
|
||||
this.client.emit('ready');
|
||||
resolve();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = (err) => {
|
||||
reject(err);
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
this.client.emit('dropped');
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Auth } from '../api/auth';
|
||||
import { Relationship, Users } from '../api/users';
|
||||
|
||||
type WebSocketError = {
|
||||
error: 'InternalError' | 'InvalidSession' | 'OnboardingNotFinished' | 'AlreadyAuthenticated'
|
||||
};
|
||||
|
||||
export type ServerboundNotification = (
|
||||
({ type: 'Authenticate' } & Auth.Session)
|
||||
);
|
||||
|
||||
export type ClientboundNotification = (
|
||||
({ type: 'Error' } & WebSocketError) |
|
||||
{ type: 'Authenticated' } |
|
||||
{ type: 'Ready', user: Users.User } |
|
||||
{ type: 'UserRelationship', user: string, status: Relationship }
|
||||
)
|
||||
Reference in New Issue
Block a user