mirror of
https://github.com/Blah-IM/Weblah.git
synced 2025-06-01 22:51:09 +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 type { BlahRichText } from '@blah-im/core/richText';
|
||||
import type { BlahKeyPair, BlahSignedPayload } from '@blah-im/core/crypto';
|
||||
import type { BlahAuth, BlahMessage, BlahRoomInfo, BlahUserJoinMessage } from '../structures';
|
||||
import type { BlahKeyPair, BlahSignedPayload, SignOptions } from '@blah-im/core/crypto';
|
||||
import {
|
||||
blahUserUnregisteredResponseSchema,
|
||||
type BlahAuth,
|
||||
type BlahMessage,
|
||||
type BlahRoomInfo,
|
||||
type BlahUserJoinMessage,
|
||||
type BlahUserRegisterRequest
|
||||
} from '../structures';
|
||||
import { BlahError } from './error';
|
||||
import type { BlahIdentity } from '@blah-im/core/identity';
|
||||
|
||||
const RECONNECT_TIMEOUT = 1500;
|
||||
const RECONNECT_MAX_TRIES = 5;
|
||||
|
@ -19,14 +27,14 @@ export class BlahChatServerConnection {
|
|||
private static commonHeaders = { 'x-blah-client': `Weblah/${version}` };
|
||||
|
||||
private endpoint_: string;
|
||||
private keypair_: BlahKeyPair | null;
|
||||
private identity_: BlahIdentity | null;
|
||||
|
||||
get endpoint() {
|
||||
return this.endpoint_;
|
||||
}
|
||||
|
||||
get keypair() {
|
||||
return this.keypair_;
|
||||
get identity() {
|
||||
return this.identity_;
|
||||
}
|
||||
|
||||
private webSocket: WebSocket | null = null;
|
||||
|
@ -34,15 +42,15 @@ export class BlahChatServerConnection {
|
|||
private serverListeners: Set<MessageListener> = new Set();
|
||||
private webSocketRetryTimeout: number | null = null;
|
||||
|
||||
constructor(endpoint: string, keypair: BlahKeyPair | null = null) {
|
||||
this.endpoint_ = endpoint;
|
||||
this.keypair_ = keypair;
|
||||
constructor(endpoint: string, identity: BlahIdentity | null = null) {
|
||||
this.endpoint_ = new URL(endpoint).href.replace(/\/$/, '');
|
||||
this.identity_ = identity;
|
||||
}
|
||||
|
||||
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 signedAuthPayload = await this.keypair.signPayload(authPayload);
|
||||
const signedAuthPayload = await this.identity.signPayload(authPayload);
|
||||
return { Authorization: JSON.stringify(signedAuthPayload) };
|
||||
}
|
||||
|
||||
|
@ -57,11 +65,12 @@ export class BlahChatServerConnection {
|
|||
private async fetchWithSignedPayload<P>(
|
||||
url: string,
|
||||
payload: P,
|
||||
init?: RequestInit
|
||||
init?: RequestInit,
|
||||
signOptions?: Omit<SignOptions, 'identityKeyID'>
|
||||
): 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, {
|
||||
...init,
|
||||
headers: {
|
||||
|
@ -73,12 +82,18 @@ export class BlahChatServerConnection {
|
|||
});
|
||||
}
|
||||
|
||||
public async apiCall<P, R>(method: 'POST' | 'GET', path: `/${string}`, payload?: P): Promise<R> {
|
||||
if (payload && !this.keypair) throw new Error('Must make authorized API call with a keypair');
|
||||
public async apiCall<P, R>(
|
||||
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;
|
||||
if (method === 'GET') {
|
||||
if (this.keypair) {
|
||||
if (this.identity) {
|
||||
response = await this.fetchWithAuthHeader(`${this.endpoint_}${path}`);
|
||||
} else {
|
||||
response = await fetch(`${this.endpoint_}${path}`, {
|
||||
|
@ -86,7 +101,12 @@ export class BlahChatServerConnection {
|
|||
});
|
||||
}
|
||||
} 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);
|
||||
|
@ -94,13 +114,28 @@ export class BlahChatServerConnection {
|
|||
}
|
||||
|
||||
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 {
|
||||
await this.apiCall('GET', '/user/me');
|
||||
} catch (e) {
|
||||
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 {
|
||||
throw e;
|
||||
}
|
||||
|
@ -122,7 +157,7 @@ export class BlahChatServerConnection {
|
|||
socket.addEventListener('close', onSocketClose);
|
||||
|
||||
socket.addEventListener('open', async () => {
|
||||
if (this.keypair) {
|
||||
if (this.identity) {
|
||||
const { Authorization } = await this.generateAuthHeader();
|
||||
socket.send(Authorization);
|
||||
}
|
||||
|
@ -146,7 +181,7 @@ export class BlahChatServerConnection {
|
|||
}
|
||||
|
||||
connect() {
|
||||
if (!this.webSocket && this.keypair) this.webSocket = this.createWebSocket();
|
||||
if (!this.webSocket && this.identity) this.webSocket = this.createWebSocket();
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
|
@ -155,8 +190,8 @@ export class BlahChatServerConnection {
|
|||
this.webSocket = null;
|
||||
}
|
||||
|
||||
changeKeyPair(keypair: BlahKeyPair | null) {
|
||||
this.keypair_ = keypair;
|
||||
changeIdentity(newIdentity: BlahIdentity | null) {
|
||||
this.identity_ = newIdentity;
|
||||
if (this.webSocket) {
|
||||
this.disconnect();
|
||||
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 { BlahSignedPayload } from '@blah-im/core/crypto';
|
||||
|
||||
import type { BlahChatServerConnection } from './chatServer';
|
||||
import type { BlahChatServerConnection } from './connection';
|
||||
import type { BlahMessage } from '../structures';
|
||||
|
||||
export default class MessageManager {
|
||||
export class MessageManager {
|
||||
connection: BlahChatServerConnection;
|
||||
roomID: string;
|
||||
messages: BlahSignedPayload<BlahMessage>[] = $state([]);
|
||||
|
@ -15,7 +15,7 @@ export default class MessageManager {
|
|||
}
|
||||
|
||||
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' };
|
||||
await this.connection.apiCall('POST', `/room/${payload.room}/item`, payload);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
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;
|
||||
joinedRooms: BlahRoomInfo[] = $state([]);
|
||||
publicRooms: BlahRoomInfo[] = $state([]);
|
||||
|
@ -11,14 +11,14 @@ export default class RoomManager {
|
|||
}
|
||||
|
||||
async joinRoom(id: string): Promise<void> {
|
||||
const keypair = this.connection.keypair;
|
||||
if (!keypair) throw new Error('Must join with a keypair');
|
||||
const identity = this.connection.identity;
|
||||
if (!identity) throw new Error('Must join with an identity');
|
||||
|
||||
const payload: BlahUserJoinMessage = {
|
||||
typ: 'add_member',
|
||||
room: id,
|
||||
permission: 1,
|
||||
user: keypair.id
|
||||
user: identity.idPublicKey.id
|
||||
};
|
||||
|
||||
await this.connection.apiCall('POST', `/room/${id}/admin`, payload);
|
||||
|
@ -33,7 +33,7 @@ export default class RoomManager {
|
|||
}
|
||||
|
||||
async fetchJoinedRooms() {
|
||||
if (!this.connection.keypair) return [];
|
||||
if (!this.connection.identity) return [];
|
||||
this.joinedRooms = await this.fetchRoomList('joined');
|
||||
}
|
||||
|
||||
|
|
|
@ -2,16 +2,32 @@ import { z } from 'zod';
|
|||
|
||||
export type BlahAuth = { typ: 'auth' };
|
||||
|
||||
export type BlahUserRegisterChallenge = {
|
||||
pow: {
|
||||
nonce: number;
|
||||
difficulty: number;
|
||||
export const blahUserUnregisteredResponseSchema = z.object({
|
||||
register_challenge: z.object({
|
||||
pow: z.object({
|
||||
nonce: z.number().int(),
|
||||
difficulty: z.number().int()
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
export type BlahUserUnregisteredResponse = {
|
||||
register_challenge: {
|
||||
pow: {
|
||||
nonce: number;
|
||||
difficulty: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export const blahUserRegisterChallengeSchema = z.object({
|
||||
pow: z.object({
|
||||
nonce: z.number().int(),
|
||||
difficulty: z.number().int()
|
||||
})
|
||||
});
|
||||
export type BlahUserRegisterRequest = {
|
||||
typ: 'user_register';
|
||||
server_url: string;
|
||||
id_url: string;
|
||||
id_key: string;
|
||||
challenge: {
|
||||
pow: {
|
||||
nonce: number;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue