cetirizine

An experimental matrix client written in reactjs utilizing tailwind and storybook
git clone git://archive.git.mtrnord.blog/MTRNord/cetirizine.git
Log | Files | Refs | README | LICENSE

commit d6c587f5c12d2c4458502e9162926cf196870931
parent e9c6b3c409a9b4377202db77c6faa035be6d06c9
Author: MTRNord <mtrnord1@gmail.com>
Date:   Fri,  5 May 2023 00:03:10 +0200

Initialize rust-crypto and add send button (missing actual function for now)

Diffstat:
Msrc/App.tsx | 5-----
Msrc/app/sdk/client.ts | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Msrc/components/input/chat/input.tsx | 2+-
Msrc/main.tsx | 31+++++++++++++++++++------------
Msrc/pages/MainPage.tsx | 58+++++++++++++++++++++++++++++++++-------------------------
5 files changed, 207 insertions(+), 46 deletions(-)

diff --git a/src/App.tsx b/src/App.tsx @@ -7,11 +7,6 @@ import LoginPage from './pages/LoginPage'; import MainPage from './pages/MainPage'; import { ProtectedRoute } from './app/protectedRoute'; import { memo } from "react"; -import { initAsync, start } from "@matrix-org/matrix-sdk-crypto-js"; - -initAsync().then(() => { - start(); -}) function App() { return <BrowserRouter basename={import.meta.env.BASE_URL}> diff --git a/src/app/sdk/client.ts b/src/app/sdk/client.ts @@ -27,6 +27,10 @@ import { IDBPDatabase, openDB } from "idb"; +import { DeviceId, KeysBackupRequest, KeysUploadRequest, OlmMachine, RequestType, RoomMessageRequest, SignatureUploadRequest, UserId } from "@matrix-org/matrix-sdk-crypto-js"; +import { KeysQueryRequest } from "@matrix-org/matrix-sdk-crypto-js"; +import { KeysClaimRequest } from "@matrix-org/matrix-sdk-crypto-js"; +import { ToDeviceRequest } from "@matrix-org/matrix-sdk-crypto-js"; export interface MatrixClientEvents { // Used to notify about changes to the room list @@ -112,6 +116,7 @@ export class MatrixClient extends EventEmitter { private profileInfo?: IProfileInfo; private lastRanges?: { [key: string]: number[][] }; private lastTxnID?: string; + private olmMachine?: OlmMachine; public get isLoggedIn(): boolean { return this.access_token !== undefined; @@ -139,6 +144,7 @@ export class MatrixClient extends EventEmitter { avatar_url: loginInfo[0].avatarUrl, displayname: loginInfo[0].displayName, }; + instance.olmMachine = await OlmMachine.initialize(new UserId(instance.mxid), new DeviceId(instance.device_id!), "cetirizine-crypto"); // Load sync info const syncTx = instance.database?.transaction('syncInfo', 'readonly'); @@ -331,6 +337,145 @@ export class MatrixClient extends EventEmitter { this.shiftRight(listKey, ranges, max + 1, index); } + private async sendIdentifyAndOneTimeKeys() { + if (!this.isLoggedIn) { + throw Error("Not logged in"); + } + if (!this.slidingSyncHostname) { + throw Error("Hostname must be set first"); + } + if (!this.olmMachine) { + throw Error("Olm machine must be set first"); + } + + const outgoing_requests = await this.olmMachine.outgoingRequests(); + + for (const request of outgoing_requests) { + // Check which type the request is + if (request.type === RequestType.KeysUpload) { + // Send the key + const request_typed = request as KeysUploadRequest; + const response = await fetch( + `${this.hostname}/_matrix/client/v3/keys/upload`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.access_token}` + }, + body: request_typed.body + } + ) + if (!response.ok) { + console.error("Failed to upload keys", response); + } + this.olmMachine.markRequestAsSent(request_typed.id!, request_typed.type, await response.text()); + } else if (request.type === RequestType.KeysQuery) { + const request_typed = request as KeysQueryRequest; + const response = await fetch( + `${this.hostname}/_matrix/client/v3/keys/query`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.access_token}` + }, + body: request_typed.body + } + ) + if (!response.ok) { + console.error("Failed to query keys", response); + } + this.olmMachine.markRequestAsSent(request_typed.id!, request_typed.type, await response.text()); + } else if (request.type === RequestType.KeysClaim) { + const request_typed = request as KeysClaimRequest; + const response = await fetch( + `${this.hostname}/_matrix/client/v3/keys/claim`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.access_token}` + }, + body: request_typed.body + } + ) + if (!response.ok) { + console.error("Failed to claim keys", response); + } + this.olmMachine.markRequestAsSent(request_typed.id!, request_typed.type, await response.text()); + } else if (request.type === RequestType.ToDevice) { + const request_typed = request as ToDeviceRequest; + const response = await fetch( + `${this.hostname}/_matrix/client/v3/sendToDevice/${request_typed.event_type}/${request_typed.txn_id}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.access_token}` + }, + body: request_typed.body + } + ) + if (!response.ok) { + console.error("Failed to send to device", response); + } + this.olmMachine.markRequestAsSent(request_typed.id!, request_typed.type, await response.text()); + } else if (request.type === RequestType.SignatureUpload) { + const request_typed = request as SignatureUploadRequest; + const response = await fetch( + `${this.hostname}/_matrix/client/v3/keys/signatures/upload`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.access_token}` + }, + body: request_typed.body + } + ) + if (!response.ok) { + console.error("Failed to upload signatures", response); + } + this.olmMachine.markRequestAsSent(request_typed.id!, request_typed.type, await response.text()); + } else if (request.type === RequestType.RoomMessage) { + const request_typed = request as RoomMessageRequest; + const response = await fetch( + `${this.hostname}/_matrix/client/v3/rooms/${request_typed.room_id}/send/${request_typed.event_type}/${request_typed.txn_id}`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.access_token}` + }, + body: request_typed.body + } + ) + if (!response.ok) { + console.error("Failed to send message", response); + } + this.olmMachine.markRequestAsSent(request_typed.id!, request_typed.type, await response.text()); + } else if (request.type === RequestType.KeysBackup) { + const request_typed = request as KeysBackupRequest; + const response = await fetch( + `${this.hostname}/_matrix/client/v3/room_keys/keys`, + { + method: "PUT", + headers: { + "Content-Type": "application/json", + "Authorization": `Bearer ${this.access_token}` + }, + body: request_typed.body + } + ) + if (!response.ok) { + console.error("Failed to backup keys", response); + } + this.olmMachine.markRequestAsSent(request_typed.id!, request_typed.type, await response.text()); + } + } + } + private async sync() { if (!this.isLoggedIn) { throw Error("Not logged in"); @@ -339,6 +484,8 @@ export class MatrixClient extends EventEmitter { throw Error("Hostname must be set first"); } + await this.sendIdentifyAndOneTimeKeys(); + // This is the initial sync case for each list let lists_ranges: { "overview": number[][]; @@ -491,9 +638,11 @@ export class MatrixClient extends EventEmitter { }, bump_event_types: ["m.room.message", "m.room.encrypted"], - // Room Subscriptions API - //room_subscriptions: {}, - //unsubscribe_rooms: [] + extensions: { + e2ee: { + enabled: true, + } + } }) }); if (!resp.ok) { @@ -969,6 +1118,8 @@ export class MatrixClient extends EventEmitter { this.access_token = json.access_token; this.device_id = json.device_id; this.mxid = json.user_id; + + this.olmMachine = await OlmMachine.initialize(new UserId(this.mxid), new DeviceId(this.device_id!), "cetirizine-crypto"); } } diff --git a/src/components/input/chat/input.tsx b/src/components/input/chat/input.tsx @@ -63,7 +63,7 @@ const ChatInput: FC<ChatInputProps> = memo(({ namespace, onChange, onError }: Ch } return ( <LexicalComposer initialConfig={initialConfig}> - <div className="editor-container"> + <div className="editor-container flex-1"> <ToolbarPlugin /> <div className="editor-inner"> <RichTextPlugin diff --git a/src/main.tsx b/src/main.tsx @@ -1,17 +1,25 @@ /// <reference types="./@types/global.d.ts" /> +import { initAsync, start } from "@matrix-org/matrix-sdk-crypto-js"; import React from 'react' import { createRoot } from 'react-dom/client'; -import App from './App'; import './index.scss'; -import { MatrixContext, defaultMatrixClient } from './app/sdk/client'; -const container = document.getElementById('root')!; -const root = createRoot(container); +initAsync().then(() => { + start(); -root.render( - <React.StrictMode> - <MatrixContext.Provider value={defaultMatrixClient}> - <App /> - </MatrixContext.Provider> - </React.StrictMode> -); -\ No newline at end of file + import('./app/sdk/client').then(({ MatrixContext, defaultMatrixClient }) => { + import('./App').then(({ default: App }) => { + const container = document.getElementById('root')!; + const root = createRoot(container); + + root.render( + <React.StrictMode> + <MatrixContext.Provider value={defaultMatrixClient}> + <App /> + </MatrixContext.Provider> + </React.StrictMode> + ); + }) + + }) +}) diff --git a/src/pages/MainPage.tsx b/src/pages/MainPage.tsx @@ -1,4 +1,4 @@ -import { Settings } from 'lucide-react'; +import { Send, Settings } from 'lucide-react'; import Avatar from '../components/avatar/avatar'; import ChatInput from '../components/input/chat/input'; import RoomList, { Section } from '../components/roomList/roomList'; @@ -196,32 +196,40 @@ const MainPage = memo(() => { </div> <RoomList sections={sections} rooms={otherRooms} /> </div> - {room && <div className='flex-1 flex flex-col'> - <div className='pb-2 flex flex-row items-center border-b-2 mt-4 ml-2'> - <Avatar displayname={room.getName()} avatarUrl={room.getAvatarURL()} dm={room.isDM()} online={room.isOnline()} /> - <div className='flex flex-row items-center'> - <h1 className='text-black font-semibold text-lg flex-shrink-0'>{room.getName()}</h1> - <p className='ml-4 text-slate-700 font-normal text-base'>{room.getTopic()}</p> + { + room && <div className='flex-1 flex flex-col'> + <div className='pb-2 flex flex-row items-center border-b-2 mt-4 ml-2'> + <Avatar displayname={room.getName()} avatarUrl={room.getAvatarURL()} dm={room.isDM()} online={room.isOnline()} /> + <div className='flex flex-row items-center'> + <h1 className='text-black font-semibold text-lg flex-shrink-0'>{room.getName()}</h1> + <p className='ml-4 text-slate-700 font-normal text-base'>{room.getTopic()}</p> + </div> + </div> + <div ref={scrollRef} className='overflow-y-auto overflow-x-hidden scrollbarSmall mr-2 my-1 flex-1 w-full flex flex-col-reverse'> + <ChatView roomID={params.roomIdOrAlias} scrollRef={scrollRef} /> + </div> + <div className='flex flex-row items-end'> + <ChatInput namespace='Editor' onChange={(editorState, editor) => { + // Convert editor state to both html and markdown. + // If there is no formatting then just use the plain text. + editorState.read(() => { + const html = $generateHtmlFromNodes(editor); + // TODO: Make sure that we strip any non matrix stuff + setHtmlMessage(html); + console.log(html); + const markdown = $convertToMarkdownString(TRANSFORMERS); + setPlainMessage(markdown); + console.log(markdown); + }); + // TODO: we need some send button + }} onError={(e) => console.error(e)} /> + <Send size={45} stroke='unset' className='stroke-slate-600 rounded m-4 hover:bg-slate-300 hover:stroke-slate-500 p-2 cursor-pointer' onClick={() => { + // TODO: Sanitize the html and send message to room + // TODO: encrypt if room is encrypted + }} /> </div> </div> - <div ref={scrollRef} className='overflow-y-auto overflow-x-hidden scrollbarSmall mr-2 my-1 flex-1 w-full flex flex-col-reverse'> - <ChatView roomID={params.roomIdOrAlias} scrollRef={scrollRef} /> - </div> - <ChatInput namespace='Editor' onChange={(editorState, editor) => { - // Convert editor state to both html and markdown. - // If there is no formatting then just use the plain text. - editorState.read(() => { - const html = $generateHtmlFromNodes(editor); - // TODO: Make sure that we strip any non matrix stuff - setHtmlMessage(html); - console.log(html); - const markdown = $convertToMarkdownString(TRANSFORMERS); - setPlainMessage(markdown); - console.log(markdown); - }); - // TODO: we need some send button - }} onError={(e) => console.error(e)} /> - </div>} + } </div > })