diff --git a/package.json b/package.json index 586b537..28b1609 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,6 @@ "@quentin-sommer/react-useragent": "^3.1.0", "classnames": "^2.2.6", "formik": "^2.1.4", - "matrix-cypher": "^0.1.12", "react": "^16.13.1", "react-dom": "^16.13.1", "react-scripts": "3.4.1", diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 00c5183..f150c63 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -16,7 +16,7 @@ limitations under the License. import React, { useEffect, useState } from 'react'; import classNames from 'classnames'; -import { Room, User } from 'matrix-cypher'; +import { Room, User } from '../matrix-cypher'; import { getMediaQueryFromMCX } from '../utils/cypher-wrapper'; import logo from '../imgs/chat-icon.svg'; diff --git a/src/components/EventPreview.tsx b/src/components/EventPreview.tsx index de7ac7f..00ac979 100644 --- a/src/components/EventPreview.tsx +++ b/src/components/EventPreview.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from 'react'; -import { Room, Event } from 'matrix-cypher'; +import { Room, Event } from '../matrix-cypher'; import RoomPreview from './RoomPreview'; diff --git a/src/components/LinkPreview.tsx b/src/components/LinkPreview.tsx index 58c0fff..ae4170c 100644 --- a/src/components/LinkPreview.tsx +++ b/src/components/LinkPreview.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React, { useState, useEffect, useContext } from 'react'; -import { getEvent, client } from 'matrix-cypher'; +import { getEvent, client } from '../matrix-cypher'; import { RoomPreviewWithTopic } from './RoomPreview'; import InviteTile from './InviteTile'; diff --git a/src/components/RoomPreview.tsx b/src/components/RoomPreview.tsx index 2874f2b..0ef2f14 100644 --- a/src/components/RoomPreview.tsx +++ b/src/components/RoomPreview.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React from 'react'; -import { Room } from 'matrix-cypher'; +import { Room } from '../matrix-cypher'; import { RoomAvatar } from './Avatar'; diff --git a/src/components/UserPreview.tsx b/src/components/UserPreview.tsx index b1a38ee..f0928c8 100644 --- a/src/components/UserPreview.tsx +++ b/src/components/UserPreview.tsx @@ -15,7 +15,7 @@ limitations under the License. */ import React, { useState, useEffect } from 'react'; -import { client, User, getUserDetails } from 'matrix-cypher'; +import { client, User, getUserDetails } from '../matrix-cypher'; import classNames from 'classnames'; import icon from '../imgs/chat-icon.svg'; diff --git a/src/contexts/HSContext.ts b/src/contexts/HSContext.ts index 1203f3f..4105af6 100644 --- a/src/contexts/HSContext.ts +++ b/src/contexts/HSContext.ts @@ -19,8 +19,6 @@ import { string, object, union, literal, TypeOf } from 'zod'; import { persistReducer } from '../utils/localStorage'; -//import { prefixFetch, Client, discoverServer } from 'matrix-cypher'; - export enum HSOptions { // The homeserver contact policy hasn't // been set yet. diff --git a/src/matrix-cypher/index.ts b/src/matrix-cypher/index.ts new file mode 100644 index 0000000..8bdc933 --- /dev/null +++ b/src/matrix-cypher/index.ts @@ -0,0 +1,19 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export * from './matrix-cypher'; +export * from './utils'; +export * from './schemas'; diff --git a/src/matrix-cypher/matrix-cypher.ts b/src/matrix-cypher/matrix-cypher.ts new file mode 100644 index 0000000..75387a2 --- /dev/null +++ b/src/matrix-cypher/matrix-cypher.ts @@ -0,0 +1,193 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the 'License'); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an 'AS IS' BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* eslint-disable import/first */ + +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import any from 'promise.any'; +any.shim() + +import VersionSchema from './schemas/VersionSchema'; +import WellKnownSchema from './schemas/WellKnownSchema'; +import UserSchema, { User } from './schemas/UserSchema'; +import RoomAliasSchema, { + RoomAlias, +} from './schemas/RoomAliasSchema'; +import PublicRoomsSchema, { + PublicRooms, + Room, +} from './schemas/PublicRoomsSchema'; +import EventSchema, { + Event, +} from './schemas/EventSchema'; +import { ensure } from './utils/promises'; +import { prefixFetch, parseJSON } from './utils/fetch'; + + +/* + * A client is a resolved homeserver name wrapped in a lambda'd fetch + */ +export type Client = (path: string) => Promise; + +/* + * Confirms that the target homeserver is properly configured and operational + */ +export const validateHS = (host: string) => + prefixFetch(host)('/_matrix/client/versions') + .then(parseJSON) + .then(VersionSchema.parse) + .then(() => host); + +/* + * Discovers the correct domain name for the host according to the spec's + * discovery rules + */ +export const discoverServer = (host: string) => + prefixFetch(host)('/.well-known/matrix/client') + .then(resp => resp.ok + ? resp.json() + .then(WellKnownSchema.parse) + .then(content => { + if (content === undefined) return host; + else if ( + 'm.homeserver' in content && content['m.homeserver'] + ) { + return content['m.homeserver'].base_url + } else { + return host + } + }) + : ensure( + resp.status === 404, + () => host, + ), + ) + .then(validateHS) + + +/* + * Takes a hs domain and resolves it to it's current domain and returns a + * client + */ +export async function client(host: string): Promise { + return prefixFetch(await discoverServer(host)) +} + +/* + * Gets the details for a user + */ +export function getUserDetails( + client: Client, + userId: string, +): Promise { + return client(`/_matrix/client/r0/profile/${userId}`) + .then(parseJSON) + .then(UserSchema.parse) +} + +/* + * Gets the roomId of a room by resolving it's alias + */ +export function getRoomIdFromAlias( + client: Client, + roomAlias: string, +): Promise { + const encodedRoomAlias = encodeURIComponent(roomAlias); + return client(`/_matrix/client/r0/directory/room/${encodedRoomAlias}`) + .then(parseJSON) + .then(RoomAliasSchema.parse); +} + +/* + * Gets the details of a room if that room is public + */ +export function getRoomDetails(clients: Client[], roomId: string): Promise { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + return Promise.any(clients.map(client => searchPublicRooms(client, roomId))); +} + +/* + * Gets a list of all public rooms on a hs + */ +export function getPublicRooms(client: Client): Promise { + return getPublicRoomsUnsafe(client) + .then(PublicRoomsSchema.parse) +} + +/* + * Similar to getPubliRooms however id doesn't confirm the data returned from + * the hs is correct + * + * This is used because the room list can be huge and validating it all takes + * a long time + */ +export function getPublicRoomsUnsafe(client: Client): Promise { + // TODO: Do not assume server will return all results in one go + return client('/_matrix/client/r0/publicRooms') + .then(parseJSON) +} + +/* + * Searches the public rooms of a homeserver for the metadata of a particular + */ +export function searchPublicRooms( + client: Client, + roomId: string, +): Promise { + // we use the unsage version here because the safe one is sloooow + return getPublicRoomsUnsafe(client) + .then(rooms => { + const [match] = rooms.chunk.filter( + chunk => chunk.room_id === roomId, + ); + return match !== undefined + ? Promise.resolve(match) + : Promise.reject(new Error( + `This server knowns no public room with id ${roomId}`, + )); + }); +} + +/* + * Gets the details of an event from the homeserver + */ +export async function getEvent( + client: Client, + roomIdOrAlias: string, + eventId: string, +): Promise { + return client(`/_matrix/client/r0/rooms/${roomIdOrAlias}/event/${eventId}`) + .then(parseJSON) + .then(EventSchema.parse); +} + +/* + * Gets an mxc resource + */ +export function convertMXCtoMediaQuery( + clientURL: string, + mxc: string, +): string { + // mxc://matrix.org/EqMZYbAYhREvHXvYFyfxOlkf + const matches = mxc.match(/mxc:\/\/(.+)\/(.+)/) + if (!matches) { + throw new Error(`mxc invalid: ${JSON.stringify({mxc})}`); + } + + return `${clientURL}/_matrix/media/r0/download/${matches[1]}/${matches[2]}`; +} diff --git a/src/matrix-cypher/schemas/EventSchema.ts b/src/matrix-cypher/schemas/EventSchema.ts new file mode 100644 index 0000000..2b0402a --- /dev/null +++ b/src/matrix-cypher/schemas/EventSchema.ts @@ -0,0 +1,30 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { object, string, TypeOf } from 'zod'; + +const EventSchema = object({ + content: object({}).nonstrict(), + type: string(), + event_id: string(), + sender: string(), + origin_server_ts: string(), + unsigned: object({}).nonstrict().optional(), + room_id: string(), +}); + +export type Event = TypeOf; +export default EventSchema; diff --git a/src/matrix-cypher/schemas/PublicRoomsSchema.ts b/src/matrix-cypher/schemas/PublicRoomsSchema.ts new file mode 100644 index 0000000..b33cb31 --- /dev/null +++ b/src/matrix-cypher/schemas/PublicRoomsSchema.ts @@ -0,0 +1,43 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { object, array, string, boolean, number, TypeOf } from 'zod'; + +export const RoomSchema = object({ + aliases: array(string()).optional(), + canonical_alias: string().optional(), + name: string().optional(), + num_joined_members: number(), + room_id: string(), + topic: string().optional(), + world_readable: boolean(), + guest_can_join: boolean(), + avatar_url: string().optional(), +}); + + +const PublicRoomsSchema = object({ + chunk: array(RoomSchema), + next_batch: string().optional(), + prev_batch: string().optional(), + total_room_count_estimate: number().optional(), +}); + +export type Room = TypeOf; +export type PublicRooms = TypeOf; + +export default PublicRoomsSchema; + diff --git a/src/matrix-cypher/schemas/RoomAliasSchema.ts b/src/matrix-cypher/schemas/RoomAliasSchema.ts new file mode 100644 index 0000000..f662a4b --- /dev/null +++ b/src/matrix-cypher/schemas/RoomAliasSchema.ts @@ -0,0 +1,26 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { object, array, string, TypeOf } from 'zod'; + +const RoomAliasSchema = object({ + room_id: string(), + servers: array(string()), +}); + +export type RoomAlias = TypeOf; +export default RoomAliasSchema; + diff --git a/src/matrix-cypher/schemas/UserSchema.ts b/src/matrix-cypher/schemas/UserSchema.ts new file mode 100644 index 0000000..4702e8e --- /dev/null +++ b/src/matrix-cypher/schemas/UserSchema.ts @@ -0,0 +1,26 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { object, string, TypeOf } from 'zod'; + +const UserSchema = object({ + avatar_url: string().optional(), + displayname: string().optional(), +}) + +export type User = TypeOf; +export default UserSchema; + diff --git a/src/matrix-cypher/schemas/VersionSchema.ts b/src/matrix-cypher/schemas/VersionSchema.ts new file mode 100644 index 0000000..3af08c0 --- /dev/null +++ b/src/matrix-cypher/schemas/VersionSchema.ts @@ -0,0 +1,21 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { object, string, array } from 'zod'; + +export default object({ + versions: array(string()), +}).nonstrict() diff --git a/src/matrix-cypher/schemas/WellKnownSchema.ts b/src/matrix-cypher/schemas/WellKnownSchema.ts new file mode 100644 index 0000000..7cfaa29 --- /dev/null +++ b/src/matrix-cypher/schemas/WellKnownSchema.ts @@ -0,0 +1,29 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import { object, string, TypeOf } from 'zod'; + +const WellKnownSchema = object({ + 'm.homeserver': object({ + 'base_url': string().url(), + }), + 'm.identity_server': object({ + 'base_url': string().url(), + }), +}); + +export type WellKnown = TypeOf; +export default WellKnownSchema; diff --git a/src/matrix-cypher/schemas/index.ts b/src/matrix-cypher/schemas/index.ts new file mode 100644 index 0000000..f7fa3c3 --- /dev/null +++ b/src/matrix-cypher/schemas/index.ts @@ -0,0 +1,24 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export * from './EventSchema'; +export * from './PublicRoomsSchema'; +export * from './RoomAliasSchema'; +export * from './UserSchema'; +export * from './VersionSchema'; +export * from './WellKnownSchema'; +export * from './index'; + diff --git a/src/matrix-cypher/utils/fetch.ts b/src/matrix-cypher/utils/fetch.ts new file mode 100644 index 0000000..44255fb --- /dev/null +++ b/src/matrix-cypher/utils/fetch.ts @@ -0,0 +1,36 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import fetch from 'cross-fetch'; + +import { ensure } from './promises'; + +/* + * Wraps a fetch with a domain for easy reuse. + */ +export function prefixFetch(host: string) { + return (path: string) => fetch( + new URL(path, host).toString(), + ); +} + +export function parseJSON(resp: Response) { + return ensure( + resp.ok, + () => resp.json(), + `Error from Homeserver. Error code: ${resp.status}`, + ); +} diff --git a/src/matrix-cypher/utils/index.ts b/src/matrix-cypher/utils/index.ts new file mode 100644 index 0000000..4e4adb1 --- /dev/null +++ b/src/matrix-cypher/utils/index.ts @@ -0,0 +1,18 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +export * from './fetch'; +export * from './promises'; diff --git a/src/matrix-cypher/utils/promises.ts b/src/matrix-cypher/utils/promises.ts new file mode 100644 index 0000000..08ee36b --- /dev/null +++ b/src/matrix-cypher/utils/promises.ts @@ -0,0 +1,60 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* + * Conditional promises + */ + +/* + * If the condition is false reject with rejectReason + * If it's true resolve with the result = resultThunk() + */ +export function ensure(condition: boolean, resultThunk: () => T | PromiseLike, rejectReason?: string) { + return condition + ? Promise.resolve(resultThunk()) + : Promise.reject(new Error(rejectReason)); +} + +/* + * Loggin utilities + */ + +/* + * Logs a then using "success: {label: successArg}" + */ +export function logThen(label: string): (v: T) => T | PromiseLike { + return (v: T) => { + console.log('success:', {[`${label}`]: v}); return v + } +} + +/* + * Logs a catch using "fail: {label: failArg}" + */ +export function logCatch(label: string): (v: T) => T | PromiseLike { + return (v: T) => { + console.log('fail:', {[`${label}`]: v}); + return Promise.reject(v) + } +} + +/* + * inserts loggers for both callbacks of a then + */ +export function logThens(label: string) { + return [logThen(label), logCatch(label)] +} + diff --git a/src/utils/cypher-wrapper.ts b/src/utils/cypher-wrapper.ts index 5155d20..7e1093e 100644 --- a/src/utils/cypher-wrapper.ts +++ b/src/utils/cypher-wrapper.ts @@ -28,7 +28,7 @@ import { searchPublicRooms, getUserDetails, convertMXCtoMediaQuery, -} from 'matrix-cypher'; +} from '../matrix-cypher'; import { LinkKind, Permalink } from '../parser/types'; /* This is a collection of methods for providing fallback metadata