import { stringify } from "query-string"; import { DataProvider, DeleteParams, Identifier, Options, RaRecord, fetchUtils } from "react-admin"; // Adds the access token to all requests const jsonClient = (url: string, options: Options = {}) => { const token = localStorage.getItem("access_token"); console.log("httpClient " + url); if (token != null) { options.user = { authenticated: true, token: `Bearer ${token}`, }; } return fetchUtils.fetchJson(url, options); }; const mxcUrlToHttp = (mxcUrl: string) => { const homeserver = localStorage.getItem("base_url"); const re = /^mxc:\/\/([^/]+)\/(\w+)/; const ret = re.exec(mxcUrl); console.log("mxcClient " + ret); if (ret == null) return null; const serverName = ret[1]; const mediaId = ret[2]; return `${homeserver}/_matrix/media/r0/thumbnail/${serverName}/${mediaId}?width=24&height=24&method=scale`; }; interface Room { room_id: string; name?: string; canonical_alias?: string; avatar_url?: string; joined_members: number; joined_local_members: number; version: number; creator: string; encryption?: string; federatable: boolean; public: boolean; join_rules: "public" | "knock" | "invite" | "private"; guest_access?: "can_join" | "forbidden"; history_visibility: "invited" | "joined" | "shared" | "world_readable"; state_events: number; room_type?: string; } interface RoomState { age: number; content: { alias?: string; }; event_id: string; origin_server_ts: number; room_id: string; sender: string; state_key: string; type: string; user_id: string; unsigned: { age?: number; }; } interface ForwardExtremity { event_id: string; state_group: number; depth: number; received_ts: number; } interface EventReport { id: number; received_ts: number; room_id: string; name: string; event_id: string; user_id: string; reason?: string; score?: number; sender: string; canonical_alias?: string; } interface Threepid { medium: string; address: string; added_at: number; validated_at: number; } interface ExternalId { auth_provider: string; external_id: string; } interface User { name: string; displayname?: string; threepids: Threepid[]; avatar_url?: string; is_guest: 0 | 1; admin: 0 | 1; deactivated: 0 | 1; erased: boolean; shadow_banned: 0 | 1; creation_ts: number; appservice_id?: string; consent_server_notice_sent?: string; consent_version?: string; consent_ts?: number; external_ids: ExternalId[]; user_type?: string; locked: boolean; } interface Device { device_id: string; display_name?: string; last_seen_ip?: string; last_seen_user_agent?: string; last_seen_ts?: number; user_id: string; } interface Connection { ip: string; last_seen: number; user_agent: string; } interface Whois { user_id: string; devices: Record< string, { sessions: { connections: Connection[]; }[]; } >; } interface Pusher { app_display_name: string; app_id: string; data: { url?: string; format: string; }; url: string; format: string; device_display_name: string; profile_tag: string; kind: string; lang: string; pushkey: string; } interface UserMedia { created_ts: number; last_access_ts?: number; media_id: string; media_length: number; media_type: string; quarantined_by?: string; safe_from_quarantine: boolean; upload_name?: string; } interface UserMediaStatistic { displayname: string; media_count: number; media_length: number; user_id: string; } interface RegistrationToken { token: string; uses_allowed: number; pending: number; completed: number; expiry_time?: number; } interface RaServerNotice { id: string; body: string; } interface Destination { destination: string; retry_last_ts: number; retry_interval: number; failure_ts: number; last_successful_stream_ordering?: number; } interface DestinationRoom { room_id: string; stream_ordering: number; } const resourceMap = { users: { path: "/_synapse/admin/v2/users", map: (u: User) => ({ ...u, id: u.name, avatar_src: u.avatar_url ? mxcUrlToHttp(u.avatar_url) : undefined, is_guest: !!u.is_guest, admin: !!u.admin, deactivated: !!u.deactivated, // need timestamp in milliseconds creation_ts_ms: u.creation_ts * 1000, }), data: "users", total: json => json.total, create: (data: RaRecord) => ({ endpoint: `/_synapse/admin/v2/users/@${encodeURIComponent(data.id)}:${localStorage.getItem("home_server")}`, body: data, method: "PUT", }), delete: (params: DeleteParams) => ({ endpoint: `/_synapse/admin/v1/deactivate/${encodeURIComponent(params.id)}`, body: { erase: true }, method: "POST", }), }, rooms: { path: "/_synapse/admin/v1/rooms", map: (r: Room) => ({ ...r, id: r.room_id, alias: r.canonical_alias, members: r.joined_members, is_encrypted: !!r.encryption, federatable: !!r.federatable, public: !!r.public, }), data: "rooms", total: json => json.total_rooms, delete: (params: DeleteParams) => ({ endpoint: `/_synapse/admin/v2/rooms/${params.id}`, body: { block: false }, }), }, reports: { path: "/_synapse/admin/v1/event_reports", map: (er: EventReport) => ({ ...er }), data: "event_reports", total: json => json.total, }, devices: { map: (d: Device) => ({ ...d, id: d.device_id, }), data: "devices", total: json => json.total, reference: (id: Identifier) => ({ endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(id)}/devices`, }), delete: (params: DeleteParams) => ({ endpoint: `/_synapse/admin/v2/users/${encodeURIComponent(params.previousData.user_id)}/devices/${params.id}`, }), }, connections: { path: "/_synapse/admin/v1/whois", map: (c: Whois) => ({ ...c, id: c.user_id, }), data: "connections", }, room_members: { map: (m: string) => ({ id: m, }), reference: (id: Identifier) => ({ endpoint: `/_synapse/admin/v1/rooms/${id}/members`, }), data: "members", total: json => json.total, }, room_state: { map: (rs: RoomState) => ({ ...rs, id: rs.event_id, }), reference: (id: Identifier) => ({ endpoint: `/_synapse/admin/v1/rooms/${id}/state`, }), data: "state", total: json => json.state.length, }, pushers: { map: (p: Pusher) => ({ ...p, id: p.pushkey, }), reference: (id: Identifier) => ({ endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/pushers`, }), data: "pushers", total: json => json.total, }, joined_rooms: { map: (jr: string) => ({ id: jr, }), reference: (id: Identifier) => ({ endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/joined_rooms`, }), data: "joined_rooms", total: json => json.total, }, users_media: { map: (um: UserMedia) => ({ ...um, id: um.media_id, }), reference: (id: Identifier) => ({ endpoint: `/_synapse/admin/v1/users/${encodeURIComponent(id)}/media`, }), data: "media", total: json => json.total, delete: (params: DeleteParams) => ({ endpoint: `/_synapse/admin/v1/media/${localStorage.getItem("home_server")}/${params.id}`, }), }, delete_media: { delete: (params: DeleteParams) => ({ endpoint: `/_synapse/admin/v1/media/${localStorage.getItem( "home_server" )}/delete?before_ts=${params.meta.before_ts}&size_gt=${ params.meta.size_gt }&keep_profiles=${params.meta.keep_profiles}`, method: "POST", }), }, protect_media: { map: (pm: UserMedia) => ({ id: pm.media_id }), create: (params: UserMedia) => ({ endpoint: `/_synapse/admin/v1/media/protect/${params.media_id}`, method: "POST", }), delete: (params: DeleteParams) => ({ endpoint: `/_synapse/admin/v1/media/unprotect/${params.id}`, method: "POST", }), }, quarantine_media: { map: (qm: UserMedia) => ({ id: qm.media_id }), create: (params: UserMedia) => ({ endpoint: `/_synapse/admin/v1/media/quarantine/${localStorage.getItem("home_server")}/${params.media_id}`, method: "POST", }), delete: (params: DeleteParams) => ({ endpoint: `/_synapse/admin/v1/media/unquarantine/${localStorage.getItem("home_server")}/${params.id}`, method: "POST", }), }, servernotices: { map: (n: { event_id: string }) => ({ id: n.event_id }), create: (data: RaServerNotice) => ({ endpoint: "/_synapse/admin/v1/send_server_notice", body: { user_id: data.id, content: { msgtype: "m.text", body: data.body, }, }, method: "POST", }), }, user_media_statistics: { path: "/_synapse/admin/v1/statistics/users/media", map: (usms: UserMediaStatistic) => ({ ...usms, id: usms.user_id, }), data: "users", total: json => json.total, }, forward_extremities: { map: (fe: ForwardExtremity) => ({ ...fe, id: fe.event_id, }), reference: (id: Identifier) => ({ endpoint: `/_synapse/admin/v1/rooms/${id}/forward_extremities`, }), data: "results", total: json => json.count, delete: (params: DeleteParams) => ({ endpoint: `/_synapse/admin/v1/rooms/${params.id}/forward_extremities`, }), }, room_directory: { path: "/_matrix/client/r0/publicRooms", map: (rd: Room) => ({ ...rd, id: rd.room_id, public: !!rd.public, guest_access: !!rd.guest_access, avatar_src: rd.avatar_url ? mxcUrlToHttp(rd.avatar_url) : undefined, }), data: "chunk", total: json => json.total_room_count_estimate, create: (params: RaRecord) => ({ endpoint: `/_matrix/client/r0/directory/list/room/${params.id}`, body: { visibility: "public" }, method: "PUT", }), delete: (params: DeleteParams) => ({ endpoint: `/_matrix/client/r0/directory/list/room/${params.id}`, body: { visibility: "private" }, method: "PUT", }), }, destinations: { path: "/_synapse/admin/v1/federation/destinations", map: (dst: Destination) => ({ ...dst, id: dst.destination, }), data: "destinations", total: json => json.total, delete: params => ({ endpoint: `/_synapse/admin/v1/federation/destinations/${params.id}/reset_connection`, method: "POST", }), }, destination_rooms: { map: (dstroom: DestinationRoom) => ({ ...dstroom, id: dstroom.room_id, }), reference: (id: Identifier) => ({ endpoint: `/_synapse/admin/v1/federation/destinations/${id}/rooms`, }), data: "rooms", total: json => json.total, }, registration_tokens: { path: "/_synapse/admin/v1/registration_tokens", map: (rt: RegistrationToken) => ({ ...rt, id: rt.token, }), data: "registration_tokens", total: json => json.registration_tokens.length, create: (params: RaRecord) => ({ endpoint: "/_synapse/admin/v1/registration_tokens/new", body: params, method: "POST", }), delete: (params: DeleteParams) => ({ endpoint: `/_synapse/admin/v1/registration_tokens/${params.id}`, }), }, }; /* eslint-disable @typescript-eslint/no-explicit-any */ function filterNullValues(key: string, value: any) { // Filtering out null properties // to reset user_type from user, it must be null if (value === null && key !== "user_type") { return undefined; } return value; } function getSearchOrder(order: "ASC" | "DESC") { if (order === "DESC") { return "b"; } else { return "f"; } } const dataProvider: DataProvider = { getList: async (resource, params) => { console.log("getList " + resource); const { user_id, name, guests, deactivated, search_term, destination, valid } = params.filter; const { page, perPage } = params.pagination; const { field, order } = params.sort; const from = (page - 1) * perPage; const query = { from: from, limit: perPage, user_id: user_id, search_term: search_term, name: name, destination: destination, guests: guests, deactivated: deactivated, valid: valid, order_by: field, dir: getSearchOrder(order), }; const homeserver = localStorage.getItem("base_url"); if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set"); const res = resourceMap[resource]; const endpoint_url = homeserver + res.path; const url = `${endpoint_url}?${stringify(query)}`; const { json } = await jsonClient(url); return { data: json[res.data].map(res.map), total: res.total(json, from, perPage), }; }, getOne: async (resource, params) => { console.log("getOne " + resource); const homeserver = localStorage.getItem("base_url"); if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set"); const res = resourceMap[resource]; const endpoint_url = homeserver + res.path; const { json } = await jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`); return { data: res.map(json) }; }, getMany: async (resource, params) => { console.log("getMany " + resource); const homeserver = localStorage.getItem("base_url"); if (!homeserver || !(resource in resourceMap)) throw Error("Homerserver not set"); const res = resourceMap[resource]; const endpoint_url = homeserver + res.path; const responses = await Promise.all(params.ids.map(id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`))); return { data: responses.map(({ json }) => res.map(json)), total: responses.length, }; }, getManyReference: async (resource, params) => { console.log("getManyReference " + resource); const { page, perPage } = params.pagination; const { field, order } = params.sort; const from = (page - 1) * perPage; const query = { from: from, limit: perPage, order_by: field, dir: getSearchOrder(order), }; const homeserver = localStorage.getItem("base_url"); if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set"); const res = resourceMap[resource]; const ref = res.reference(params.id); const endpoint_url = `${homeserver}${ref.endpoint}?${stringify(query)}`; const { json } = await jsonClient(endpoint_url); return { data: json[res.data].map(res.map), total: res.total(json, from, perPage), }; }, update: async (resource, params) => { console.log("update " + resource); const homeserver = localStorage.getItem("base_url"); if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set"); const res = resourceMap[resource]; const endpoint_url = homeserver + res.path; const { json } = await jsonClient(`${endpoint_url}/${encodeURIComponent(params.id)}`, { method: "PUT", body: JSON.stringify(params.data, filterNullValues), }); return { data: res.map(json) }; }, updateMany: async (resource, params) => { console.log("updateMany " + resource); const homeserver = localStorage.getItem("base_url"); if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set"); const res = resourceMap[resource]; const endpoint_url = homeserver + res.path; const responses = await Promise.all( params.ids.map(id => jsonClient(`${endpoint_url}/${encodeURIComponent(id)}`), { method: "PUT", body: JSON.stringify(params.data, filterNullValues), }) ); return { data: responses.map(({ json }) => json) }; }, create: async (resource, params) => { console.log("create " + resource); const homeserver = localStorage.getItem("base_url"); if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set"); const res = resourceMap[resource]; if (!("create" in res)) return Promise.reject(); const create = res.create(params.data); const endpoint_url = homeserver + create.endpoint; const { json } = await jsonClient(endpoint_url, { method: create.method, body: JSON.stringify(create.body, filterNullValues), }); return { data: res.map(json) }; }, createMany: async (resource: string, params: { ids: Identifier[]; data: RaRecord }) => { console.log("createMany " + resource); const homeserver = localStorage.getItem("base_url"); if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set"); const res = resourceMap[resource]; if (!("create" in res)) throw Error(`Create ${resource} is not allowed`); const responses = await Promise.all( params.ids.map(id => { params.data.id = id; const cre = res.create(params.data); const endpoint_url = homeserver + cre.endpoint; return jsonClient(endpoint_url, { method: cre.method, body: JSON.stringify(cre.body, filterNullValues), }); }) ); return { data: responses.map(({ json }) => json) }; }, delete: async (resource, params) => { console.log("delete " + resource); const homeserver = localStorage.getItem("base_url"); if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set"); const res = resourceMap[resource]; if ("delete" in res) { const del = res.delete(params); const endpoint_url = homeserver + del.endpoint; const { json } = await jsonClient(endpoint_url, { method: "method" in del ? del.method : "DELETE", body: "body" in del ? JSON.stringify(del.body) : null, }); return { data: json }; } else { const endpoint_url = homeserver + res.path; const { json } = await jsonClient(`${endpoint_url}/${params.id}`, { method: "DELETE", body: JSON.stringify(params.previousData, filterNullValues), }); return { data: json }; } }, deleteMany: async (resource, params) => { console.log("deleteMany " + resource); const homeserver = localStorage.getItem("base_url"); if (!homeserver || !(resource in resourceMap)) throw Error("Homeserver not set"); const res = resourceMap[resource]; if ("delete" in res) { const responses = await Promise.all( params.ids.map(id => { const del = res.delete({ ...params, id: id }); const endpoint_url = homeserver + del.endpoint; return jsonClient(endpoint_url, { method: "method" in del ? del.method : "DELETE", body: "body" in del ? JSON.stringify(del.body) : null, }); }) ); return { data: responses.map(({ json }) => json), }; } else { const endpoint_url = homeserver + res.path; const responses = await Promise.all( params.ids.map(id => jsonClient(`${endpoint_url}/${id}`, { method: "DELETE", // body: JSON.stringify(params.data, filterNullValues), @FIXME }) ) ); return { data: responses.map(({ json }) => json) }; } }, }; export default dataProvider;