From 53b716f382c5320f196be9cc1b3b7a7d4967613a Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Fri, 24 Jul 2020 11:34:21 +0200 Subject: [PATCH] WIP --- app/javascript/mastodon/actions/accounts.js | 30 +-- app/javascript/mastodon/actions/crypto.js | 111 +++++++++ .../mastodon/actions/crypto/store.js | 110 +++++++++ app/javascript/mastodon/actions/statuses.js | 20 +- app/javascript/mastodon/actions/streaming.js | 4 + .../features/direct_timeline/index.js | 36 ++- app/javascript/mastodon/reducers/crypto.js | 15 ++ app/javascript/mastodon/reducers/index.js | 2 + app/javascript/mastodon/storage/db.js | 27 --- app/javascript/mastodon/storage/modifier.js | 211 ------------------ config/routes.rb | 28 +-- config/webpack/rules/index.js | 2 + config/webpack/rules/wasm.js | 8 + config/webpack/shared.js | 3 + package.json | 2 + yarn.lock | 9 + 16 files changed, 298 insertions(+), 320 deletions(-) create mode 100644 app/javascript/mastodon/actions/crypto.js create mode 100644 app/javascript/mastodon/actions/crypto/store.js create mode 100644 app/javascript/mastodon/reducers/crypto.js delete mode 100644 app/javascript/mastodon/storage/db.js delete mode 100644 app/javascript/mastodon/storage/modifier.js create mode 100644 config/webpack/rules/wasm.js diff --git a/app/javascript/mastodon/actions/accounts.js b/app/javascript/mastodon/actions/accounts.js index cb2c682a4..455cfed60 100644 --- a/app/javascript/mastodon/actions/accounts.js +++ b/app/javascript/mastodon/actions/accounts.js @@ -1,5 +1,4 @@ import api, { getLinks } from '../api'; -import openDB from '../storage/db'; import { importAccount, importFetchedAccount, importFetchedAccounts } from './importer'; export const ACCOUNT_FETCH_REQUEST = 'ACCOUNT_FETCH_REQUEST'; @@ -74,24 +73,6 @@ export const FOLLOW_REQUEST_REJECT_REQUEST = 'FOLLOW_REQUEST_REJECT_REQUEST'; export const FOLLOW_REQUEST_REJECT_SUCCESS = 'FOLLOW_REQUEST_REJECT_SUCCESS'; export const FOLLOW_REQUEST_REJECT_FAIL = 'FOLLOW_REQUEST_REJECT_FAIL'; -function getFromDB(dispatch, getState, index, id) { - return new Promise((resolve, reject) => { - const request = index.get(id); - - request.onerror = reject; - - request.onsuccess = () => { - if (!request.result) { - reject(); - return; - } - - dispatch(importAccount(request.result)); - resolve(request.result.moved && getFromDB(dispatch, getState, index, request.result.moved)); - }; - }); -} - export function fetchAccount(id) { return (dispatch, getState) => { dispatch(fetchRelationships([id])); @@ -102,17 +83,8 @@ export function fetchAccount(id) { dispatch(fetchAccountRequest(id)); - openDB().then(db => getFromDB( - dispatch, - getState, - db.transaction('accounts', 'read').objectStore('accounts').index('id'), - id, - ).then(() => db.close(), error => { - db.close(); - throw error; - })).catch(() => api(getState).get(`/api/v1/accounts/${id}`).then(response => { + api(getState).get(`/api/v1/accounts/${id}`).then(response => { dispatch(importFetchedAccount(response.data)); - })).then(() => { dispatch(fetchAccountSuccess()); }).catch(error => { dispatch(fetchAccountFail(id, error)); diff --git a/app/javascript/mastodon/actions/crypto.js b/app/javascript/mastodon/actions/crypto.js new file mode 100644 index 000000000..7b8ab01ae --- /dev/null +++ b/app/javascript/mastodon/actions/crypto.js @@ -0,0 +1,111 @@ +import api, { getLinks } from '../api'; +import Olm from 'olm'; +import olmModule from 'olm/olm.wasm'; +import CryptoStore from './crypto/store'; + +export const CRYPTO_INITIALIZE_REQUEST = 'CRYPTO_INITIALIZE_REQUEST'; +export const CRYPTO_INITIALIZE_SUCCESS = 'CRYPTO_INITIALIZE_SUCCESS'; +export const CRYPTO_INITIALIZE_FAIL = 'CRYPTO_INITIALIZE_FAIL '; + +const cryptoStore = new CryptoStore(); + +const loadOlm = () => Olm.init({ + + locateFile: path => { + if (path.endsWith('.wasm')) { + return olmModule; + } + + return path; + }, + +}); + +const getRandomBytes = size => { + const array = new Uint8Array(size); + crypto.getRandomValues(array); + return array.buffer; +}; + +const generateDeviceId = () => { + const id = new Uint16Array(getRandomBytes(2))[0]; + return id & 0x3fff; +}; + +export const initializeCryptoRequest = () => ({ + type: CRYPTO_INITIALIZE_REQUEST, +}); + +export const initializeCryptoSuccess = account => ({ + type: CRYPTO_INITIALIZE_SUCCESS, + account, +}); + +export const initializeCryptoFail = error => ({ + type: CRYPTO_INITIALIZE_FAIL, + error, +}); + +export const initializeCrypto = () => (dispatch, getState) => { + dispatch(initializeCryptoRequest()); + + loadOlm().then(() => { + return cryptoStore.getAccount(); + }).then(account => { + dispatch(initializeCryptoSuccess(account)); + }).catch(err => { + console.error(err); + dispatch(initializeCryptoFail(err)); + }); +}; + +export const enableCrypto = () => (dispatch, getState) => { + dispatch(initializeCryptoRequest()); + + loadOlm().then(() => { + const deviceId = generateDeviceId(); + const account = new Olm.Account(); + + account.create(); + account.generate_one_time_keys(10); + + const deviceName = 'Browser'; + const identityKeys = JSON.parse(account.identity_keys()); + const oneTimeKeys = JSON.parse(account.one_time_keys()); + + return cryptoStore.storeAccount(account).then(api(getState).post('/api/v1/crypto/keys/upload', { + device: { + device_id: deviceId, + name: deviceName, + fingerprint_key: identityKeys.ed25519, + identity_key: identityKeys.curve25519, + }, + + one_time_keys: Object.keys(oneTimeKeys.curve25519).map(key => ({ + key_id: key, + key: oneTimeKeys.curve25519[key], + signature: account.sign(oneTimeKeys.curve25519[key]), + })), + })).then(() => { + account.mark_keys_as_published(); + }).then(() => { + return cryptoStore.storeAccount(account); + }).then(() => { + dispatch(initializeCryptoSuccess(account)); + }); + }).catch(err => { + console.error(err); + dispatch(initializeCryptoFail(err)); + }); +}; + +const MESSAGE_PREKEY = 0; + +export const receiveCrypto = encryptedMessage => (dispatch, getState) => { + const { account_id, device_id, type, body } = encryptedMessage; + const deviceKey = `${account_id}:${device_id}`; + + cryptoStore.decryptMessage(deviceKey, type, body).then(payloadString => { + console.log(encryptedMessage, payloadString); + }); +}; diff --git a/app/javascript/mastodon/actions/crypto/store.js b/app/javascript/mastodon/actions/crypto/store.js new file mode 100644 index 000000000..438b3464b --- /dev/null +++ b/app/javascript/mastodon/actions/crypto/store.js @@ -0,0 +1,110 @@ +import Dexie from 'dexie'; +import Olm from 'olm'; + +const MESSAGE_TYPE_PREKEY = 0; + +export default class CryptoStore { + + constructor() { + this.pickleKey = 'DEFAULT_KEY'; + + this.db = new Dexie('mastodon-crypto'); + + this.db.version(1).stores({ + accounts: '', + sessions: 'deviceKey,sessionId', + }); + } + + // FIXME: Need to call free() on returned accounts at some point + // but it needs to happen *after* you're done using them + getAccount () { + return this.db.accounts.get('-').then(pickledAccount => { + if (typeof pickledAccount === 'undefined') { + return null; + } + + const account = new Olm.Account(); + + account.unpickle(this.pickleKey, pickledAccount); + + return account; + }); + } + + storeAccount (account) { + return this.db.accounts.put(account.pickle(this.pickleKey), '-'); + } + + storeSession (deviceKey, session) { + return this.db.sessions.put({ + deviceKey, + sessionId: session.session_id(), + pickledSession: session.pickle(this.pickleKey), + }); + } + + createInboundSession (deviceKey, type, body) { + return this.getAccount().then(account => { + const session = new Olm.Session(); + + let payloadString; + + try { + session.create_inbound(account, body); + account.remove_one_time_keys(session); + this.storeAccount(account); + + payloadString = session.decrypt(type, body); + + this.storeSession(deviceKey, session); + } finally { + session.free(); + account.free(); + } + + return payloadString; + }); + } + + // FIXME: Need to call free() on returned sessions at some point + // but it needs to happen *after* you're done using them + getSessionsForDevice (deviceKey) { + return this.db.sessions.where('deviceKey').equals(deviceKey).toArray().then(sessions => sessions.map(sessionData => { + const session = new Olm.Session(); + + session.unpickle(this.pickleKey, sessionData.pickledSession); + + return session; + })); + } + + decryptMessage (deviceKey, type, body) { + return this.getSessionsForDevice(deviceKey).then(sessions => { + let payloadString; + + sessions.forEach(session => { + try { + payloadString = this.decryptMessageForSession(deviceKey, session, type, body); + } catch (e) { + console.error(e); + } + }); + + if (typeof payloadString !== 'undefined') { + return payloadString; + } + + if (type === MESSAGE_TYPE_PREKEY) { + return this.createInboundSession(deviceKey, type, body); + } + }); + } + + decryptMessageForSession (deviceKey, session, type, body) { + const payloadString = session.decrypt(type, body); + this.storeSession(deviceKey, session); + return payloadString; + } + +} diff --git a/app/javascript/mastodon/actions/statuses.js b/app/javascript/mastodon/actions/statuses.js index e565e0b0a..b236c9cf3 100644 --- a/app/javascript/mastodon/actions/statuses.js +++ b/app/javascript/mastodon/actions/statuses.js @@ -1,6 +1,4 @@ import api from '../api'; -import openDB from '../storage/db'; -import { evictStatus } from '../storage/modifier'; import { deleteFromTimelines } from './timelines'; import { importFetchedStatus, importFetchedStatuses, importAccount, importStatus, importFetchedAccount } from './importer'; @@ -94,23 +92,10 @@ export function fetchStatus(id) { dispatch(fetchStatusRequest(id, skipLoading)); - openDB().then(db => { - const transaction = db.transaction(['accounts', 'statuses'], 'read'); - const accountIndex = transaction.objectStore('accounts').index('id'); - const index = transaction.objectStore('statuses').index('id'); - - return getFromDB(dispatch, getState, accountIndex, index, id).then(() => { - db.close(); - }, error => { - db.close(); - throw error; - }); - }).then(() => { - dispatch(fetchStatusSuccess(skipLoading)); - }, () => api(getState).get(`/api/v1/statuses/${id}`).then(response => { + api(getState).get(`/api/v1/statuses/${id}`).then(response => { dispatch(importFetchedStatus(response.data)); dispatch(fetchStatusSuccess(skipLoading)); - })).catch(error => { + }).catch(error => { dispatch(fetchStatusFail(id, error, skipLoading)); }); }; @@ -152,7 +137,6 @@ export function deleteStatus(id, routerHistory, withRedraft = false) { dispatch(deleteStatusRequest(id)); api(getState).delete(`/api/v1/statuses/${id}`).then(response => { - evictStatus(id); dispatch(deleteStatusSuccess(id)); dispatch(deleteFromTimelines(id)); dispatch(importFetchedAccount(response.data.account)); diff --git a/app/javascript/mastodon/actions/streaming.js b/app/javascript/mastodon/actions/streaming.js index 7cecff66e..527b90ab3 100644 --- a/app/javascript/mastodon/actions/streaming.js +++ b/app/javascript/mastodon/actions/streaming.js @@ -8,6 +8,7 @@ import { } from './timelines'; import { updateNotifications, expandNotifications } from './notifications'; import { updateConversations } from './conversations'; +import { receiveCrypto } from './crypto'; import { fetchAnnouncements, updateAnnouncements, @@ -59,6 +60,9 @@ export function connectTimelineStream (timelineId, path, pollingRefresh = null, case 'announcement.delete': dispatch(deleteAnnouncement(data.payload)); break; + case 'encrypted_message': + dispatch(receiveCrypto(JSON.parse(data.payload))); + break; } }, }; diff --git a/app/javascript/mastodon/features/direct_timeline/index.js b/app/javascript/mastodon/features/direct_timeline/index.js index 5ce795760..8b25f3194 100644 --- a/app/javascript/mastodon/features/direct_timeline/index.js +++ b/app/javascript/mastodon/features/direct_timeline/index.js @@ -3,17 +3,21 @@ import { connect } from 'react-redux'; import PropTypes from 'prop-types'; import Column from '../../components/column'; import ColumnHeader from '../../components/column_header'; -import { mountConversations, unmountConversations, expandConversations } from '../../actions/conversations'; +import { mountConversations } from '../../actions/conversations'; import { addColumn, removeColumn, moveColumn } from '../../actions/columns'; import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { connectDirectStream } from '../../actions/streaming'; +import { initializeCrypto, enableCrypto } from 'mastodon/actions/crypto'; import ConversationsListContainer from './containers/conversations_list_container'; const messages = defineMessages({ title: { id: 'column.direct', defaultMessage: 'Direct messages' }, }); -export default @connect() +const mapStateToProps = state => ({ + enabled: state.getIn(['crypto', 'enabled']), +}); + +export default @connect(mapStateToProps) @injectIntl class DirectTimeline extends React.PureComponent { @@ -24,6 +28,7 @@ class DirectTimeline extends React.PureComponent { intl: PropTypes.object.isRequired, hasUnread: PropTypes.bool, multiColumn: PropTypes.bool, + enabled: PropTypes.bool, }; handlePin = () => { @@ -49,29 +54,23 @@ class DirectTimeline extends React.PureComponent { const { dispatch } = this.props; dispatch(mountConversations()); - dispatch(expandConversations()); - this.disconnect = dispatch(connectDirectStream()); + dispatch(initializeCrypto()); } componentWillUnmount () { this.props.dispatch(unmountConversations()); - - if (this.disconnect) { - this.disconnect(); - this.disconnect = null; - } } setRef = c => { this.column = c; } - handleLoadMore = maxId => { - this.props.dispatch(expandConversations({ maxId })); + handleEnableCrypto = () => { + this.props.dispatch(enableCrypto()); } render () { - const { intl, hasUnread, columnId, multiColumn, shouldUpdateScroll } = this.props; + const { intl, hasUnread, columnId, multiColumn, shouldUpdateScroll, enabled } = this.props; const pinned = !!columnId; return ( @@ -87,14 +86,9 @@ class DirectTimeline extends React.PureComponent { multiColumn={multiColumn} /> - } - shouldUpdateScroll={shouldUpdateScroll} - /> + {!enabled && } + + {enabled && Crypto enabled} ); } diff --git a/app/javascript/mastodon/reducers/crypto.js b/app/javascript/mastodon/reducers/crypto.js new file mode 100644 index 000000000..e68e45ca9 --- /dev/null +++ b/app/javascript/mastodon/reducers/crypto.js @@ -0,0 +1,15 @@ +import { CRYPTO_INITIALIZE_SUCCESS } from 'mastodon/actions/crypto'; +import { Map as ImmutableMap } from 'immutable'; + +const initialState = ImmutableMap({ + enabled: false, +}); + +export default function crypto (state = initialState, action) { + switch(action.type) { + case CRYPTO_INITIALIZE_SUCCESS: + return state.set('enabled', action.account !== null); + default: + return state; + } +}; diff --git a/app/javascript/mastodon/reducers/index.js b/app/javascript/mastodon/reducers/index.js index 3823bb05e..3cf6e9169 100644 --- a/app/javascript/mastodon/reducers/index.js +++ b/app/javascript/mastodon/reducers/index.js @@ -36,6 +36,7 @@ import trends from './trends'; import missed_updates from './missed_updates'; import announcements from './announcements'; import markers from './markers'; +import crypto from './crypto'; const reducers = { announcements, @@ -75,6 +76,7 @@ const reducers = { trends, missed_updates, markers, + crypto, }; export default combineReducers(reducers); diff --git a/app/javascript/mastodon/storage/db.js b/app/javascript/mastodon/storage/db.js deleted file mode 100644 index 377a792a7..000000000 --- a/app/javascript/mastodon/storage/db.js +++ /dev/null @@ -1,27 +0,0 @@ -export default () => new Promise((resolve, reject) => { - // ServiceWorker is required to synchronize the login state. - // Microsoft Edge 17 does not support getAll according to: - // Catalog of standard and vendor APIs across browsers - Microsoft Edge Development - // https://developer.microsoft.com/en-us/microsoft-edge/platform/catalog/?q=specName%3Aindexeddb - if (!('caches' in self && 'getAll' in IDBObjectStore.prototype)) { - reject(); - return; - } - - const request = indexedDB.open('mastodon'); - - request.onerror = reject; - request.onsuccess = ({ target }) => resolve(target.result); - - request.onupgradeneeded = ({ target }) => { - const accounts = target.result.createObjectStore('accounts', { autoIncrement: true }); - const statuses = target.result.createObjectStore('statuses', { autoIncrement: true }); - - accounts.createIndex('id', 'id', { unique: true }); - accounts.createIndex('moved', 'moved'); - - statuses.createIndex('id', 'id', { unique: true }); - statuses.createIndex('account', 'account'); - statuses.createIndex('reblog', 'reblog'); - }; -}); diff --git a/app/javascript/mastodon/storage/modifier.js b/app/javascript/mastodon/storage/modifier.js deleted file mode 100644 index 9fadabef4..000000000 --- a/app/javascript/mastodon/storage/modifier.js +++ /dev/null @@ -1,211 +0,0 @@ -import openDB from './db'; - -const accountAssetKeys = ['avatar', 'avatar_static', 'header', 'header_static']; -const storageMargin = 8388608; -const storeLimit = 1024; - -// navigator.storage is not present on: -// Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.100 Safari/537.36 Edge/16.16299 -// estimate method is not present on Chrome 57.0.2987.98 on Linux. -export const storageFreeable = 'storage' in navigator && 'estimate' in navigator.storage; - -function openCache() { - // ServiceWorker and Cache API is not available on iOS 11 - // https://webkit.org/status/#specification-service-workers - return self.caches ? caches.open('mastodon-system') : Promise.reject(); -} - -function printErrorIfAvailable(error) { - if (error) { - console.warn(error); - } -} - -function put(name, objects, onupdate, oncreate) { - return openDB().then(db => (new Promise((resolve, reject) => { - const putTransaction = db.transaction(name, 'readwrite'); - const putStore = putTransaction.objectStore(name); - const putIndex = putStore.index('id'); - - objects.forEach(object => { - putIndex.getKey(object.id).onsuccess = retrieval => { - function addObject() { - putStore.add(object); - } - - function deleteObject() { - putStore.delete(retrieval.target.result).onsuccess = addObject; - } - - if (retrieval.target.result) { - if (onupdate) { - onupdate(object, retrieval.target.result, putStore, deleteObject); - } else { - deleteObject(); - } - } else { - if (oncreate) { - oncreate(object, addObject); - } else { - addObject(); - } - } - }; - }); - - putTransaction.oncomplete = () => { - const readTransaction = db.transaction(name, 'readonly'); - const readStore = readTransaction.objectStore(name); - const count = readStore.count(); - - count.onsuccess = () => { - const excess = count.result - storeLimit; - - if (excess > 0) { - const retrieval = readStore.getAll(null, excess); - - retrieval.onsuccess = () => resolve(retrieval.result); - retrieval.onerror = reject; - } else { - resolve([]); - } - }; - - count.onerror = reject; - }; - - putTransaction.onerror = reject; - })).then(resolved => { - db.close(); - return resolved; - }, error => { - db.close(); - throw error; - })); -} - -function evictAccountsByRecords(records) { - return openDB().then(db => { - const transaction = db.transaction(['accounts', 'statuses'], 'readwrite'); - const accounts = transaction.objectStore('accounts'); - const accountsIdIndex = accounts.index('id'); - const accountsMovedIndex = accounts.index('moved'); - const statuses = transaction.objectStore('statuses'); - const statusesIndex = statuses.index('account'); - - function evict(toEvict) { - toEvict.forEach(record => { - openCache() - .then(cache => accountAssetKeys.forEach(key => cache.delete(records[key]))) - .catch(printErrorIfAvailable); - - accountsMovedIndex.getAll(record.id).onsuccess = ({ target }) => evict(target.result); - - statusesIndex.getAll(record.id).onsuccess = - ({ target }) => evictStatusesByRecords(target.result); - - accountsIdIndex.getKey(record.id).onsuccess = - ({ target }) => target.result && accounts.delete(target.result); - }); - } - - evict(records); - - db.close(); - }).catch(printErrorIfAvailable); -} - -export function evictStatus(id) { - evictStatuses([id]); -} - -export function evictStatuses(ids) { - return openDB().then(db => { - const transaction = db.transaction('statuses', 'readwrite'); - const store = transaction.objectStore('statuses'); - const idIndex = store.index('id'); - const reblogIndex = store.index('reblog'); - - ids.forEach(id => { - reblogIndex.getAllKeys(id).onsuccess = - ({ target }) => target.result.forEach(reblogKey => store.delete(reblogKey)); - - idIndex.getKey(id).onsuccess = - ({ target }) => target.result && store.delete(target.result); - }); - - db.close(); - }).catch(printErrorIfAvailable); -} - -function evictStatusesByRecords(records) { - return evictStatuses(records.map(({ id }) => id)); -} - -export function putAccounts(records, avatarStatic) { - const avatarKey = avatarStatic ? 'avatar_static' : 'avatar'; - const newURLs = []; - - put('accounts', records, (newRecord, oldKey, store, oncomplete) => { - store.get(oldKey).onsuccess = ({ target }) => { - accountAssetKeys.forEach(key => { - const newURL = newRecord[key]; - const oldURL = target.result[key]; - - if (newURL !== oldURL) { - openCache() - .then(cache => cache.delete(oldURL)) - .catch(printErrorIfAvailable); - } - }); - - const newURL = newRecord[avatarKey]; - const oldURL = target.result[avatarKey]; - - if (newURL !== oldURL) { - newURLs.push(newURL); - } - - oncomplete(); - }; - }, (newRecord, oncomplete) => { - newURLs.push(newRecord[avatarKey]); - oncomplete(); - }).then(records => Promise.all([ - evictAccountsByRecords(records), - openCache().then(cache => cache.addAll(newURLs)), - ])).then(freeStorage, error => { - freeStorage(); - throw error; - }).catch(printErrorIfAvailable); -} - -export function putStatuses(records) { - put('statuses', records) - .then(evictStatusesByRecords) - .catch(printErrorIfAvailable); -} - -export function freeStorage() { - return storageFreeable && navigator.storage.estimate().then(({ quota, usage }) => { - if (usage + storageMargin < quota) { - return null; - } - - return openDB().then(db => new Promise((resolve, reject) => { - const retrieval = db.transaction('accounts', 'readonly').objectStore('accounts').getAll(null, 1); - - retrieval.onsuccess = () => { - if (retrieval.result.length > 0) { - resolve(evictAccountsByRecords(retrieval.result).then(freeStorage)); - } else { - resolve(caches.delete('mastodon-system')); - } - }; - - retrieval.onerror = reject; - - db.close(); - })); - }); -} diff --git a/config/routes.rb b/config/routes.rb index 349db0934..a16f89687 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -340,22 +340,22 @@ Rails.application.routes.draw do end end - # namespace :crypto do - # resources :deliveries, only: :create + namespace :crypto do + resources :deliveries, only: :create - # namespace :keys do - # resource :upload, only: [:create] - # resource :query, only: [:create] - # resource :claim, only: [:create] - # resource :count, only: [:show] - # end + namespace :keys do + resource :upload, only: [:create] + resource :query, only: [:create] + resource :claim, only: [:create] + resource :count, only: [:show] + end - # resources :encrypted_messages, only: [:index] do - # collection do - # post :clear - # end - # end - # end + resources :encrypted_messages, only: [:index] do + collection do + post :clear + end + end + end resources :conversations, only: [:index, :destroy] do member do diff --git a/config/webpack/rules/index.js b/config/webpack/rules/index.js index bbf6b0187..78c0a4704 100644 --- a/config/webpack/rules/index.js +++ b/config/webpack/rules/index.js @@ -2,6 +2,7 @@ const babel = require('./babel'); const css = require('./css'); const file = require('./file'); const nodeModules = require('./node_modules'); +const wasm = require('./wasm'); // Webpack loaders are processed in reverse order // https://webpack.js.org/concepts/loaders/#loader-features @@ -11,4 +12,5 @@ module.exports = { css, nodeModules, babel, + wasm, }; diff --git a/config/webpack/rules/wasm.js b/config/webpack/rules/wasm.js new file mode 100644 index 000000000..f2ad679c3 --- /dev/null +++ b/config/webpack/rules/wasm.js @@ -0,0 +1,8 @@ +module.exports = { + test: /\.wasm$/, + type: "javascript/auto", + loader: "file-loader", + options: { + name: '[name]-[hash].[ext]' + } +}; diff --git a/config/webpack/shared.js b/config/webpack/shared.js index 15a209253..dfe7ebead 100644 --- a/config/webpack/shared.js +++ b/config/webpack/shared.js @@ -109,5 +109,8 @@ module.exports = { // Called by http-link-header in an API we never use, increases // bundle size unnecessarily Buffer: false, + + // Called by olm + fs: 'empty', }, }; diff --git a/package.json b/package.json index 37c6d97d3..5ee1bd7a1 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "css-loader": "^3.6.0", "cssnano": "^4.1.10", "detect-passive-events": "^1.0.2", + "dexie": "^3.0.1", "dotenv": "^8.2.0", "emoji-mart": "Gargron/emoji-mart#build", "es6-symbol": "^3.1.3", @@ -117,6 +118,7 @@ "object-fit-images": "^3.2.3", "object.values": "^1.1.1", "offline-plugin": "^5.0.7", + "olm": "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz", "path-complete-extname": "^1.0.0", "pg": "^6.4.0", "postcss-loader": "^3.0.0", diff --git a/yarn.lock b/yarn.lock index 5449ae10f..0f6eed6b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3811,6 +3811,11 @@ detect-passive-events@^1.0.2: resolved "https://registry.yarnpkg.com/detect-passive-events/-/detect-passive-events-1.0.4.tgz#6ed477e6e5bceb79079735dcd357789d37f9a91a" integrity sha1-btR35uW863kHlzXc01d4nTf5qRo= +dexie@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dexie/-/dexie-3.0.1.tgz#faafeb94be0d5e18b25d700546a2c05725511cfc" + integrity sha512-/s4KzlaerQnCad/uY1ZNdFckTrbdMVhLlziYQzz62Ff9Ick1lHGomvTXNfwh4ApEZATyXRyVk5F6/y8UU84B0w== + diff-sequences@^25.2.6: version "25.2.6" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-25.2.6.tgz#5f467c00edd35352b7bca46d7927d60e687a76dd" @@ -7658,6 +7663,10 @@ offline-plugin@^5.0.7: minimatch "^3.0.3" slash "^1.0.0" +"olm@https://packages.matrix.org/npm/olm/olm-3.1.4.tgz": + version "3.1.4" + resolved "https://packages.matrix.org/npm/olm/olm-3.1.4.tgz#0f03128b7d3b2f614d2216409a1dfccca765fdb3" + on-finished@~2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947"