mirror of
https://github.com/Blah-IM/Weblah.git
synced 2025-07-28 16:02:40 +00:00
refactor: connection module and update identity handling
This commit is contained in:
parent
b05c47037c
commit
fbfcc34102
5 changed files with 97 additions and 42 deletions
|
@ -1,8 +1,16 @@
|
||||||
import { version } from '$app/environment';
|
import { version } from '$app/environment';
|
||||||
import type { BlahRichText } from '@blah-im/core/richText';
|
import type { BlahRichText } from '@blah-im/core/richText';
|
||||||
import type { BlahKeyPair, BlahSignedPayload } from '@blah-im/core/crypto';
|
import type { BlahKeyPair, BlahSignedPayload, SignOptions } from '@blah-im/core/crypto';
|
||||||
import type { BlahAuth, BlahMessage, BlahRoomInfo, BlahUserJoinMessage } from '../structures';
|
import {
|
||||||
|
blahUserUnregisteredResponseSchema,
|
||||||
|
type BlahAuth,
|
||||||
|
type BlahMessage,
|
||||||
|
type BlahRoomInfo,
|
||||||
|
type BlahUserJoinMessage,
|
||||||
|
type BlahUserRegisterRequest
|
||||||
|
} from '../structures';
|
||||||
import { BlahError } from './error';
|
import { BlahError } from './error';
|
||||||
|
import type { BlahIdentity } from '@blah-im/core/identity';
|
||||||
|
|
||||||
const RECONNECT_TIMEOUT = 1500;
|
const RECONNECT_TIMEOUT = 1500;
|
||||||
const RECONNECT_MAX_TRIES = 5;
|
const RECONNECT_MAX_TRIES = 5;
|
||||||
|
@ -19,14 +27,14 @@ export class BlahChatServerConnection {
|
||||||
private static commonHeaders = { 'x-blah-client': `Weblah/${version}` };
|
private static commonHeaders = { 'x-blah-client': `Weblah/${version}` };
|
||||||
|
|
||||||
private endpoint_: string;
|
private endpoint_: string;
|
||||||
private keypair_: BlahKeyPair | null;
|
private identity_: BlahIdentity | null;
|
||||||
|
|
||||||
get endpoint() {
|
get endpoint() {
|
||||||
return this.endpoint_;
|
return this.endpoint_;
|
||||||
}
|
}
|
||||||
|
|
||||||
get keypair() {
|
get identity() {
|
||||||
return this.keypair_;
|
return this.identity_;
|
||||||
}
|
}
|
||||||
|
|
||||||
private webSocket: WebSocket | null = null;
|
private webSocket: WebSocket | null = null;
|
||||||
|
@ -34,15 +42,15 @@ export class BlahChatServerConnection {
|
||||||
private serverListeners: Set<MessageListener> = new Set();
|
private serverListeners: Set<MessageListener> = new Set();
|
||||||
private webSocketRetryTimeout: number | null = null;
|
private webSocketRetryTimeout: number | null = null;
|
||||||
|
|
||||||
constructor(endpoint: string, keypair: BlahKeyPair | null = null) {
|
constructor(endpoint: string, identity: BlahIdentity | null = null) {
|
||||||
this.endpoint_ = endpoint;
|
this.endpoint_ = new URL(endpoint).href.replace(/\/$/, '');
|
||||||
this.keypair_ = keypair;
|
this.identity_ = identity;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async generateAuthHeader(): Promise<{ Authorization: string }> {
|
private async generateAuthHeader(): Promise<{ Authorization: string }> {
|
||||||
if (!this.keypair) throw new Error('Must generate auth header with a keypair');
|
if (!this.identity) throw new Error('Must generate auth header with an identity');
|
||||||
const authPayload: BlahAuth = { typ: 'auth' };
|
const authPayload: BlahAuth = { typ: 'auth' };
|
||||||
const signedAuthPayload = await this.keypair.signPayload(authPayload);
|
const signedAuthPayload = await this.identity.signPayload(authPayload);
|
||||||
return { Authorization: JSON.stringify(signedAuthPayload) };
|
return { Authorization: JSON.stringify(signedAuthPayload) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,11 +65,12 @@ export class BlahChatServerConnection {
|
||||||
private async fetchWithSignedPayload<P>(
|
private async fetchWithSignedPayload<P>(
|
||||||
url: string,
|
url: string,
|
||||||
payload: P,
|
payload: P,
|
||||||
init?: RequestInit
|
init?: RequestInit,
|
||||||
|
signOptions?: Omit<SignOptions, 'identityKeyID'>
|
||||||
): Promise<Response> {
|
): Promise<Response> {
|
||||||
if (!this.keypair) throw new Error('Must fetch with a keypair');
|
if (!this.identity) throw new Error('Must fetch with an identity');
|
||||||
|
|
||||||
const signedPayload = await this.keypair.signPayload(payload);
|
const signedPayload = await this.identity.signPayload(payload, signOptions);
|
||||||
return fetch(url, {
|
return fetch(url, {
|
||||||
...init,
|
...init,
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -73,12 +82,18 @@ export class BlahChatServerConnection {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async apiCall<P, R>(method: 'POST' | 'GET', path: `/${string}`, payload?: P): Promise<R> {
|
public async apiCall<P, R>(
|
||||||
if (payload && !this.keypair) throw new Error('Must make authorized API call with a keypair');
|
method: 'POST' | 'GET',
|
||||||
|
path: `/${string}`,
|
||||||
|
payload?: P,
|
||||||
|
signOptions?: Omit<SignOptions, 'identityKeyID'>
|
||||||
|
): Promise<R> {
|
||||||
|
if (payload && !this.identity)
|
||||||
|
throw new Error('Must make authorized API call with an identity');
|
||||||
|
|
||||||
let response: Response;
|
let response: Response;
|
||||||
if (method === 'GET') {
|
if (method === 'GET') {
|
||||||
if (this.keypair) {
|
if (this.identity) {
|
||||||
response = await this.fetchWithAuthHeader(`${this.endpoint_}${path}`);
|
response = await this.fetchWithAuthHeader(`${this.endpoint_}${path}`);
|
||||||
} else {
|
} else {
|
||||||
response = await fetch(`${this.endpoint_}${path}`, {
|
response = await fetch(`${this.endpoint_}${path}`, {
|
||||||
|
@ -86,7 +101,12 @@ export class BlahChatServerConnection {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
response = await this.fetchWithSignedPayload(`${this.endpoint_}${path}`, payload, { method });
|
response = await this.fetchWithSignedPayload(
|
||||||
|
`${this.endpoint_}${path}`,
|
||||||
|
payload,
|
||||||
|
{ method },
|
||||||
|
signOptions
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!response.ok) throw await BlahError.fromResponse(response);
|
if (!response.ok) throw await BlahError.fromResponse(response);
|
||||||
|
@ -94,13 +114,28 @@ export class BlahChatServerConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
async tryRegisterIfNoyYet(): Promise<void> {
|
async tryRegisterIfNoyYet(): Promise<void> {
|
||||||
if (!this.keypair) throw new Error('Must register with a keypair');
|
if (!this.identity) throw new Error('Must register with an identity');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.apiCall('GET', '/user/me');
|
await this.apiCall('GET', '/user/me');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof BlahError && e.statusCode === 404) {
|
if (e instanceof BlahError && e.statusCode === 404) {
|
||||||
// TODO: Register user
|
const { data } = blahUserUnregisteredResponseSchema.safeParse(e.raw);
|
||||||
|
if (!data) throw e;
|
||||||
|
|
||||||
|
const request: BlahUserRegisterRequest = {
|
||||||
|
typ: 'user_register',
|
||||||
|
server_url: this.endpoint_,
|
||||||
|
id_url: this.identity.profile.id_urls[0],
|
||||||
|
id_key: this.identity.idPublicKey.id,
|
||||||
|
challenge: {
|
||||||
|
pow: { nonce: data.register_challenge.pow.nonce }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await this.apiCall('POST', '/user/register', request, {
|
||||||
|
powDifficulty: data.register_challenge.pow.difficulty
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
|
@ -122,7 +157,7 @@ export class BlahChatServerConnection {
|
||||||
socket.addEventListener('close', onSocketClose);
|
socket.addEventListener('close', onSocketClose);
|
||||||
|
|
||||||
socket.addEventListener('open', async () => {
|
socket.addEventListener('open', async () => {
|
||||||
if (this.keypair) {
|
if (this.identity) {
|
||||||
const { Authorization } = await this.generateAuthHeader();
|
const { Authorization } = await this.generateAuthHeader();
|
||||||
socket.send(Authorization);
|
socket.send(Authorization);
|
||||||
}
|
}
|
||||||
|
@ -146,7 +181,7 @@ export class BlahChatServerConnection {
|
||||||
}
|
}
|
||||||
|
|
||||||
connect() {
|
connect() {
|
||||||
if (!this.webSocket && this.keypair) this.webSocket = this.createWebSocket();
|
if (!this.webSocket && this.identity) this.webSocket = this.createWebSocket();
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
|
@ -155,8 +190,8 @@ export class BlahChatServerConnection {
|
||||||
this.webSocket = null;
|
this.webSocket = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
changeKeyPair(keypair: BlahKeyPair | null) {
|
changeIdentity(newIdentity: BlahIdentity | null) {
|
||||||
this.keypair_ = keypair;
|
this.identity_ = newIdentity;
|
||||||
if (this.webSocket) {
|
if (this.webSocket) {
|
||||||
this.disconnect();
|
this.disconnect();
|
||||||
this.connect();
|
this.connect();
|
4
src/lib/blah/connection/index.ts
Normal file
4
src/lib/blah/connection/index.ts
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
export * from './connection';
|
||||||
|
export * from './error';
|
||||||
|
export * from './messageManager.svelte';
|
||||||
|
export * from './roomManager.svelte';
|
|
@ -1,10 +1,10 @@
|
||||||
import type { BlahRichText } from '@blah-im/core/richText';
|
import type { BlahRichText } from '@blah-im/core/richText';
|
||||||
import type { BlahSignedPayload } from '@blah-im/core/crypto';
|
import type { BlahSignedPayload } from '@blah-im/core/crypto';
|
||||||
|
|
||||||
import type { BlahChatServerConnection } from './chatServer';
|
import type { BlahChatServerConnection } from './connection';
|
||||||
import type { BlahMessage } from '../structures';
|
import type { BlahMessage } from '../structures';
|
||||||
|
|
||||||
export default class MessageManager {
|
export class MessageManager {
|
||||||
connection: BlahChatServerConnection;
|
connection: BlahChatServerConnection;
|
||||||
roomID: string;
|
roomID: string;
|
||||||
messages: BlahSignedPayload<BlahMessage>[] = $state([]);
|
messages: BlahSignedPayload<BlahMessage>[] = $state([]);
|
||||||
|
@ -15,7 +15,7 @@ export default class MessageManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendMessage(message: BlahRichText): Promise<void> {
|
async sendMessage(message: BlahRichText): Promise<void> {
|
||||||
if (!this.connection.keypair) throw new Error('Must send message with a keypair');
|
if (!this.connection.identity) throw new Error('Must send message with a keypair');
|
||||||
const payload: BlahMessage = { room: this.roomID, rich_text: message, typ: 'chat' };
|
const payload: BlahMessage = { room: this.roomID, rich_text: message, typ: 'chat' };
|
||||||
await this.connection.apiCall('POST', `/room/${payload.room}/item`, payload);
|
await this.connection.apiCall('POST', `/room/${payload.room}/item`, payload);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import type { BlahRoomInfo, BlahUserJoinMessage } from '../structures';
|
import type { BlahRoomInfo, BlahUserJoinMessage } from '../structures';
|
||||||
import type { BlahChatServerConnection } from './chatServer';
|
import type { BlahChatServerConnection } from './connection';
|
||||||
|
|
||||||
export default class RoomManager {
|
export class RoomManager {
|
||||||
connection: BlahChatServerConnection;
|
connection: BlahChatServerConnection;
|
||||||
joinedRooms: BlahRoomInfo[] = $state([]);
|
joinedRooms: BlahRoomInfo[] = $state([]);
|
||||||
publicRooms: BlahRoomInfo[] = $state([]);
|
publicRooms: BlahRoomInfo[] = $state([]);
|
||||||
|
@ -11,14 +11,14 @@ export default class RoomManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
async joinRoom(id: string): Promise<void> {
|
async joinRoom(id: string): Promise<void> {
|
||||||
const keypair = this.connection.keypair;
|
const identity = this.connection.identity;
|
||||||
if (!keypair) throw new Error('Must join with a keypair');
|
if (!identity) throw new Error('Must join with an identity');
|
||||||
|
|
||||||
const payload: BlahUserJoinMessage = {
|
const payload: BlahUserJoinMessage = {
|
||||||
typ: 'add_member',
|
typ: 'add_member',
|
||||||
room: id,
|
room: id,
|
||||||
permission: 1,
|
permission: 1,
|
||||||
user: keypair.id
|
user: identity.idPublicKey.id
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.connection.apiCall('POST', `/room/${id}/admin`, payload);
|
await this.connection.apiCall('POST', `/room/${id}/admin`, payload);
|
||||||
|
@ -33,7 +33,7 @@ export default class RoomManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchJoinedRooms() {
|
async fetchJoinedRooms() {
|
||||||
if (!this.connection.keypair) return [];
|
if (!this.connection.identity) return [];
|
||||||
this.joinedRooms = await this.fetchRoomList('joined');
|
this.joinedRooms = await this.fetchRoomList('joined');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,16 +2,32 @@ import { z } from 'zod';
|
||||||
|
|
||||||
export type BlahAuth = { typ: 'auth' };
|
export type BlahAuth = { typ: 'auth' };
|
||||||
|
|
||||||
export type BlahUserRegisterChallenge = {
|
export const blahUserUnregisteredResponseSchema = z.object({
|
||||||
pow: {
|
register_challenge: z.object({
|
||||||
nonce: number;
|
pow: z.object({
|
||||||
difficulty: number;
|
nonce: z.number().int(),
|
||||||
|
difficulty: z.number().int()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BlahUserUnregisteredResponse = {
|
||||||
|
register_challenge: {
|
||||||
|
pow: {
|
||||||
|
nonce: number;
|
||||||
|
difficulty: number;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const blahUserRegisterChallengeSchema = z.object({
|
export type BlahUserRegisterRequest = {
|
||||||
pow: z.object({
|
typ: 'user_register';
|
||||||
nonce: z.number().int(),
|
server_url: string;
|
||||||
difficulty: z.number().int()
|
id_url: string;
|
||||||
})
|
id_key: string;
|
||||||
});
|
challenge: {
|
||||||
|
pow: {
|
||||||
|
nonce: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue