Repository: cryptag/leapchat Branch: develop Commit: c9aa4a398869 Files: 122 Total size: 583.7 KB Directory structure: gitextract_3ku5ae8o/ ├── .babelrc ├── .editorconfig ├── .eslintrc ├── .github/ │ └── workflows/ │ └── pull_request_javascript_check.yml ├── .gitignore ├── .nvmrc ├── LICENSE.md ├── Makefile ├── README.md ├── db/ │ ├── init_sql.sh │ ├── migrate.sh │ ├── postgrest.conf │ └── sql/ │ ├── init001.sql │ ├── migration001.sql │ ├── migration002.sql │ ├── migration003.sql │ ├── pre.sql │ ├── table01_rooms.sql │ └── table02_messages.sql ├── docker-compose.yml ├── fedora_install.sh ├── go.mod ├── go.sum ├── gzip.go ├── json.go ├── leapchat.go ├── messages.go ├── miniware/ │ └── miniware.go ├── package.json ├── pg_types.go ├── playwright.config.js ├── room.go ├── room_test.go ├── server.go ├── server_test.go ├── src/ │ ├── components/ │ │ ├── App.js │ │ ├── chat/ │ │ │ ├── AutoSuggest.js │ │ │ ├── ChatContainer.js │ │ │ ├── ChatRoom.js │ │ │ ├── EmojiSuggestions.js │ │ │ ├── MentionSuggestions.js │ │ │ ├── Message.js │ │ │ ├── MessageBox.js │ │ │ ├── MessageForm.js │ │ │ ├── MessageList.js │ │ │ ├── UserIcon.js │ │ │ ├── UserList.js │ │ │ ├── UserStatusIcons.js │ │ │ └── toolbar/ │ │ │ ├── InviteIcon.js │ │ │ ├── OpenSearchIcon.js │ │ │ └── ToggleAudioIcon.js │ │ ├── general/ │ │ │ ├── AlertContainer.js │ │ │ └── Throbber.js │ │ ├── layout/ │ │ │ ├── ChatRoom.js │ │ │ ├── Header.js │ │ │ ├── Info.js │ │ │ ├── Logo.js │ │ │ └── Settings.js │ │ └── modals/ │ │ ├── InfoModal.js │ │ ├── PincodeModal.js │ │ ├── SearchModal.js │ │ ├── SettingsModal.js │ │ ├── SharingModal.js │ │ └── Username.js │ ├── constants/ │ │ ├── emoji.js │ │ └── messaging.js │ ├── data/ │ │ ├── constants.js │ │ ├── effWordlist.js │ │ ├── minishare.js │ │ └── username.js │ ├── index-template.ejs │ ├── index.js │ ├── static/ │ │ ├── assets.json │ │ ├── css/ │ │ │ └── Lato.css │ │ ├── js/ │ │ │ └── emoji-fixed.js │ │ └── sass/ │ │ ├── _emojiPicker.scss │ │ ├── _layout.scss │ │ ├── _suggestions.scss │ │ ├── _variables.scss │ │ └── main.scss │ ├── store/ │ │ ├── actions/ │ │ │ ├── alertActions.js │ │ │ ├── chatActions.js │ │ │ └── settingsActions.js │ │ ├── epics/ │ │ │ ├── chatEpics.js │ │ │ ├── helpers/ │ │ │ │ ├── ChatHandler.js │ │ │ │ ├── createDetectPageVisibilityObservable.js │ │ │ │ └── urls.js │ │ │ └── index.js │ │ └── reducers/ │ │ ├── alertReducer.js │ │ ├── chatReducer.js │ │ ├── helpers/ │ │ │ └── deviceState.js │ │ ├── index.js │ │ └── settingsReducer.js │ └── utils/ │ ├── audio.js │ ├── chat.js │ ├── crypto/ │ │ ├── nacl.js │ │ └── scrypt.js │ ├── detect_browser.js │ ├── emoji_convertor.js │ ├── encrypter.js │ ├── link_attr_blank.js │ ├── miniLock.js │ ├── origin_polyfill.js │ ├── pagevisibility.js │ ├── sessions.js │ ├── suggestions.js │ ├── tags.js │ ├── time.js │ └── vh_fix.js ├── test/ │ ├── .setup.js │ ├── playwright/ │ │ ├── ChangeUsername.spec.js │ │ ├── InfoModal.spec.js │ │ ├── InviteUsers.spec.js │ │ ├── Message.spec.js │ │ ├── SearchModal.spec.js │ │ ├── SetUsername.spec.js │ │ ├── SettingsModal.spec.js │ │ └── Welcome.spec.js │ └── utils/ │ └── tags.test.js ├── webpack.config.base.js ├── webpack.config.dev.js └── webpack.config.prod.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ "@babel/preset-react", "@babel/preset-env" ], "plugins": [ "system-import-transformer", "transform-class-properties", "@babel/plugin-proposal-object-rest-spread" ] } ================================================ FILE: .editorconfig ================================================ indent_style = space indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: .eslintrc ================================================ { "parser": "@babel/eslint-parser", "plugins": [ "@babel", "react" ], "parserOptions": { "ecmaFeatures": { "arrowFunctions": true, "binaryLiterals": true, "blockBindings": true, "classes": true, "defaultParams": true, "destructuring": true, "forOf": true, "modules": true, "objectLiteralComputedProperties": true, "objectLiteralDuplicateProperties": false, "objectLiteralShorthandMethods": true, "objectLiteralShorthandProperties": true, "octalLiterals": true, "restParams": true, "spread": true, "templateStrings": true, "unicodeCodePointEscapes": true, "globalReturn": false, "jsx": true, "experimentalObjectRestSpread": true, } }, "rules": { "indent": ["error", 2], "semi": [2], "react/jsx-indent-props": ["error", 2], "react/jsx-indent": ["error", 2] }, "env": { "node": true, "es6": true, "jest": true, "jasmine": true } } ================================================ FILE: .github/workflows/pull_request_javascript_check.yml ================================================ name: JavaScript Lint and Test Check on: pull_request: branches: [develop] jobs: run_eslinter: name: Runs ESLint and execute test suite runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Use Node.js uses: actions/setup-node@v3 with: node-version: 14.x - run: npm install - run: npm run lint - run: npm run mocha # - run: npx playwright install-deps # - run: npm test ================================================ FILE: .gitignore ================================================ *~ node_modules/ build/ leapchat # bower static assets static/lib/ # docker container volumes _docker-volumes/ # Editors .vscode/ # Mac OS X Bullshit .DS_Store # Playwright tests playwright-report/ ================================================ FILE: .nvmrc ================================================ v14.0.0 ================================================ FILE: LICENSE.md ================================================ Copyright (C) 2017 CrypTag This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . ================================================ FILE: Makefile ================================================ build: go build npm run build release: check-env @echo 'Hopefully "git diff" is empty!' @echo 'Creating release for version $(version) ...' @echo 'Manually change the version in these 2 files to $(version) and I, your loyal, Makefile, shall do the rest!' @emacsclient -t package.json @emacsclient -t package-lock.json @git add -p package.json package-lock.json @git commit -m 'Version bump to v$(version)' @git tag v$(version) @git push --tags origin develop deploy: check-env @tar zcvpf releases/leapchat-v$(version)-$$(mydate.sh).tar.gz ./leapchat ./db ./build $(MAKE) upload upload: @scp $$(ls -t releases/*.tar.gz | head -1) leapchat-minishare:~/gocode/src/github.com/cryptag/leapchat/releases/ @ssh leapchat-minishare all-deploy: $(MAKE) -B build $(MAKE) release $(MAKE) deploy check-env: ifndef version $(error "version" variable is undefined; re-run with "version=1.2.3" or similar) endif ================================================ FILE: README.md ================================================ # LeapChat LeapChat is an ephemeral chat application. LeapChat uses [miniLock](https://web.archive.org/web/20180508023310/https://minilock.io/) for challenge/response-based authentication. This app also enables users to create chat rooms, invite others to said rooms (via a special URL with a passphrase at the end of it that is used to generate a miniLock keypair), and of course send (encrypted) messages to the other chat room participants. ## Security Features - All messages are encrypted end-to-end - The server cannot see anyone's usernames, which are encrypted and attached to each message - Users can "leap" from one room to the next so that if an adversary clicks on an old invite link, it cannot be used to join the room - (Feature coming soon!) - [Very secure headers](https://securityheaders.io/?q=https%3A%2F%2Fwww.leapchat.org&followRedirects=on) thanks to [gosecure](https://github.com/cryptag/gosecure). - TODO (many more) ## Instances There is currently one public instance running at [leapchat.org](https://www.leapchat.org). # Running LeapChat ## Getting Started ### Install Go If you're on Linux or macOS _and_ if don't already have [Go](https://golang.org/dl/) version 1.14 or newer installed (`$ go version` will tell you), you can install Go by running: ``` curl https://raw.githubusercontent.com/elimisteve/install-go/master/install-go.sh | bash source ~/.bashrc ``` Then grab and build the `leapchat` source: ``` go get github.com/cryptag/leapchat ``` ### JavaScript and Node Setup Install Node v14. We recommend using the [Node Version Manager (nvm)](https://github.com/nvm-sh/nvm) package to manage your node environments. If you're using NVM, you can install the correct node version by running: ``` nvm install # run from inside of leapchat/ dir, uses .nvmrc file nvm install v14.0.0 # run from anywhere ``` Then, to configure the use of the correct node version whenever you enter the project: ``` cd ~/code/leapchat && nvm use ``` To install JavaScript dependencies: ``` npm install ``` In development, when you want to see your frontend code changes immediately on a browser refresh, run the command that boots up a watch process to re-compile the frontend whenever a file changes: ``` npm run dev ``` In order to do a one-time build of the production assets: ``` npm run build ``` The frontend is served through an HTTP server running in the go binary. This allows us to make API requests from the browser without any CORS configuration. ### macOS Instructions If you don't already have Postgres 9.5 to Postgres 12 installed and running, install it with Homebrew: ``` brew install postgresql@12 ``` (It may ask you to append a line to your shell config; watch for this and follow those instructions.) Next, you'll need three terminals. **In the first terminal**, run database migrations, download `postgrest`, and have `postgrest` connect to Postgres: Start the PostgreSQL server: ``` brew services start postgresql@12 ``` Then, create the default database and run the migrations. ``` cd $(go env GOPATH)/src/github.com/cryptag/leapchat/db chmod a+rx ~/ createdb sudo -u $USER bash init_sql.sh ``` We use [PostgREST](https://postgrest.org/en/stable/) to expose the database to the application. PostgREST provides a REST API interface that maps to the underlying tables. To install and run the REST API with the LeapChat configuration file: ``` brew install postgrest postgrest db/postgrest.conf ``` If you get an error, make sure that Postgres is running. On Mac OS, you can check PostgreSQL status by running: ``` brew services list ``` **In the second terminal**, run LeapChat's Go backend: ``` cd $(go env GOPATH)/src/github.com/cryptag/leapchat go build ./leapchat ``` **In the third terminal**, install JavaScript dependencies and start LeapChat's auto-reloading dev server: ``` cd $(go env GOPATH)/src/github.com/cryptag/leapchat npm install npm run dev ``` LeapChat's dev server should now be running on ! #### macOS: Once you're set up ...then run in 3 different terminals: ``` brew services start postgresql@12 postgrest db/postgrest.conf ``` ``` ./leapchat ``` ``` npm run dev ``` ### Linux Instructions (for Ubuntu; works on Debian if other dependencies met) If you don't already have Node 14.x installed (`node --version` will tell you the installed version), install Node by running: ``` curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - sudo apt-get install nodejs ``` If you don't already have Postgres 9.5 or newer installed and running, install it by running: ``` sudo apt-get install postgresql postgresql-contrib ``` Next, you'll need three terminals. **In the first terminal**, run database migrations, download `postgrest`, and have `postgrest` connect to Postgres: ``` cd $(go env GOPATH)/src/github.com/cryptag/leapchat/db chmod a+rx ~/ sudo -u postgres bash init_sql.sh wget https://github.com/PostgREST/postgrest/releases/download/v7.0.0/postgrest-v7.0.0-ubuntu.tar.xz tar xvf postgrest-v7.0.0-ubuntu.tar.xz ./postgrest postgrest.conf ``` **In the second terminal**, run LeapChat's Go backend: ``` cd $(go env GOPATH)/src/github.com/cryptag/leapchat go build ./leapchat ``` **In the third terminal**, install JavaScript dependencies and start LeapChat's auto-reloading dev server: ``` cd $(go env GOPATH)/src/github.com/cryptag/leapchat npm install npm run build npm run start ``` LeapChat should now be running at ! #### Linux: Once you're set up ...then run in 3 different terminals: ``` cd db ./postgrest postgrest.conf ``` ``` ./leapchat ``` ``` npm run start ``` ### Production Build and Deploy Make sure you're in the default branch (currently `develop`), and make sure that `git diff` doesn't return anything, then run these commands to create a new, versioned release of LeapChat, perform a production build, then deploy that build to production: (Be sure to customize `version` to the actual new version number.) ``` make all-deploy version=1.2.3 ``` Or to run the build, release, and deploy steps individually: ``` make -B build make release version=1.2.3 make deploy version=1.2.3 ``` If the build and release succeed but the upload (and thus the rest of the deployment) fails, you can deploy the latest local build (in `./releases/`) with ``` make upload ``` Once SSH'd in, kill the old `leapchat` process then run ``` cd ~/gocode/src/github.com/cryptag/leapchat tar xvf $(ls -t releases/*.tar.gz | head -1) sudo setcap cap_net_bind_service=+ep leapchat ./leapchat -prod -domain www.leapchat.org -http :80 -https :443 -iframe-origin www.leapchat.org 2>&1 | tee -a logs.txt ``` ## Documentation Links Open via `npm`: ``` npm docs bootstrap npm docs react-bootstrap npm docs react-icons ``` ## JavaScript Testing ### Unit Tests For unit tests, use [mocha](https://mochajs.org/) as the testing framework and test runner, with [chai](http://chaijs.com/)'s expect API. Unit tests are located at `./test/` and have an extension of `.test.js`. To run unit tests only, run: ``` npm run mocha ``` ### Browser Tests For browser tests, we use [playwright](https://playwright.dev/). This should be installed for you via `npm`, but you may need to install the playwright browser have tests run successfully: ``` npx playwright install-deps ``` Browser tests are located at `./test/playwright` and have an extension of `.spec.js`. To run browser tests only, run: ``` npm run playwright ``` By default, the browser tests run in headless mode. To run with an interactive browser session, run: ``` npm run webtests ``` **To run all of the tests, all together**: ``` npm test ``` ### Documentation Links Playwright has good docs. For quick access, here are some useful links: [Accessing the DOM with Playwright](https://playwright.dev/docs/api/class-locator) [Test Assertions with Playwright](https://playwright.dev/docs/test-assertions) Currently experimental: [Unit Testing React Components with Playwright](https://playwright.dev/docs/test-components) ## Go Testing To run the golang tests: ``` $ go test [-v] ./... ``` # Cryptography Notice This distribution includes cryptographic software. The country in which you currently reside may have restrictions on the import, possession, use, and/or re-export to another country, of encryption software. BEFORE using any encryption software, please check your country's laws, regulations and policies concerning the import, possession, or use, and re-export of encryption software, to see if this is permitted. See for more information. The U.S. Government Department of Commerce, Bureau of Industry and Security (BIS), has classified this software as Export Commodity Control Number (ECCN) 5D002.C.1, which includes information security software using or performing cryptographic functions with asymmetric algorithms. The form and manner of this distribution makes it eligible for export under the License Exception ENC Technology Software Unrestricted (TSU) exception (see the BIS Export Administration Regulations, Section 740.13) for both object code and source code. ================================================ FILE: db/init_sql.sh ================================================ #!/bin/bash set -euo pipefail # Create 'leapchat' database, associated role psql -d postgres < sql/pre.sql export pg_user=postgres if [ "`uname -s`" != "Linux" ]; then # For Mac OS X pg_user=$USER fi # More initialization for file in sql/init*.sql; do psql -U $pg_user -d leapchat < "$file" done # Create tables for file in sql/table*.sql; do psql -U $pg_user -d leapchat < "$file" done /bin/bash migrate.sh sql/migration*.sql ================================================ FILE: db/migrate.sh ================================================ #!/bin/bash # Steve Phillips / elimisteve # 2017.05.18 set -euo pipefail # Run migrations for file in $*; do psql -U ${pg_user:-postgres} -d leapchat < "$file" done ================================================ FILE: db/postgrest.conf ================================================ db-uri = "postgres://superuser:superuser@localhost:5432/leapchat" db-schema = "public" db-anon-role = "superuser" db-pool = 10 server-host = "127.0.0.1" server-port = 3000 ================================================ FILE: db/sql/init001.sql ================================================ create extension if not exists "uuid-ossp"; ================================================ FILE: db/sql/migration001.sql ================================================ ALTER TABLE messages ALTER COLUMN message SET DEFAULT ''; ================================================ FILE: db/sql/migration002.sql ================================================ ALTER TABLE messages ALTER COLUMN ttl_secs SET NOT NULL; ================================================ FILE: db/sql/migration003.sql ================================================ CREATE FUNCTION delete_expired_messages() RETURNS void AS $$ DELETE FROM messages WHERE created + interval '1s' * ttl_secs < now(); $$ LANGUAGE SQL VOLATILE; ================================================ FILE: db/sql/pre.sql ================================================ CREATE USER superuser WITH PASSWORD 'superuser'; CREATE DATABASE leapchat OWNER superuser ENCODING 'UTF8'; GRANT ALL ON DATABASE leapchat TO superuser; ALTER USER superuser CREATEDB; ================================================ FILE: db/sql/table01_rooms.sql ================================================ CREATE TABLE rooms ( room_id text NOT NULL UNIQUE PRIMARY KEY CHECK (40 <= LENGTH(room_id) AND LENGTH(room_id) <= 55) ); ALTER TABLE rooms OWNER TO superuser; ================================================ FILE: db/sql/table02_messages.sql ================================================ CREATE TABLE messages ( message_id uuid NOT NULL UNIQUE PRIMARY KEY DEFAULT uuid_generate_v4(), room_id text NOT NULL REFERENCES rooms ON DELETE CASCADE, message text NOT NULL, message_enc bytea NOT NULL, ttl_secs integer DEFAULT 7776000 CHECK (60 <= ttl_secs AND ttl_secs <= 7776000), created timestamp WITH time zone DEFAULT now() ); ALTER TABLE messages OWNER TO superuser; ================================================ FILE: docker-compose.yml ================================================ version: '3.1' services: postgres: image: postgres:latest ports: - 127.0.0.1:5432:5432 environment: - POSTGRES_PASSWORD=superuser - POSTGRES_USER=superuser - POSTGRES_DB=leapchat volumes: - ./_docker-volumes/postgres:/var/lib/postgresql/data postgrest: image: postgrest/postgrest:latest ports: - 3000:3000 environment: PGUSER: superuser PGPASSWORD: superuser PGHOST: postgres PGPORT: 5432 PGDATABASE: leapchat PGSCHEMA: public DB_ANON_ROLE: postgres depends_on: - postgres ================================================ FILE: fedora_install.sh ================================================ #!/bin/bash # Matthew Leeds # 2017.07.08 sudo dnf install postgresql postgresql-server postgresql-contrib ================================================ FILE: go.mod ================================================ module github.com/cryptag/leapchat go 1.14 require ( github.com/cathalgarvey/base58 v0.0.0-20150930172411-5e83fd6f66e3 // indirect github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e // indirect github.com/cryptag/go-minilock v0.0.0-20230307201426-f138c5839651 github.com/cryptag/gosecure v0.0.0-20180117073251-9b5880940d72 github.com/dchest/blake2s v1.0.0 // indirect github.com/gorilla/context v1.1.1 github.com/gorilla/mux v1.7.1 github.com/gorilla/websocket v1.4.1 github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d github.com/sirupsen/logrus v1.4.1 github.com/stretchr/testify v1.8.2 github.com/tv42/base58 v1.0.0 // indirect golang.org/x/crypto v0.7.0 launchpad.net/gocheck v0.0.0-20140225173054-000000000087 // indirect ) ================================================ FILE: go.sum ================================================ github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/cathalgarvey/base58 v0.0.0-20150930172411-5e83fd6f66e3 h1:UiCAzJ/7RzEVGU8SxQcrgtkbOUCDYfGYHAZHYvWbW3o= github.com/cathalgarvey/base58 v0.0.0-20150930172411-5e83fd6f66e3/go.mod h1:JHU0bsaIIAwKdJ84j3JNHDGsXehsQIS6EySrtGIZ4fs= github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e h1:0XBUw73chJ1VYSsfvcPvVT7auykAJce9FpRr10L6Qhw= github.com/cmars/basen v0.0.0-20150613233007-fe3947df716e/go.mod h1:P13beTBKr5Q18lJe1rIoLUqjM+CB1zYrRg44ZqGuQSA= github.com/cryptag/go-minilock v0.0.0-20160315171457-c7289f173516 h1:2IfztZwF6swS/zXafkcBppYad9lO+Cvd2Au/IICmXTM= github.com/cryptag/go-minilock v0.0.0-20160315171457-c7289f173516/go.mod h1:caKtUaGD8uPTpeGNYJCZmHe9c1viq8LxZtlsVPgMGHc= github.com/cryptag/go-minilock v0.0.0-20230307201426-f138c5839651 h1:rOxjZMKV4wDUe1DHinSJ8oLgI7F3W8f9uI4T17KnD48= github.com/cryptag/go-minilock v0.0.0-20230307201426-f138c5839651/go.mod h1:mP3jjk8yMP5bPGrEG/jTgg3e1A3671eIPo35BVjfO+M= github.com/cryptag/gosecure v0.0.0-20180117073251-9b5880940d72 h1:Sq2y8zqJ0Jn06iwMQkp1jVAdReL2T3QCqfrVu4IhoSc= github.com/cryptag/gosecure v0.0.0-20180117073251-9b5880940d72/go.mod h1:x0RZxj8S2BI5vAoBOTvjZk1pawEY5+9n0xp8wzd3VQg= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dchest/blake2s v1.0.0 h1:gHCBR8ecSImY/Nwk7X0Q2KJAJcpI/HSkUAQDi8MCP4Q= github.com/dchest/blake2s v1.0.0/go.mod h1:GrKn2Lc4hWqAwRrbneYuvZ6kugiJMrjk3HHtcJkEhbs= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/mux v1.7.1 h1:Dw4jY2nghMMRsh1ol8dv1axHkDwMQK2DHerMNJsIpJU= github.com/gorilla/mux v1.7.1/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJJqL93Duh1xhAs4I2Zs8mKS89XWXFGp9cs= github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da h1:5y58+OCjoHCYB8182mpf/dEsq0vwTKPOo4zGfH0xW9A= github.com/justinas/alice v0.0.0-20171023064455-03f45bd4b7da/go.mod h1:oLH0CmIaxCGXD67VKGR5AacGXZSMznlmeqM8RzPrcY8= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ= github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/tv42/base58 v1.0.0 h1:ZN6pfg9LN98oUzMfc9axMNXuWxqJezO2S+atn1S5f4U= github.com/tv42/base58 v1.0.0/go.mod h1:JvBtPdU9grJ9mB4/W/j8gK5KJwXHkwIrB9DC2snzGC4= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= launchpad.net/gocheck v0.0.0-20140225173054-000000000087 h1:Izowp2XBH6Ya6rv+hqbceQyw/gSGoXfH/UPoTGduL54= launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= ================================================ FILE: gzip.go ================================================ package main import ( "compress/gzip" "io" "net/http" "strings" ) // GZip solution derived from // https://www.lemoda.net/go/gzip-handler/index.html and // https://stackoverflow.com/a/50898293/197160 type gzipResponseWriter struct { io.Writer http.ResponseWriter } // Use the Writer part of gzipResponseWriter to write the output. func (w gzipResponseWriter) Write(b []byte) (int, error) { return w.Writer.Write(b) } func inAnyStr(s string, container []string) bool { for i := 0; i < len(container); i++ { if strings.Contains(container[i], s) { return true } } return false } func gzipHandler(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if inAnyStr("gzip", r.Header["Accept-Encoding"]) { w.Header().Set("Content-Encoding", "gzip") gz := gzip.NewWriter(w) defer gz.Close() h.ServeHTTP(gzipResponseWriter{Writer: gz, ResponseWriter: w}, r) return } h.ServeHTTP(w, r) }) } ================================================ FILE: json.go ================================================ // Steve Phillips / elimisteve // 2017.01.16 package main import ( "fmt" "net/http" "github.com/gorilla/websocket" log "github.com/sirupsen/logrus" ) const contentTypeJSON = "application/json; charset=utf-8" func WriteError(w http.ResponseWriter, errStr string, secretErr error) error { return WriteErrorStatus(w, errStr, secretErr, http.StatusInternalServerError) } func WriteErrorStatus(w http.ResponseWriter, errStr string, secretErr error, status int) error { log.Debugf("Real error: %v", secretErr) log.Debugf("Returning HTTP %d w/error: %q", status, errStr) w.Header().Set("Content-Type", contentTypeJSON) w.WriteHeader(status) _, err := fmt.Fprintf(w, `{"error":%q}`, errStr) return err } // WebSockets func WSWriteError(wsConn *websocket.Conn, errStr string, secretErr error) error { log.Debugf("WebSocket error: " + secretErr.Error()) wsErr := fmt.Sprintf(`{"error":%q}`, errStr) err := wsConn.WriteMessage(websocket.TextMessage, []byte(wsErr)) wsConn.Close() // TODO: Will this panic? return err } ================================================ FILE: leapchat.go ================================================ package main import ( "flag" "strings" "github.com/cryptag/go-minilock/taber" "github.com/cryptag/leapchat/miniware" log "github.com/sirupsen/logrus" ) var ( randomServerKey *taber.Keys BUILD_DIR = "build" ) func init() { k, err := taber.RandomKey() if err != nil { log.Fatalf("Error generating random server key: %v\n", err) } // Setting global var randomServerKey = k } func main() { httpAddr := flag.String("http", "127.0.0.1:8080", "Address to listen on HTTP") httpsAddr := flag.String("https", "127.0.0.1:8443", "Address to listen on HTTPS") domain := flag.String("domain", "", "Domain of this service") iframeOrigin := flag.String("iframe-origin", "", "Origin that may embed this LeapChat instance into an iframe."+ " May include port. Only used with -prod flag.") prod := flag.Bool("prod", false, "Run in Production mode.") onionPush := flag.Bool("onionpush", false, "Serve OnionPush instead of LeapChat") flag.Parse() if *onionPush { *httpAddr = "127.0.0.1:5001" BUILD_DIR = "public" } if *prod { log.SetLevel(log.FatalLevel) } else { log.SetLevel(log.DebugLevel) } m := miniware.NewMapper() srv := NewServer(m, *httpAddr) if *prod { if *domain == "" { log.Fatal("You must specify a -domain when using the -prod flag.") } manager := getAutocertManager(*domain) // Setup http->https redirection httpsPort := strings.SplitN(*httpsAddr, ":", 2)[1] go redirectToHTTPS(*httpAddr, httpsPort, *domain, manager) // Production modifications to server ProductionServer(srv, *httpsAddr, *domain, manager, *iframeOrigin) log.Infof("Listening on %v", *httpsAddr) log.Fatal(srv.ListenAndServeTLS("", "")) } else { log.Infof("Listening on %v", *httpAddr) log.Fatal(srv.ListenAndServe()) } } ================================================ FILE: messages.go ================================================ package main import ( "encoding/json" "net/http" "github.com/cryptag/leapchat/miniware" "github.com/gorilla/websocket" log "github.com/sirupsen/logrus" ) type Message []byte type OutgoingPayload struct { Ephemeral []Message `json:"ephemeral"` FromServer FromServer `json:"from_server,omitempty"` } type FromServer struct { AllMessagesDeleted bool `json:"all_messages_deleted,omitempty"` } type ToServer struct { TTL *int `json:"ttl_secs"` DeleteAllMessages bool `json:"delete_all_messages"` } type IncomingPayload struct { Ephemeral []Message `json:"ephemeral"` ToServer ToServer `json:"to_server"` } func WSMessagesHandler(rooms *RoomManager) func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) { // Both guaranteed by middleware wsConn, _ := miniware.GetWebsocketConn(r) roomID, _ := miniware.GetMinilockID(r) room := rooms.GetRoom(roomID) client := &Client{ wsConn: wsConn, room: room, } room.AddClient(client) go messageReader(room, client) } } func messageReader(room *Room, client *Client) { msgs, err := room.GetMessages() if err != nil { client.SendError(err.Error(), err) return } // Send them already-existing messages err = client.SendMessages(msgs...) if err != nil { client.SendError(err.Error(), err) return } for { messageType, p, err := client.wsConn.ReadMessage() if err != nil { // TODO: Consider adding more checks log.Debugf("Error reading ws message: %s", err) room.RemoveClient(client) return } // Respond to message depending on message type switch messageType { case websocket.TextMessage: var payload IncomingPayload err := json.Unmarshal(p, &payload) if err != nil { log.Debugf("Error unmarshalling message `%s` -- %s", p, err) continue } if payload.ToServer.DeleteAllMessages { err = room.DeleteAllMessages() if err != nil { room.BroadcastMessages(client, Message(err.Error())) log.Errorf("Error deleting all messages from room %s -- %s", room.ID, err) continue } room.BroadcastDeleteSignal() continue } err = room.AddMessages(payload.Ephemeral, payload.ToServer.TTL) if err != nil { log.Debugf("Error from AddMessages: %v", err) continue } room.BroadcastMessages(client, payload.Ephemeral...) case websocket.BinaryMessage: log.Debug("Binary messages are unsupported") case websocket.CloseMessage: log.Debug("Got close message") room.RemoveClient(client) return default: log.Debugf("Unsupport messageType: %d", messageType) } } } ================================================ FILE: miniware/miniware.go ================================================ // Steve Phillips / elimisteve // 2017.04.01 package miniware import ( "errors" "fmt" "net/http" "sync" "time" "github.com/cryptag/go-minilock/taber" gorillacontext "github.com/gorilla/context" "github.com/gorilla/websocket" log "github.com/sirupsen/logrus" ) const ( MINILOCK_ID_KEY = "minilock_id" MINILOCK_KEYPAIR_KEY = "minilock_keypair" WEBSOCKET_CONNECTION = "websocket_connection" AuthError = "Error authorizing you" ) var ( ErrAuthTokenNotFound = errors.New("Auth token not found") ErrMinilockIDNotFound = errors.New("miniLock ID not found") ) type Mapper struct { lock sync.RWMutex m map[string]string // map[authToken]minilockID } func NewMapper() *Mapper { return &Mapper{m: map[string]string{}} } func (m *Mapper) GetMinilockID(authToken string) (string, error) { m.lock.RLock() defer m.lock.RUnlock() mID, ok := m.m[authToken] if !ok { return "", ErrAuthTokenNotFound } return mID, nil } func (m *Mapper) SetMinilockID(authToken, mID string) error { m.lock.Lock() defer m.lock.Unlock() m.m[authToken] = mID return nil } var upgrader = websocket.Upgrader{ ReadBufferSize: 1024, WriteBufferSize: 1024, HandshakeTimeout: 45 * time.Second, CheckOrigin: func(r *http.Request) bool { origin := r.Header.Get("Origin") return origin == "http://127.0.0.1:8080" || // dev origin == "http://localhost:8080" || // dev origin == "http://10.0.2.2:8080" || // Android emulator origin == "http://leapchat.org" || // prod origin == "https://leapchat.org" || // prod origin == "http://www.leapchat.org" || // prod origin == "https://www.leapchat.org" || // prod origin == "" || // CLI origin == "http://localhost" // Capacitor }, } func Auth(h http.Handler, m *Mapper) func(w http.ResponseWriter, req *http.Request) { return func(w http.ResponseWriter, req *http.Request) { wsConn, err := upgrader.Upgrade(w, req, nil) if err != nil { errStr := "Unable to upgrade to websocket conn" log.Debug(errStr + ": " + err.Error()) writeError(w, errStr, http.StatusBadRequest) return } var authToken string auth := make(chan interface{}) go func() { messageType, p, err := wsConn.ReadMessage() if err != nil { auth <- err return } log.Debugf("Received message of type %v: `%s`", messageType, p) if messageType != websocket.TextMessage { auth <- fmt.Errorf("Wanted type %v (TextMessage), got %v", websocket.TextMessage, messageType) return } auth <- string(p) }() timeout := time.After(5 * time.Second) select { case <-timeout: errStr := "Timed out; didn't send miniLock ID/room ID fast enough" writeWSError(wsConn, errStr) return case token := <-auth: switch maybeToken := token.(type) { case error: errStr := maybeToken.Error() writeWSError(wsConn, errStr) return case string: authToken = maybeToken // FALL THROUGH } } mID, err := m.GetMinilockID(authToken) if err != nil { status := http.StatusInternalServerError if err == ErrAuthTokenNotFound { status = http.StatusUnauthorized } log.Debugf("%v error from GetMinilockID: %v", status, err) writeWSError(wsConn, AuthError) return } log.Infof("`%s` just authed successfully; auth token: `%s`\n", mID, authToken) // TODO: Update auth token's TTL/lease to be 1 hour from // _now_, not just 1 hour since when they first logged in keypair, err := taber.FromID(mID) if err != nil { log.Debugf("Error from GetMinilockID: %v", err) writeWSError(wsConn, "Your miniLock ID is invalid?...") return } gorillacontext.Set(req, MINILOCK_ID_KEY, mID) gorillacontext.Set(req, MINILOCK_KEYPAIR_KEY, keypair) gorillacontext.Set(req, WEBSOCKET_CONNECTION, wsConn) h.ServeHTTP(w, req) } } func GetMinilockID(req *http.Request) (string, error) { mID := gorillacontext.Get(req, MINILOCK_ID_KEY) mIDStr, ok := mID.(string) if !ok { return "", ErrMinilockIDNotFound } return mIDStr, nil } func GetWebsocketConn(req *http.Request) (*websocket.Conn, error) { wsConnInterface := gorillacontext.Get(req, WEBSOCKET_CONNECTION) wsConn, ok := wsConnInterface.(*websocket.Conn) if !ok { return nil, ErrMinilockIDNotFound } return wsConn, nil } func writeWSError(wsConn *websocket.Conn, errStr string) error { log.Debug(errStr) resp := fmt.Sprintf(`{"error":%q}`, errStr) err := wsConn.WriteMessage(websocket.TextMessage, []byte(resp)) wsConn.Close() return err } func writeError(w http.ResponseWriter, errStr string, statusCode int) { errJSON := fmt.Sprintf(`{"error":%q}`, errStr) http.Error(w, errJSON, statusCode) } ================================================ FILE: package.json ================================================ { "name": "LeapChat", "version": "0.7.8", "description": "Self-destructing, encrypted, in-browser chat", "main": "index.js", "scripts": { "lint": "./node_modules/.bin/eslint ./src", "build": "node_modules/.bin/webpack --config webpack.config.prod.js", "start": "npm run build && ./leapchat", "dev": "node_modules/.bin/webpack --config webpack.config.dev.js --progress", "be": "./leapchat", "mocha": "./node_modules/.bin/mocha --reporter nyan test/.setup.js test/**/*.test.js", "playwright": "npx playwright test", "webtest": "npx playwright test --headed", "test": "npm run mocha && npm run playwright", "lintfix": "./node_modules/.bin/eslint --fix --parser @babel/eslint-parser --ext js --no-eslintrc --rule 'indent: [\"error\", 2]' --rule 'semi: [2]' ./src" }, "repository": { "type": "git", "url": "git://github.com/cryptag/leapchat.git" }, "keywords": [ "cryptag", "messaging", "chat", "privacy", "security", "encryption", "file", "sharing" ], "author": "Steve Phillips ", "license": "AGPL-3.0", "bugs": { "url": "https://github.com/cryptag/leapchat/issues" }, "homepage": "https://github.com/cryptag/leapchat#readme", "dependencies": { "@babel/preset-env": "^7.20.2", "@emoji-mart/data": "^1.1.2", "atob": "^2.1.2", "babel-loader": "^9.1.2", "btoa": "^1.2.1", "crypto-browserify": "^3.12.0", "emoji-datasource-apple": "^3.0.0", "emoji-js": "^3.5.0", "emoji-mart": "^1.0.1", "guid": "0.0.12", "jquery": "^3.6.3", "js-sha512": "^0.8.0", "markdown-it": "^13.0.1", "minisearch": "^6.0.1", "react": "^18.2.0", "react-bootstrap": "^2.7.2", "react-dom": "^18.2.0", "react-icons": "^4.7.1", "react-redux": "^8.0.5", "redux": "^4.2.1", "stream-browserify": "^3.0.0", "utf8": "^3.0.0" }, "devDependencies": { "@babel/core": "^7.20.12", "@babel/eslint-parser": "^7.19.1", "@babel/eslint-plugin": "^7.19.1", "@babel/plugin-proposal-object-rest-spread": "^7.20.7", "@babel/preset-react": "^7.18.6", "@babel/register": "^7.18.9", "@playwright/test": "^1.30.0", "@webpack-cli/generators": "^3.0.1", "babel-plugin-system-import-transformer": "^4.0.0", "babel-plugin-transform-class-properties": "^6.24.1", "babel-plugin-transform-object-rest-spread": "^6.26.0", "blake2s": "^1.1.0", "bootstrap": "^5.2.3", "bs58": "^5.0.0", "buffer": "^6.0.3", "chai": "^4.3.7", "copy-webpack-plugin": "^4.6.0", "css-loader": "^6.7.3", "eslint": "^8.34.0", "eslint-plugin-react": "^7.32.2", "file-loader": "^6.2.0", "html-webpack-plugin": "^5.5.0", "jsdom": "^10.1.0", "mini-css-extract-plugin": "^2.7.2", "mocha": "^10.2.0", "node-env-file": "^0.1.8", "node-sass": "^8.0.0", "playwright": "^1.30.0", "prettier": "^2.8.4", "redux-observable": "^0.19.0", "rxjs": "^5.5.12", "sass": "^1.58.0", "sass-loader": "^13.2.0", "style-loader": "^3.3.1", "webpack": "^5.75.0", "webpack-cli": "^5.0.1", "webpack-dev-server": "^4.11.1" } } ================================================ FILE: pg_types.go ================================================ // Steve Phillips / elimisteve // 2017.05.18 package main import ( "bytes" "encoding/base64" "encoding/json" "fmt" "io/ioutil" "net/http" "time" log "github.com/sirupsen/logrus" ) type PGClient struct { BaseURL string } func NewPGClient(baseURL string) *PGClient { return &PGClient{ BaseURL: baseURL, } } func (cl *PGClient) Post(urlSuffix string, payload interface{}) (*http.Response, error) { var payloadb []byte if payload != nil { b, err := json.Marshal(payload) if err != nil { return nil, err } payloadb = b } log.Debugf("POST'ing to %s: %s", urlSuffix, payloadb) r := bytes.NewReader(payloadb) req, _ := http.NewRequest("POST", cl.BaseURL+urlSuffix, r) // req.Header.Add("Prefer", "return=representation") req.Header.Add("Prefer", "return=none") req.Header.Add("Content-Type", "application/json") return http.DefaultClient.Do(req) } func (cl *PGClient) PostWanted(urlSuffix string, payload interface{}, statusWanted int) error { resp, err := cl.Post(urlSuffix, payload) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != statusWanted { body, _ := ioutil.ReadAll(resp.Body) respjson := map[string]interface{}{} err = json.Unmarshal(body, &respjson) return fmt.Errorf( "Got HTTP %v from PostgREST, wanted %v. Resp: %#v (err unmarshal: %v)", resp.StatusCode, statusWanted, respjson, err) } return nil } func (cl *PGClient) Get(urlSuffix string) (*http.Response, error) { log.Debugf("GET'ing from %s", urlSuffix) req, _ := http.NewRequest("GET", cl.BaseURL+urlSuffix, nil) // req.Header.Add("Prefer", "return=representation") req.Header.Add("Prefer", "return=none") req.Header.Add("Content-Type", "application/json") return http.DefaultClient.Do(req) } func (cl *PGClient) Delete(urlSuffix string) (*http.Response, error) { log.Debugf("DELETE'ing from %s", urlSuffix) req, _ := http.NewRequest("DELETE", cl.BaseURL+urlSuffix, nil) // req.Header.Add("Prefer", "return=representation") req.Header.Add("Prefer", "return=none") // req.Header.Add("Content-Type", "application/json") return http.DefaultClient.Do(req) } func (cl *PGClient) GetInto(urlSuffix string, respobj interface{}) error { resp, err := cl.Get(urlSuffix) if err != nil { return err } defer resp.Body.Close() statusWanted := http.StatusOK body, _ := ioutil.ReadAll(resp.Body) if resp.StatusCode != statusWanted { respjson := map[string]interface{}{} err = json.Unmarshal(body, &respjson) return fmt.Errorf( "Got HTTP %v from PostgREST, wanted %v. Resp: %#v (err unmarshal: %v)", resp.StatusCode, statusWanted, respjson, err) } return json.Unmarshal(body, respobj) } type PGRoom struct { RoomID string `json:"room_id"` } func (room PGRoom) Create(cl *PGClient) error { return cl.PostWanted("/rooms", room, http.StatusCreated) } type PGMessage struct { MessageID string `json:"message_id,omitempty"` RoomID string `json:"room_id"` Message string `json:"message,omitempty"` MessageEnc string `json:"message_enc"` TTL *int `json:"ttl_secs,omitempty"` Created *time.Time `json:"created,omitempty"` } type pgPostMessage PGMessage func (msg *PGMessage) MarshalJSON() ([]byte, error) { m := pgPostMessage(*msg) // Store binary data in Postgres base64-encoded m.MessageEnc = base64.StdEncoding.EncodeToString([]byte(m.MessageEnc)) return json.Marshal(m) } type PGMessages []*PGMessage func (msgs PGMessages) Create(pgClient *PGClient) error { // TODO: Parse resp.Body as []*PGMessage, return to user return pgClient.PostWanted("/messages", msgs, http.StatusCreated) } ================================================ FILE: playwright.config.js ================================================ const { defineConfig, devices } = require('@playwright/test'); module.exports = defineConfig({ testDir: "./test/playwright", expect: { timout: 5000 }, fullyParallel: true, reporter: 'html', use: { // does not work, wtf? // baseUrl: "http://localhost:8080/", timeout: 5 * 1000, headless: true // change to 'false' if you want to see a browser open }, projects: [ { name: 'webkit', use: { ...devices['Desktop Safari'] }, }, { name: 'firefox', use: { ...devices['Desktop Firefox'] }, } //, // { // name: 'chromium', // use: { ...devices['Desktop Chrome'] }, // }, ] }); ================================================ FILE: room.go ================================================ package main import ( "encoding/base64" "encoding/hex" "encoding/json" "fmt" "io/ioutil" "sync" "time" "github.com/gorilla/websocket" log "github.com/sirupsen/logrus" ) const ( POSTGREST_BASE_URL = "http://localhost:3000" ) var ( DELETE_EXPIRED_MESSAGES_PERIOD = 10 * time.Minute pgClient = NewPGClient(POSTGREST_BASE_URL) AllRooms = NewRoomManager(pgClient) ) type RoomManager struct { lock sync.RWMutex rooms map[string]*Room // map[miniLockID]*Room pgClient *PGClient } func NewRoomManager(pgClient *PGClient) *RoomManager { go func() { tick := time.Tick(DELETE_EXPIRED_MESSAGES_PERIOD) var err error for { err = pgClient.PostWanted("/rpc/delete_expired_messages", nil, 204) if err != nil { log.Infof("Error deleting expired messages: %v", err) } else { log.Debugf("Just deleted expired messages") } <-tick } }() return &RoomManager{ pgClient: pgClient, rooms: map[string]*Room{}, } } func (rm *RoomManager) GetRoom(roomID string) *Room { rm.lock.Lock() defer rm.lock.Unlock() room, ok := rm.rooms[roomID] if !ok { room = NewRoom(roomID, rm.pgClient) rm.rooms[roomID] = room } return room } type Room struct { ID string // miniLock ID Clients []*Client clientLock sync.RWMutex pgClient *PGClient } func NewRoom(roomID string, pgClient *PGClient) *Room { // TODO: Error handling _ = PGRoom{RoomID: roomID}.Create(pgClient) return &Room{ ID: roomID, pgClient: pgClient, } } func (r *Room) GetMessages() ([]Message, error) { var pgMessages PGMessages err := r.pgClient.GetInto( "/messages?select=message_enc&order=created.desc&limit=100&room_id=eq."+r.ID, &pgMessages) if err != nil { return nil, err } msgs := make([]Message, len(pgMessages)) var bindata []byte // Grab just the message_enc field, reverse the order, and turn // PostgREST's hex string response back into base64 for i := 0; i < len(pgMessages); i++ { bindata, err = byteaToBytes(pgMessages[i].MessageEnc) if err != nil { log.Debugf("Error from byteaToBytes: %s", err) continue } msgs[len(pgMessages)-i-1] = bindata } return msgs, nil } func (r *Room) AddMessages(msgs []Message, ttlSecs *int) error { post := make(PGMessages, len(msgs)) for i := 0; i < len(msgs); i++ { post[i] = &PGMessage{ RoomID: r.ID, MessageEnc: string(msgs[i]), TTL: ttlSecs, } } return post.Create(r.pgClient) } func byteaToBytes(hexdata string) ([]byte, error) { if len(hexdata) <= 2 { return []byte{}, nil } // Postgres prefixes the stored strings with "\\x" hexdatab := []byte(hexdata[2:]) b64b := make([]byte, hex.DecodedLen(len(hexdatab))) n, err := hex.Decode(b64b, hexdatab) if err != nil { return nil, err } bindata := make([]byte, base64.StdEncoding.DecodedLen(len(b64b))) n, err = base64.StdEncoding.Decode(bindata, b64b) if err != nil { return nil, err } return bindata[:n], nil } func (r *Room) AddClient(c *Client) { r.clientLock.Lock() defer r.clientLock.Unlock() r.Clients = append(r.Clients, c) } func (r *Room) RemoveClient(c *Client) { r.clientLock.Lock() defer r.clientLock.Unlock() for i, client := range r.Clients { if client == c { r.Clients = append(r.Clients[:i], r.Clients[i+1:]...) if client.wsConn != nil { client.wsConn.Close() } break } } } // If it is a message from the room, make the sender nil. func (r *Room) BroadcastMessages(sender *Client, msgs ...Message) { r.clientLock.RLock() defer r.clientLock.RUnlock() for _, client := range r.Clients { go func(client *Client) { err := client.SendMessages(msgs...) if err != nil { log.Debugf("Error sending message. Err: %s", err) } }(client) } } func (r *Room) DeleteAllMessages() error { resp, err := r.pgClient.Delete("/messages?room_id=eq." + r.ID) if err != nil { return err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return err } if len(body) != 0 { return fmt.Errorf("Error deleting messages: `%s`", body) } return nil } func (r *Room) BroadcastDeleteSignal() { r.clientLock.RLock() defer r.clientLock.RUnlock() for _, client := range r.Clients { go func(client *Client) { err := client.SendDeleteSignal() if err != nil { log.Debugf("Error sending message. Err: %s", err) } }(client) } } type Client struct { wsConn *websocket.Conn writeLock sync.Mutex room *Room } func (c *Client) SendMessages(msgs ...Message) error { c.writeLock.Lock() defer c.writeLock.Unlock() outgoing := OutgoingPayload{Ephemeral: msgs} body, err := json.Marshal(outgoing) if err != nil { return err } err = c.wsConn.WriteMessage(websocket.TextMessage, body) if err != nil { log.Debugf("Error sending message to client. Removing client from room. Err: %s", err) c.room.RemoveClient(c) return err } return nil } func (c *Client) SendDeleteSignal() error { c.writeLock.Lock() defer c.writeLock.Unlock() outgoing := OutgoingPayload{ Ephemeral: []Message{}, FromServer: FromServer{ AllMessagesDeleted: true, }, } body, err := json.Marshal(outgoing) if err != nil { return err } err = c.wsConn.WriteMessage(websocket.TextMessage, body) if err != nil { log.Debugf("Error sending message to client. Removing client from room. Err: %s", err) c.room.RemoveClient(c) return err } return nil } func (c *Client) SendError(errStr string, secretErr error) error { c.writeLock.Lock() defer c.writeLock.Unlock() return WSWriteError(c.wsConn, errStr, secretErr) } ================================================ FILE: room_test.go ================================================ package main import ( "fmt" "sync" "testing" "github.com/stretchr/testify/assert" ) func TestRoomManager(t *testing.T) { rooms := NewRoomManager(pgClient) roomIDs := []string{} wg := &sync.WaitGroup{} for i := 0; i < 10; i++ { roomID := fmt.Sprintf("room-%d", i) roomIDs = append(roomIDs, roomID) wg.Add(1) go func(roomID string) { rooms.GetRoom(roomID) wg.Done() }(roomID) } wg.Wait() for _, roomID := range roomIDs { assert.NotNil(t, rooms.rooms[roomID], fmt.Sprintf("RoomID: %v\nRooms: %v\n", roomID, rooms)) } } func TestRoomMessages(t *testing.T) { r := NewRoom("testing-room-testing-room-testing-room-testing", pgClient) wg := &sync.WaitGroup{} msgs := [][]Message{} expectedMsgs := []Message{} for i := 0; i < 10; i++ { msgs = append(msgs, []Message{}) for n := 0; n < 10; n++ { msgs[i] = append(msgs[i], Message{'a'}) expectedMsgs = append(expectedMsgs, Message{'a'}) } } wg.Add(len(msgs) * 2) ttlSecs := 60 for i := 0; i < 10; i++ { go func(i int) { err := r.AddMessages(msgs[i], &ttlSecs) if err != nil { t.Logf("Error from AddMessages: %s", err) // FALLTHROUGH } wg.Done() }(i) go func() { _, err := r.GetMessages() if err != nil { t.Logf("Error from GetMessages: %s", err) // FALLTHROUGH } wg.Done() }() } wg.Wait() gotMsgs, _ := r.GetMessages() assert.Equal(t, expectedMsgs, gotMsgs) } func TestRoomClients(t *testing.T) { r := NewRoom("testing-room-testing-room-testing-room-testing", pgClient) clients := []*Client{} wg := &sync.WaitGroup{} for i := 0; i < 10; i++ { client := &Client{} clients = append(clients, client) wg.Add(1) go func(c *Client) { r.AddClient(c) wg.Done() }(client) } wg.Wait() assert.Equal(t, clients, r.Clients) for _, client := range clients { wg.Add(1) go func(c *Client) { r.RemoveClient(c) wg.Done() }(client) } wg.Wait() assert.Empty(t, r.Clients) } // TODO func TestRoomBroadcastMessages(t *testing.T) {} ================================================ FILE: server.go ================================================ package main import ( "fmt" "net/http" "strings" "time" "github.com/cryptag/gosecure/canary" "github.com/cryptag/gosecure/content" "github.com/cryptag/gosecure/csp" "github.com/cryptag/gosecure/frame" "github.com/cryptag/gosecure/hsts" "github.com/cryptag/gosecure/referrer" "github.com/cryptag/gosecure/xss" "github.com/cryptag/leapchat/miniware" minilock "github.com/cryptag/go-minilock" "github.com/cryptag/go-minilock/taber" "github.com/gorilla/mux" "github.com/justinas/alice" uuid "github.com/nu7hatch/gouuid" log "github.com/sirupsen/logrus" "golang.org/x/crypto/acme/autocert" ) const ( MINILOCK_ID_KEY = "minilock_id" ) func NewRouter(m *miniware.Mapper) *mux.Router { r := mux.NewRouter() // pgClient defined in room.go r.HandleFunc("/api/login", Login(m, pgClient)).Methods("GET") msgsHandler := miniware.Auth( http.HandlerFunc(WSMessagesHandler(AllRooms)), m, ) r.HandleFunc("/api/ws/messages/all", msgsHandler).Methods("GET") r.PathPrefix("/").Handler(gzipHandler(http.FileServer(http.Dir("./" + BUILD_DIR)))).Methods("GET") http.Handle("/", r) return r } func NewServer(m *miniware.Mapper, httpAddr string) *http.Server { r := NewRouter(m) return &http.Server{ Addr: httpAddr, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 120 * time.Second, Handler: r, } } func ProductionServer(srv *http.Server, httpsAddr, domain string, manager *autocert.Manager, iframeOrigin string) { gotWarrant := false middleware := alice.New(canary.GetHandler(&gotWarrant), csp.GetCustomHandlerStyleUnsafeInline(domain, domain), hsts.PreloadHandler, frame.GetHandler(iframeOrigin), content.GetHandler, xss.GetHandler, referrer.NoHandler) srv.Handler = middleware.Then(manager.HTTPHandler(srv.Handler)) srv.Addr = httpsAddr srv.TLSConfig = manager.TLSConfig() } func Login(m *miniware.Mapper, pgClient *PGClient) func(w http.ResponseWriter, req *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { mID, keypair, err := parseMinilockID(req) if err != nil { WriteErrorStatus(w, "Error: invalid miniLock ID", err, http.StatusBadRequest) return } err = PGRoom{RoomID: mID}.Create(pgClient) if err != nil && !strings.Contains(err.Error(), "duplicate key value violates unique constraint") { WriteErrorStatus(w, "Error creating new room", err, http.StatusInternalServerError) return } log.Infof("Login: `%s` is trying to log in\n", mID) newUUID, err := uuid.NewV4() if err != nil { WriteError(w, "Error generating new auth token; sorry!", err) return } authToken := newUUID.String() err = m.SetMinilockID(authToken, mID) if err != nil { WriteError(w, "Error saving new auth token; sorry!", err) return } filename := "type:authtoken" contents := []byte(authToken) sender := randomServerKey recipient := keypair encAuthToken, err := minilock.EncryptFileContents(filename, contents, sender, recipient) if err != nil { WriteError(w, "Error encrypting auth token to you; sorry!", err) return } w.Write(encAuthToken) }) } func parseMinilockID(req *http.Request) (string, *taber.Keys, error) { mID := req.Header.Get("X-Minilock-Id") // Validate miniLock ID by trying to generate public key from it keypair, err := taber.FromID(mID) if err != nil { return "", nil, fmt.Errorf("Error validating miniLock ID: %v", err) } return mID, keypair, nil } func redirectToHTTPS(httpAddr, httpsPort, domain string, manager *autocert.Manager) { srv := &http.Server{ Addr: httpAddr, ReadTimeout: 5 * time.Second, WriteTimeout: 5 * time.Second, IdleTimeout: 5 * time.Second, Handler: manager.HTTPHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { w.Header().Set("Connection", "close") url := "https://" + domain + ":" + httpsPort + req.URL.String() if httpsPort == "443" { url = "https://" + domain + req.URL.String() } http.Redirect(w, req, url, http.StatusFound) })), } log.Infof("Listening on %v\n", httpAddr) log.Fatal(srv.ListenAndServe()) } func getAutocertManager(domain string) *autocert.Manager { domains := []string{domain} // Support both website.com and www.website.com if strings.HasPrefix(domain, "www.") { domains = append(domains, domain[len("www."):]) } else { domains = append(domains, "www." + domain) } return &autocert.Manager{ Prompt: autocert.AcceptTOS, HostPolicy: autocert.HostWhitelist(domains...), Cache: autocert.DirCache("./" + domain), } } ================================================ FILE: server_test.go ================================================ package main import ( "fmt" "net/http" "net/http/httptest" "testing" "github.com/stretchr/testify/assert" ) func TestParseMinilockID(t *testing.T) { // request without header returns Error req := httptest.NewRequest(http.MethodGet, "/", nil) mID, keypair, err := parseMinilockID(req) assert.Equal(t, "", mID, fmt.Sprintf("mID was not an empty string: %v\n", mID)) assert.Nil(t, keypair, fmt.Sprintf("keypair was unexpectedly not nil: %v\n", keypair)) assert.NotNil(t, err, fmt.Sprintf("err was unexpectedly nil: %v\n", err)) // request with invalid header value returns Error invalidMiniLockId := "2aSQkrU5hp" req.Header.Set("X-Minilock-Id", invalidMiniLockId) mID, keypair, err = parseMinilockID(req) assert.Equal(t, "", mID, fmt.Sprintf("mID was not an empty string: %v\n", mID)) assert.Nil(t, keypair, fmt.Sprintf("keypair was unexpectedly not nil: %v\n", keypair)) assert.NotNil(t, err, fmt.Sprintf("err was unexpectedly nil: %v\n", err)) // request with valid header value returns value, nil Error validMiniLockId := "s9dDgRKVWvnkpifRXiSgFGj9QLgq1BZ3qvzCsnxPFDrQG" req.Header.Set("X-Minilock-Id", validMiniLockId) mID, keypair, err = parseMinilockID(req) assert.Equal(t, validMiniLockId, mID, fmt.Sprintf("mID was unexpected an empty string: %v\n", mID)) assert.NotNil(t, keypair, fmt.Sprintf("keypair was unexpectedly nil: %v\n", keypair)) assert.Nil(t, err, fmt.Sprintf("err was unexpectedly not nil: %v\n", err)) } ================================================ FILE: src/components/App.js ================================================ import React, { Component } from 'react'; import { connect } from 'react-redux'; import { setUsername, initConnection, initChat } from '../store/actions/chatActions'; import Header from './layout/Header'; import ChatContainer from './chat/ChatContainer'; import PincodeModal from './modals/PincodeModal'; import UsernameModal from './modals/Username'; // evaluate on initial render only, not on every re-render. const isNewRoom = Boolean(!document.location.hash); class App extends Component { constructor(props) { super(props); this.state = { modals: { username: { isVisible: false }, pincode: { isVisible: false } } }; } componentDidMount() { this.props.initChat(); this.connectIfNeeded(); } componentDidUpdate(prevProps, prevState) { this.connectIfNeeded(); } connectIfNeeded() { if (!this.props.pincodeRequired && this.props.shouldConnect){ this.onInitConnection(); } } onSetPincode = (pincode = "") => { if (!pincode || pincode.endsWith("--")) { this.onError('Invalid pincode!'); return; } this.onInitConnection(pincode); }; createDeviceSession(passphrase) { document.location.hash = '#' + passphrase; } onInitConnection(pincode='') { const urlHash = document.location.hash + pincode; this.props.initConnection(this.createDeviceSession, urlHash); } onToggleModalVisibility = (modalName, isVisible) => { let modalsState = {...this.state.modals}; modalsState[modalName].isVisible = isVisible; this.setState({ modals: modalsState }); }; onClosePincodeModal = () => { this.setState({ showPincodeModal: false }); }; render() { const { username, pincodeRequired, previousUsername, authenticating, connecting, connected, } = this.props; let showUsernameModal = this.state.modals.username.isVisible; showUsernameModal = !pincodeRequired && (showUsernameModal || username === ''); const chatInputFocus = !pincodeRequired && !showUsernameModal && username !== ''; return (
{pincodeRequired && } {showUsernameModal && }
); } } App.propTypes = {}; const mapStateToProps = (reduxState) => { return { username: reduxState.chat.username, previousUsername: reduxState.chat.previousUsername, pincodeRequired: reduxState.chat.pincodeRequired, shouldConnect: reduxState.chat.shouldConnect, connecting: reduxState.chat.connecting, connected: reduxState.chat.connected, }; }; const mapDispatchToProps = (dispatch) => { return { initChat: () => dispatch(initChat()), initConnection: (createDeviceSession, urlHash) => dispatch(initConnection(createDeviceSession, urlHash)), setUsername: (username) => dispatch(setUsername(username)), }; }; export default connect(mapStateToProps, mapDispatchToProps)(App); ================================================ FILE: src/components/chat/AutoSuggest.js ================================================ import React from 'react'; import { connect } from 'react-redux'; import emoji from '../../utils/emoji_convertor'; import md from '../../utils/link_attr_blank'; import MentionSuggestions from './MentionSuggestions'; import EmojiSuggestions from './EmojiSuggestions'; const AutoSuggest = ({ chat }) => { const isMentions = chat.suggestionWord[0] === '@'; return (
{ isMentions ? 'User' :'Emoji' } matching "{chat.suggestionWord}" to navigate to select
{ isMentions ? : }
); }; export default connect(({ chat }) => ({chat}))(AutoSuggest); ================================================ FILE: src/components/chat/ChatContainer.js ================================================ import React, { Component } from 'react'; import { PropTypes } from 'prop-types'; import { connect } from 'react-redux'; import MessageBox from './MessageBox'; import MessageForm from './MessageForm'; import AutoSuggest from './AutoSuggest'; import AlertContainer from '../general/AlertContainer'; const ChatContainer = ({ suggestions, messageInputFocus, onToggleModalVisibility }) => { return (
{suggestions.length > 0 && }
); }; ChatContainer.propTypes = { messageInputFocus: PropTypes.bool.isRequired, onToggleModalVisibility: PropTypes.func.isRequired, }; const mapStateToProps = (reduxState) => { return { suggestions: reduxState.chat.suggestions, }; }; export default connect(mapStateToProps)(ChatContainer); ================================================ FILE: src/components/chat/ChatRoom.js ================================================ import React, { Component } from 'react'; class ChatRoom extends Component { onSelectRoom(){ let roomKey = this.props.chatRoom.key; this.props.onSelectRoom(roomKey); } render(){ let chatRoom = this.props.chatRoom; return (
  • {chatRoom.roomname}
  • ); } } // class ChatRoom extends Component { // render(){ // let username = this.props.username; // let room = this.props.room; // // return ( //
    // {(room.messages || []).map(message => { // let fromMe = (message.from === username); // return ( //
    // {message.from}: {message.msg} //
    // ) // })} //
    // ) // } // } export default ChatRoom; ================================================ FILE: src/components/chat/EmojiSuggestions.js ================================================ import React from 'react'; import { connect } from 'react-redux'; import { addSuggestion } from '../../store/actions/chatActions'; import emoji from '../../utils/emoji_convertor'; import md from '../../utils/link_attr_blank'; import { scrollIntoViewOptions } from '../../utils/suggestions'; const EmojiSuggestions = ({ addSuggestion, chat }) => (
      {chat.suggestions.map((emoj, i) => { const suggestion = emoji.replace_colons(emoj.name) + emoj.name; const suggestionMD = suggestion.replace(/<\/span>/g, '![emoji]($1)'); const renderItem = md.renderInline(suggestionMD); const activeItem = chat.highlightedSuggestion === i; let props = { key : i, onClick : (e) => addSuggestion(emoj.name), className : activeItem ? 'active': '', }; if (activeItem) { props.ref = (item) => { if (item) item.scrollIntoView(scrollIntoViewOptions); }; } return (
    • ); })}
    ); export default connect(({ chat }) => ({chat}), { addSuggestion })(EmojiSuggestions); ================================================ FILE: src/components/chat/MentionSuggestions.js ================================================ import React from 'react'; import { connect } from 'react-redux'; import { addSuggestion } from '../../store/actions/chatActions'; import { scrollIntoViewOptions } from '../../utils/suggestions'; import { UserStatusIconBubble } from './UserStatusIcons'; const MentionSuggestions = ({ chat, addSuggestion }) => (
      {chat.suggestions.map((user, i) => { const activeItem = chat.highlightedSuggestion === i; const mention = user.name; let props = { key : i, onClick : (e) => addSuggestion(mention), className : activeItem ? 'active': '', }; if (activeItem) { props.ref = (item) => { if (item) item.scrollIntoView(scrollIntoViewOptions); }; } return
    • {mention.slice(1)}
    • ; })}
    ); export default connect(({ chat }) => ({chat}), { addSuggestion })(MentionSuggestions); ================================================ FILE: src/components/chat/Message.js ================================================ import React, { Component } from 'react'; import emoji from '../../utils/emoji_convertor'; import md from '../../utils/link_attr_blank'; class Message extends Component { render() { let { message, username } = this.props; let fromMe = message.from === username; let messageClass = fromMe ? 'chat-outgoing' : 'chat-incoming'; let emojified = emoji.replace_colons(message.msg); // Convert `emoji.replace_colons`-generated tags to Markdown let emojiMD = emojified.replace( /<\/span>/g, (match, $1, $2, $3) => { // Example: // // $1 == /static/img/emoji/apple/64/ // $2 == 1f604 // $3 == .png // emoji.data[$2][3][0] == smile // return '![:smile:](/static/img/emoji/apple/64/1f604.png)' let emojiName = 'emoji'; let emojiNameArray = null; // Sometimes $2 looks something like 1f604-1f604-1f604-1f604 const parts = ($2).split('-'); const partsLength = parts.length; for (let i = partsLength; i > 0; i--) { emojiNameArray = emoji.data[parts.slice(0, i).join('-')]; if (emojiNameArray) { break; } } if (emojiNameArray && emojiNameArray.length >= 4 && emojiNameArray[3].length >= 1) { emojiName = emojiNameArray[3][0]; } return '![:' + emojiName + ':](' + $1 + $2 + $3 + ')'; } ); // Render escaped HTML/Markdown let linked = md.render(emojiMD); return (
  • {message.from}
  • ); } } export default Message; ================================================ FILE: src/components/chat/MessageBox.js ================================================ import React, { Component } from 'react'; import MessageList from './MessageList'; import { connect } from 'react-redux'; import { closePicker } from '../../store/actions/chatActions'; import { playNotification } from '../../utils/audio'; class MessageBox extends Component { constructor(props){ super(props); this.messagesEnd = null; this.state = { notifiedIds: [] }; } onNewMessages = () => { if (this.props.isAudioEnabled){ playNotification(); } this.scrollToBottom(); }; scrollToBottom = () => { this.messagesEnd && this.messagesEnd.scrollIntoView(); }; // Separate function to set reference because of https://reactjs.org/docs/refs-and-the-dom.html#caveats setMessagesEndRef = (element) => { this.messagesEnd = element; }; render(){ let { messages, username, closePicker } = this.props; return (
    this.onNewMessages()} messages={messages} username={username} />
    ); } } const mapStateToProps = (reduxState) => { return { messages: reduxState.chat.messages, username: reduxState.chat.username, isAudioEnabled: reduxState.settings.isAudioEnabled, }; }; export default connect(mapStateToProps, { closePicker })(MessageBox); ================================================ FILE: src/components/chat/MessageForm.js ================================================ import React, { Component } from 'react'; import { PropTypes } from 'prop-types'; import { connect } from 'react-redux'; import { Button } from 'react-bootstrap'; import { FaArrowAltCircleRight } from 'react-icons/fa'; import { FaSmile } from 'react-icons/fa'; import { Picker } from 'emoji-mart'; import emoji from '../../constants/emoji'; import { emojiSuggestions, mentionSuggestions } from '../../utils/suggestions'; import { messageUpdate, clearMessage, togglePicker, addEmoji, closePicker, startSuggestions, stopSuggestions, downSuggestion, upSuggestion, addSuggestion, sendMessage, } from '../../store/actions/chatActions'; import ToggleAudioIcon from './toolbar/ToggleAudioIcon'; import InviteIcon from './toolbar/InviteIcon'; import OpenSearchIcon from './toolbar/OpenSearchIcon'; class MessageForm extends Component { constructor(props) { super(props); this.messageInput = React.createRef(); } componentDidMount() { this.resolveFocus(); } componentDidUpdate() { this.resolveFocus(); } resolveFocus() { if (this.props.shouldHaveFocus) { this.messageInput.current.focus(); } } onKeyPress = (e) => { const cursorIndex = this.messageInput.current.selectionStart; const { suggestionStart, suggestions, highlightedSuggestion, statuses} = this.props; // Send on unless has been pressed if (e.key === 'Enter' && !e.nativeEvent.shiftKey) { if (suggestions.length > 0) { const selected = suggestions[highlightedSuggestion]; e.preventDefault(); return this.props.addSuggestion(selected.name); } this.onSendMessage(e); this.props.closePicker(); } if (e.key === ':' && suggestionStart === null) { this.props.startSuggestions(cursorIndex, emojiSuggestions); } if (e.key === '@' && suggestionStart === null) { this.props.startSuggestions(cursorIndex, mentionSuggestions, statuses); } if(e.nativeEvent.code === 'Space' && suggestionStart !== null) { this.props.stopSuggestions(); } }; onKeyDown = (e) => { const { message, suggestionWord, statuses } = this.props; const cursorIndex = this.messageInput.current.selectionStart; const before = message.slice(0, cursorIndex - 1); const word = suggestionWord; const filterSuggestions = word[0] === '@' ? mentionSuggestions : emojiSuggestions; if (e.key === 'Backspace' && before.endsWith(word) && word) { const start = before.length - word.length; this.props.startSuggestions(start, filterSuggestions, statuses); } }; isPayloadValid(message) { if (message && message.length > 0) { return true; } return false; } onSendMessage = (e) => { e.preventDefault(); const { message, username } = this.props; if (!this.isPayloadValid(message)) { return false; } this.props.sendMessage({ message, username }); this.props.clearMessage(); }; backgroundImageFn = (set, sheetSize) => { if (set !== 'apple' || sheetSize !== 64) { console.log('WARNING: using set "apple" and sheetSize 64 rather than', set, 'and', sheetSize, 'as was requested'); } return '/' + emoji.EMOJI_APPLE_64_SHEET; }; addEmoji = (emoji) => { const cursorIndex = this.messageInput.current.selectionStart; this.props.addEmoji(emoji.colons, cursorIndex); }; handleKeyDown = (e) => { const { suggestions, suggestionStart } = this.props; const cursorIndex = this.messageInput.current.selectionStart; if (e.key === 'Backspace' && cursorIndex - suggestionStart === 1) { this.props.stopSuggestions(); } if (e.key === 'ArrowUp' && suggestions.length > 0) { e.preventDefault(); this.props.upSuggestion(); } if (e.key === 'ArrowDown' && suggestions.length > 0) { e.preventDefault(); this.props.downSuggestion(); } }; onMessageUpdate = (e) => { const message = e.target.value; this.props.messageUpdate(message); }; render() { const { message, showEmojiPicker, username, messages, } = this.props; const { togglePicker, isAudioEnabled, onSetIsAudioEnabled } = this.props; return (
    {showEmojiPicker && }
    ); } } MessageForm.propType = { shouldHaveFocus: PropTypes.bool.isRequired, onToggleModalVisibility: PropTypes.func.isRequired, }; const mapStateToProps = (reduxState) => { return { isAudioEnabled: reduxState.settings.isAudioEnabled, ...reduxState.chat, }; }; export default connect(mapStateToProps, { messageUpdate, clearMessage, togglePicker, addEmoji, closePicker, startSuggestions, stopSuggestions, downSuggestion, upSuggestion, addSuggestion, sendMessage, })(MessageForm); ================================================ FILE: src/components/chat/MessageList.js ================================================ import React, { Component } from 'react'; import { findDOMNode } from 'react-dom'; import Message from './Message'; class MessageList extends Component { shouldComponentUpdate(nextProps, nextState) { return nextProps.messages.length > 0 && nextProps.messages .map(m => m.id) .concat(this.props.messages.map(m => m.id)) .reduce((acc, id) => { if(!acc.indexOf(id) === -1){ acc.push(id); } return acc; }, []) .length !== this.props.messages; } componentDidUpdate(){ this.props.onNewMessages(); } render() { const { messages, username } = this.props; return (
      {messages.map((message, i) => { return ( ); })}
    ); } } export default MessageList; ================================================ FILE: src/components/chat/UserIcon.js ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { FaUsers } from 'react-icons/fa'; const UserIcon = ({ onToggleUserList }) => { return (
    ); }; UserIcon.propTypes = { onToggleUserList: PropTypes.func.isRequired }; export default UserIcon; ================================================ FILE: src/components/chat/UserList.js ================================================ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { connect } from "react-redux"; import { Button } from 'react-bootstrap'; import { FaShareAlt } from 'react-icons/fa'; import { UserStatusIcon } from './UserStatusIcons'; import SharingModal from '../modals/SharingModal'; const UserList = ({ username, statuses, displayUserList, onToggleModalVisibility, }) => { const [showSharingModal, setShowSharingModal] = useState(false); const currentUsername = username; const userStatuses = Object.keys(statuses).map(username => { const status = statuses[username]; return { status, username }; }); const styleUserList = () => { return { display: displayUserList ? "block" : "none" }; }; return (
      {userStatuses.map((userStatus, i) => { return (
    • ); })}
    {showSharingModal && setShowSharingModal(false)} />}
    ); }; UserList.propTypes = { username: PropTypes.string.isRequired, displayUserList: PropTypes.bool.isRequired, onToggleModalVisibility: PropTypes.func.isRequired }; const mapStateToProps = (reduxState) => ({ statuses: reduxState.chat.statuses }); export default connect(mapStateToProps)(UserList); ================================================ FILE: src/components/chat/UserStatusIcons.js ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { Tooltip, OverlayTrigger } from 'react-bootstrap'; import { FaCircle } from 'react-icons/fa'; import { FaMinusCircle } from 'react-icons/fa'; import { FaEdit } from 'react-icons/fa'; const editUsernameTooltip = ( Edit Username ); export const UserStatusIconBubble = ({ status }) => { let statusIcon = ; if (status === 'viewing') { statusIcon = ; } else if (status === 'online') { statusIcon = ; } return ( <> {statusIcon} ); }; UserStatusIconBubble.propTypes = { status: PropTypes.string.isRequired, }; export const UserStatusIcon = ({ username, status, isCurrentUser, onToggleModalVisibility }) => { const onShowUsernameModal = () => { onToggleModalVisibility('username', true); }; return (
    {username} {isCurrentUser &&  (me) } {isCurrentUser && // // }
    ); }; UserStatusIcon.propTypes = { username: PropTypes.string.isRequired, status: PropTypes.string.isRequired, isCurrentUser: PropTypes.bool.isRequired, onToggleModalVisibility: PropTypes.func.isRequired }; const styleUserStatus = { display: 'flex', flexDirection: 'row', alignItems: 'center' }; const styleDots = { marginTop: '.2em', marginRight: '.2em', marginBottom: '.2em' }; const styleViewing = Object.assign( { color: 'green' }, styleDots ); const styleOnline = Object.assign( { color: 'yellow' }, styleDots ); const styleOffline = Object.assign( { color: 'gray' }, styleDots ); const styleEditUsername = { cursor: 'pointer', marginLeft: 'auto', marginRight: '2px' // For optical vertical alignment with gear icon }; ================================================ FILE: src/components/chat/toolbar/InviteIcon.js ================================================ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Tooltip, OverlayTrigger } from 'react-bootstrap'; import { FaShareAltSquare } from 'react-icons/fa'; import SharingModal from '../../modals/SharingModal'; const shareChatTooltip = ( Invite to Chat ); const InviteIcon = () => { const [showSharingModal, setShowSharingModal] = useState(false); return (
    {/* */} {/* */} setShowSharingModal(true)} /> {showSharingModal && setShowSharingModal(false)} />}
    ); }; InviteIcon.propTypes = {}; export default InviteIcon; ================================================ FILE: src/components/chat/toolbar/OpenSearchIcon.js ================================================ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Tooltip, OverlayTrigger } from 'react-bootstrap'; import { FaSearch } from 'react-icons/fa'; import SearchModal from '../../modals/SearchModal'; const OpenSearchIcon = ({ username, messages }) => { const [showSearchModal, setShowSearchModal] = useState(false); const openTooltip = ( Search Content ); return (
    {/* */} {/* */} setShowSearchModal(true)} /> {showSearchModal && setShowSearchModal(false)} />}
    ); }; OpenSearchIcon.propTypes = { username: PropTypes.string.isRequired, messages: PropTypes.array.isRequired, }; export default OpenSearchIcon; ================================================ FILE: src/components/chat/toolbar/ToggleAudioIcon.js ================================================ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Tooltip, OverlayTrigger } from 'react-bootstrap'; import { FaVolumeUp } from 'react-icons/fa'; import { FaVolumeMute } from 'react-icons/fa'; import { disableAudio, enableAudio } from '../../../store/actions/settingsActions'; const disableAudioTooltip = ( Mute Audio ); const enableAudioTooltip = ( Enable Audio ); const DisableAudioIcon = ({ onSetIsAudioEnabled }) => ( // // disableAudio()} /> ); const EnableAudioIcon = ({ onSetIsAudioEnabled }) => ( // // enableAudio()} /> ); const ToggleAudioIcon = ({ isAudioEnabled, enableAudio, disableAudio, }) => { return (
    {isAudioEnabled && disableAudio()} />} {!isAudioEnabled && enableAudio()} />}
    ); }; ToggleAudioIcon.propTypes = {}; const mapStateToProps = (reduxState) => { return { isAudioEnabled: reduxState.settings.isAudioEnabled, }; }; const mapDispatchToProps = (dispatch) => { return { enableAudio: () => dispatch(enableAudio()), disableAudio: () => dispatch(disableAudio()), }; }; export default connect(mapStateToProps, mapDispatchToProps)(ToggleAudioIcon); ================================================ FILE: src/components/general/AlertContainer.js ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Alert } from 'react-bootstrap'; import { dismissAlert } from '../../store/actions/alertActions'; // https://v4-alpha.getbootstrap.com/components/alerts/#examples const alertStyles = ['success', 'danger', 'warning', 'info']; const AlertContainer = ({ alertMessage, alertStyle, dismissAlert, alertRenderSeconds, }) => { if (!alertStyles.includes(alertStyle)){ alertStyle = 'success'; } if (alertRenderSeconds && alertRenderSeconds > 0) { // auto-dismiss option setTimeout(() => { dismissAlert(); }, alertRenderSeconds * 1000); } return (
    {alertMessage && {alertMessage} }
    ); }; AlertContainer.propTypes = {}; const mapStateToProps = (reduxState) => { return { ...reduxState.alert, }; }; const mapDispatchToProps = (dispatch) => { return { dismissAlert: () => dispatch(dismissAlert()), }; }; export default connect(mapStateToProps, mapDispatchToProps)(AlertContainer); ================================================ FILE: src/components/general/Throbber.js ================================================ import React, { Component } from 'react'; import ReactDOM from 'react-dom'; const throbberDotStyles = { 'margin': '10px', 'height': '20px', 'width': '20px', 'borderRadius': '10px', 'backgroundColor': '#999', 'float': 'left', 'transform': 'scale(0.7)' }; class Throbber extends Component { componentDidMount(){ this.animateLoadingDots(); } // this is messy, just a test animating react elements // by their ref directly. animateLoadingDots(){ let keyframes = [ { 'transform': 'scale(0.7)' }, { 'transform': 'scale(1.0)' }, { 'transform': 'scale(0.7)' }, ]; let properties = { duration: 1000, iterations: Infinity }; let firstDot = ReactDOM.findDOMNode(this.refs.firstDot); firstDot.animate(keyframes, properties); properties['delay'] = 200; let secondDot = ReactDOM.findDOMNode(this.refs.secondDot); secondDot.animate(keyframes, properties); properties['delay'] = 400; let thirdDot = ReactDOM.findDOMNode(this.refs.thirdDot); thirdDot.animate(keyframes, properties); } render(){ return (
    ); } } export default Throbber; ================================================ FILE: src/components/layout/ChatRoom.js ================================================ import React, { Component } from 'react'; class ChatRoom extends Component { render(){ let username = this.props.username; let rooms = this.props.rooms; return (
    {(this.props.room.messages || []).map(message => { let fromMe = (message.from === username); return (
    {message.from}: {message.msg}
    ); })}
    ); } } export default ChatRoom; ================================================ FILE: src/components/layout/Header.js ================================================ import React, { Component, useState } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import UserIcon from '../chat/UserIcon'; import UserList from '../chat/UserList'; import Logo from './Logo'; import Settings from './Settings'; import Info from './Info'; import { closePicker } from '../../store/actions/chatActions'; const Header = ({ username, onToggleModalVisibility }) => { const [showUserList, setShowUserList] = useState(false); const onToggleUserList = () => { setShowUserList((current) => !current); }; return (
    ); }; Header.propTypes = { username: PropTypes.string.isRequired, onToggleModalVisibility: PropTypes.func.isRequired }; export default connect(null, { closePicker })(Header); ================================================ FILE: src/components/layout/Info.js ================================================ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import { Tooltip, OverlayTrigger } from 'react-bootstrap'; import { FaInfoCircle } from 'react-icons/fa'; import InfoModal from '../modals/InfoModal'; const infoTooltip = ( Open LeapChat Info ); const Info = () => { const [showInfoModal, setShowInfoModal] = useState(false); return (
    setShowInfoModal(true)} size={19}/> {showInfoModal && setShowInfoModal(false)} />}
    ); }; Info.propTypes = {}; export default Info; ================================================ FILE: src/components/layout/Logo.js ================================================ import React from 'react'; const Logo = () => { return (
    LeapChat
    ); }; export default Logo; ================================================ FILE: src/components/layout/Settings.js ================================================ import React, { useState } from 'react'; import { PropTypes } from 'prop-types'; import { Tooltip, OverlayTrigger } from 'react-bootstrap'; import { FaCog } from 'react-icons/fa'; import SettingsModal from '../modals/SettingsModal'; const settingsTooltip = ( Open Settings ); const Settings = () => { const [showSettingsModal, setShowSettingsModal] = useState(false); return (
    {/* */} {/* */} setShowSettingsModal(true)} /> {showSettingsModal && setShowSettingsModal(false)} />}
    ); }; Settings.propTypes = {}; export default Settings; ================================================ FILE: src/components/modals/InfoModal.js ================================================ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { Modal } from 'react-bootstrap'; const InfoModal = ({ isVisible, onClose }) => { return (

    Welcome to LeapChat!

    LeapChat: encrypted, ephemeral, in-browser chat.

    Just visit leapchat.org and a new, secure chat room will instantly be created for you. And once you're in, just link people to that page to invite them to join you!

    Why LeapChat?

    You shouldn't have to sacrifice your privacy and personal information just to chat online. Slack, HipChat, and others make you create an account with your email address, their software doesn't encrypt your messages (they can see everything), and the messages last forever unless you manually delete them. In contrast, LeapChat does encrypt your messages (even we can't see them!), doesn't require you to hand over your email address, and messages last for a maximum of 90 days (this will soon be configurable to a shorter duration). Plus, you can host LeapChat on your own server, since it's open source!

    How does it work?

    When you click on a link to a LeapChat room:
    1. Your browser loads the HTML, CSS, and JavaScript from the server (e.g., leapchat.org)
    2. That JavaScript code then grabs the long passphrase at the end of the URL (the "URL hash" -- everything after the `#`), then passes it to miniLock, which then deterministically generates a keypair from that passphrase
    3. That cryptographic keypair is then used by your browser (and every other chat participant) to encrypt and decrypt messages to and from the people you're chatting with
    The server can't even see your username! That's encrypted, too, and is attached to the messages you send.

    Can I type markdown in my messages?

    Yup! To learn about Markdown syntax, like surrounding words with **double asterisks** to make them bold, or with _underscores_ to make them italicized, check out this guide.

    ); }; InfoModal.propTypes = { onClose: PropTypes.func.isRequired }; export default InfoModal; ================================================ FILE: src/components/modals/PincodeModal.js ================================================ import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { Modal, Button } from 'react-bootstrap'; import { genPassphrase } from '../../data/minishare'; class PincodeModal extends PureComponent { componentDidMount() { this.pincodeInput.focus(); } componentDidUpdate(){ if(this.props.showModal){ this.pincodeInput.focus(); } } onPincodeKeyPress = (e) => { if (e.which === 13) { this.onSetPincodeClick(); } }; isPincodeValid(pincode) { if (!pincode || pincode.endsWith("--")) { return false; } return true; } onSetPincodeClick = (e) => { const pincode = this.pincodeInput.value; if (!this.isPincodeValid(pincode)) { alert('Invalid pincode!'); } else { this.props.onSetPincode(pincode); } }; setRandomPincodeInForm = () => { this.pincodeInput.value = genPassphrase(2); }; onClose = () => { this.props.onToggleModalVisibility('pincode', false); }; render() { let { showModal, pincode } = this.props; return (
    Set Pincode
    { this.pincodeInput = input; }} defaultValue={pincode} placeholder="Enter pincode or password" onKeyPress={this.onPincodeKeyPress} autoFocus={true} />
    {pincode && }
    ); } } PincodeModal.propType = { showModal: PropTypes.bool.isRequired, onToggleModalVisibility: PropTypes.func.isRequired, onSetPincode: PropTypes.func.isRequired }; export default PincodeModal; ================================================ FILE: src/components/modals/SearchModal.js ================================================ import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { Modal, Popover, OverlayTrigger } from 'react-bootstrap'; import { FaInfoCircle } from 'react-icons/fa'; import MiniSearch from 'minisearch'; import Message from '../chat/Message'; const searchInfoPopover = ( Your messages are encrypted in transit and in storage on our servers. So how can you search them?

    Simple! The search happens entirely in the browser. So you can rest easy.
    ); class SearchModal extends Component { constructor(props) { super(props); const miniSearch = this.indexAllMessages(props.messages); this.state = { username: this.props.username, isVisible: this.props.isVisible, miniSearch: miniSearch, isLoadingResults: false, searchResults: [] }; } indexAllMessages(messages) { const miniSearch = new MiniSearch({ fields: ['msg', 'from'], // fields to index for full-text search storeFields: ['msg', 'from'] // fields to return with search results }); messages = this.props.messages.map((message, i) => { return {"id": i, ...message}; }); miniSearch.addAll(messages); return miniSearch; }; getSearchResults = (e) => { this.setState({ isLoadingResults: true }); let searchResults = this.state.miniSearch.search(e.target.value); this.setState({ searchResults: searchResults, isLoadingResults: false }); }; render() { const { isVisible, searchResults, isLoadingResults, username } = this.state; searchResults.sort((a, b) => a.score > b.score); return (
    Search{' '} {/* */} {/* */}

    {isLoadingResults &&

    Loading results...

    } {searchResults.length > 0 &&
      {searchResults.map((result, i) => { return ; })}
    }
    ); } } SearchModal.propTypes = { username: PropTypes.string.isRequired, isVisible: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, messages: PropTypes.array.isRequired, }; export default SearchModal; ================================================ FILE: src/components/modals/SettingsModal.js ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { Modal, Button, OverlayTrigger, Tooltip } from 'react-bootstrap'; import { FaExternalLinkAlt } from 'react-icons/fa'; import { chatHandler } from '../../store/epics/chatEpics'; const onDeleteAllMsgs = (e) => { if (window.confirm("Are you sure you want to delete every existing chat message from this chat room? This action cannot be undone.")) { chatHandler.sendDeleteAllMessagesSignalToServer(); } }; const SettingsModal = ({ isVisible, onClose }) => { return (
    Settings

    Feedback

    Do you have feedback or suggestions on how we can improve LeapChat? We're listening!{' '} Share your feedback here.{' '}


    Danger Zone

    By clicking here, you will delete all messages in this chat from the server, for all users, forever.

    ); }; SettingsModal.propTypes = { isVisible: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, }; export default SettingsModal; ================================================ FILE: src/components/modals/SharingModal.js ================================================ import React from 'react'; import PropTypes from 'prop-types'; import { Modal, Button, OverlayTrigger, Tooltip } from 'react-bootstrap'; import { FaShareAlt } from 'react-icons/fa'; import { FaShareAltSquare } from 'react-icons/fa'; const onCopyShareLink = (e) => { navigator.clipboard.writeText(window.location.href); }; const onShareLink = (e) => { navigator.share({ url: window.location.href, title: "LeapChat", text: "Join me on LeapChat" }); }; const copyLinkTooltip = ( Link copied! ); const SharingModal = ({ isVisible, onClose }) => { return (
    Invite to Chat { navigator.share &&

    Share Link

    Invite with a link shared via SMS, Email, etc.


    }

    Copy Link

    Invite with a link copied to your clipboard.

    ); }; SharingModal.propTypes = { isVisible: PropTypes.bool.isRequired, onClose: PropTypes.func.isRequired, }; export default SharingModal; ================================================ FILE: src/components/modals/Username.js ================================================ import React, { useRef, useState } from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Modal, Button, Alert, ProgressBar } from 'react-bootstrap'; import { generateRandomUsername } from '../../data/username'; import { enableAudio } from '../../store/actions/settingsActions'; export const MAX_USERNAME_LENGTH = 45; const UsernameModal = ({ isVisible, isNewRoom, previousUsername, username, onToggleModalVisibility, setUsername, enableAudio, connecting, connected, }) => { const usernameInput = useRef(null); const [failMessage, setFailMessage ] = useState(""); const onClose = () => { onToggleModalVisibility('username', false); }; const setRandomUsernameInForm = () => { usernameInput.current.value = generateRandomUsername(); usernameInput.current.focus(); }; const isUsernameValid = () => { const usernameFromForm = usernameInput.current.value; if (!usernameFromForm || usernameFromForm.length === 0) { setFailMessage("Must not be empty"); return false; } else if (usernameFromForm.length > MAX_USERNAME_LENGTH) { setFailMessage(`Length must not exceed ${MAX_USERNAME_LENGTH}`); return false; } setFailMessage(""); return true; }; const onUsernameKeyUp = (e) => { if (e.which === 13) { onSetUsername(); } }; const setDefaultAudio = () => { // set the audio to the user's previously selected preference; enable by default let isAudioEnabled = JSON.parse(localStorage.getItem('isAudioEnabled') || 'true'); if (isAudioEnabled){ enableAudio(); } }; const onSetUsername = () => { if (isUsernameValid()) { setUsername(usernameInput.current.value); setDefaultAudio(); onClose(); } }; let progress = 0; let statusMessage = (

    Cranking a bunch of gears.

    ); if (connecting) { progress = 50; statusMessage =

    Creating a secure connection with LeapChat servers.

    ; } else if (connected) { progress = 95; statusMessage =

    Connected!

    ; } return (
    Set Username
    {/* username is empty on initial page load, not on subsequent 'edit username' opens */} {!username && isNewRoom && New room created! } {!username && !isNewRoom && Successfully joined room! } {failMessage &&

    Invalid Username: {failMessage}
    }
    {!connected &&
    {statusMessage}
    }
    {username && }
    ); }; UsernameModal.propTypes = { isVisible: PropTypes.bool.isRequired, isNewRoom: PropTypes.bool.isRequired, previousUsername: PropTypes.string.isRequired, username: PropTypes.string.isRequired, onToggleModalVisibility: PropTypes.func.isRequired, setUsername: PropTypes.func.isRequired, connecting: PropTypes.bool.isRequired, connected: PropTypes.bool.isRequired, }; const mapDispatchToProps = (dispatch) => { return { enableAudio: () => dispatch(enableAudio()), }; }; export default connect(null, mapDispatchToProps)(UsernameModal); ================================================ FILE: src/constants/emoji.js ================================================ exports.EMOJI_APPLE_64_PATH = 'static/img/emoji/apple/64/'; exports.EMOJI_APPLE_64_SHEET = 'static/img/emoji/apple/sheets/64.png'; ================================================ FILE: src/constants/messaging.js ================================================ export const SERVER_ERROR_PREFIX = "Error from server: "; export const AUTH_ERROR = "Error authorizing you"; // Must match Go's miniware.AuthError export const ON_CLOSE_RECONNECT_MESSAGE = "Message WebSocket closed. Reconnecting..."; export const ONE_MINUTE = 60 * 1000; export const USER_STATUS_DELAY_MS = 10 * ONE_MINUTE; export const PARANOID_USERNAME = ' '; export const USERNAME_KEY = 'username'; ================================================ FILE: src/data/constants.js ================================================ export const adjectives = ["Abdominal", "Able", "Abnormal", "Abrasive", "Active", "Affected", "Agnostic", "Agreeable", "Alienable", "Alive", "Ambiguous", "Ambitious", "Amendable", "Amiable", "Amiss", "Amniotic", "Amusable", "Angelfish", "Angelic", "Angular", "Antarctic", "Antitrust", "Antiviral", "Arbitrary", "Arguable", "Armful", "Arrive", "Arrogant", "Astonish", "Atonable", "Atrocious", "Attentive", "Attic", "Atypical", "Audacious", "Audible", "Authentic", "Autistic", "Automatic", "Available", "Average", "Aware", "Babble", "Bagful", "Banish", "Bankable", "Bauble", "Blissful", "Bluish", "Boastful", "Bobble", "Bodacious", "Botanical", "Bountiful", "Bronchial", "Bubble", "Bullish", "Cable", "Canal", "Canary", "Capable", "Capillary", "Captive", "Cardinal", "Catatonic", "Catchable", "Catfish", "Cathedral", "Caucasian", "Cautious", "Celestial", "Celtic", "Chewable", "Childish", "Chive", "Circular", "Citable", "Clean", "Clear", "Clerical", "Climatic", "Closable", "Coastal", "Cognitive", "Cohesive", "Comic", "Composite", "Concerned", "Conducive", "Confident", "Congenial", "Conical", "Constable", "Constant", "Copious", "Coral", "Coronary", "Corporal", "Corporate", "Corral", "Corrosive", "Couch", "Countable", "Cranial", "Crawfish", "Crayfish", "Creatable", "Creative", "Credible", "Crouch", "Crucial", "Crushable", "Cryptic", "Cubical", "Culinary", "Culpable", "Cultural", "Cupid", "Curable", "Cursive", "Custodian", "Customary", "Cyclic", "Daily", "Debatable", "Decal", "Deceptive", "Decidable", "Defective", "Deferral", "Delicious", "Delirious", "Deniable", "Denial", "Deprive", "Devious", "Dexterous", "Diabetic", "Diary", "Diffusive", "Diocese", "Disabled", "Dividable", "Divisible", "Divisive", "Doable", "Domestic", "Drainable", "Dramatic", "Drastic", "Dreadful", "Dreary", "Dribble", "Drinkable", "Drippy", "Drivable", "Drop-down", "Durable", "Dutiful", "Earful", "Earthy", "Easeful", "Eatable", "Eccentric", "Economic", "Effective", "Egotistic", "Elastic", "Elderly", "Electable", "Elective", "Eligible", "Elliptic", "Elusive", "Embellish", "Empathic", "Emphatic", "Empirical", "Enable", "Endurable", "Energetic", "Enigmatic", "Enjoyable", "Enviable", "Envious", "Epidemic", "Epidermal", "Epidural", "Epileptic", "Equal", "Equivocal", "Erasable", "Ergonomic", "Erratic", "Escapable", "Essential", "Establish", "Eternal", "Evasive", "Everyday", "Excitable", "Exclusive", "Excusable", "Exemplary", "Expansive", "Expensive", "Expletive", "Exposable", "External", "Fable", "Fabulous", "Facial", "Factual", "False", "Fanatic", "Fantastic", "Favorable", "Federal", "Feeble", "Ferocious", "Festive", "Financial", "Flammable", "Flavorful", "Floral", "Flyable", "Fondue", "Frantic", "Fraternal", "Freezable", "French", "Fresh", "Fretful", "Frightful", "Frivolous", "Generic", "Generous", "Gigantic", "Glacial", "Glamorous", "Gleeful", "Glorious", "Glutinous", "Good", "Gorgeous", "Gothic", "Graceful", "Gracious", "Granular", "Green", "Grievous", "Grumble", "Guidable", "Gullible", "Habitable", "Habitual", "Happy", "Harmful", "Helpful", "Humble", "Humongous", "Humorous", "Hungry", "Hybrid", "Hypnotic", "Icky", "Identical", "Illusive", "Imaginary", "Immovable", "Impaired", "Imperial", "Impish", "Implosive", "Impulse", "Impulsive", "Irregular", "Irritable", "Itinerary", "Jovial", "Joyous", "Judicial", "Judiciary", "Jumble", "Jumbo", "Junior", "Kinetic", "Kissable", "Laborious", "Landlady", "Lantern", "Large", "Last", "Lavish", "Laxative", "Legal", "Legible", "Letdown", "Liable", "Librarian", "Library", "Likely", "Little", "Livable", "Lucrative", "Ludicrous", "Luminous", "Lumpish", "Lustrous", "Luxurious", "Magical", "Magician", "Magnetic", "Majestic", "Mammal", "Mammary", "Mannish", "Manual", "Many", "Marital", "Marsupial", "Marvelous", "Massive", "Maternal", "Molecular", "Monetary", "Monstrous", "Monthly", "Mortician", "Mortuary", "Mountable", "Mournful", "Movable", "Much", "Mumble", "Municipal", "Mural", "Muscular", "Music", "Mutable", "Mutual", "Nappy", "Natural", "Nautical", "Negative", "Nervous", "Neurotic", "Next", "Nibble", "Nimble", "Ninth", "Numeral", "Numeric", "Numerous", "Obituary", "Oblivious", "Obnoxious", "Obsessive", "Obtrusive", "Obvious", "Olive", "Ominous", "Operable", "Operative", "Other", "Ouch", "Ovary", "Overall", "Overdrive", "Overdue", "Palatable", "Palpable", "Parasitic", "Passable", "Passive", "Paternal", "Payable", "Pebble", "Pecan", "Pediatric", "Pelican", "Perceive", "Perennial", "Perpetual", "Plausible", "Playable", "Pliable", "Plural", "Polish", "Poppy", "Porous", "Portable", "Possible", "Postal", "Pouch", "Preamble", "Pregnant", "Preppy", "Previous", "Primary", "Private", "Proactive", "Probable", "Probiotic", "Provable", "Psychic", "Public", "Pulmonary", "Punctual", "Puppy", "Pursuable", "Quarterly", "Quiet", "Quizzical", "Quotable", "Radial", "Radish", "Ramble", "Rare", "Rascal", "Reactive", "Reburial", "Rebuttal", "Recital", "Reckless", "Reclusive", "Rectal", "Referable", "Refinish", "Refurbish", "Refurnish", "Refutable", "Regretful", "Regular", "Relatable", "Related", "Relative", "Reliable", "Relic", "Relish", "Relive", "Reluctant", "Remedial", "Remindful", "Removable", "Renewable", "Rentable", "Rental", "Reptilian", "Repulsive", "Reputable", "Resemble", "Residual", "Residue", "Resistant", "Retail", "Retinal", "Retouch", "Retrial", "Reusable", "Revisable", "Revivable", "Revocable", "Rigid", "Rival", "Robust", "Rockfish", "Rocky", "Rosy", "Roundish", "Routine", "Rubble", "Rumble", "Rural", "Sacred", "Salaried", "Salary", "Salutary", "Sanctuary", "Sandfish", "Sanitary", "Sappy", "Sarcastic", "Satiable", "Satirical", "Satisfied", "Savior", "Scary", "Scenic", "Schematic", "Scrabble", "Scribble", "Second", "Secret", "Sectional", "Secular", "Sedative", "Seismic", "Selective", "Semantic", "Semifinal", "Senior", "Sensitive", "Sensuous", "Septic", "Seventh", "Shakable", "Siamese", "Siberian", "Singular", "Sinuous", "Sizable", "Skeletal", "Skeptic", "Skillful", "Sleek", "Sliceable", "Sloppy", "Spearfish", "Spendable", "Spherical", "Spinal", "Spiral", "Spiritual", "Spotty", "Squabble", "Squeamish", "Stable", "Stainable", "Starfish", "Static", "Statistic", "Steep", "Steerable", "Stellar", "Stoppable", "Storable", "Strenuous", "Strive", "Stubble", "Stubborn", "Stumble", "Subarctic", "Subatomic", "Subject", "Subsonic", "Subtotal", "Such", "Sudden", "Suitable", "Sulphuric", "Superior", "Surgical", "Survive", "Swear", "Swimmable", "Symphonic", "Synthetic", "Tactical", "Thespian", "Thimble", "Tiny", "Treble", "Tremble", "Tribunal", "Tributary", "Trivial", "Tropical", "Trouble", "Trustable", "Twistable", "Umbilical", "Unable", "Unboxed", "Uncurious", "Uneasy", "Uneatable", "Unequal", "Unethical", "Unfixable", "Unfixed", "Unhappy", "Unhealthy", "Unhelpful", "Unisexual", "Unlawful", "Unlikable", "Unlivable", "Unlovable", "Unlucky", "Unmindful", "Unmixable", "Unmixed", "Unmoral", "Unmovable", "Unnamable", "Unnatural", "Unpopular", "Unrelated", "Unsafe", "Unselfish", "Unsocial", "Unstable", "Unsteady", "Unstylish", "Untaxed", "Unusable", "Unused", "Unusual", "Unviable", "Unvocal", "Unwary", "Usable", "Useable", "Usual", "Valid", "Variable", "Various", "Vascular", "Vehicular", "Vengeful", "Venomous", "Vertical", "Viable", "Viewable", "Vigorous", "Viral", "Virtual", "Virtuous", "Viscous", "Visible", "Vivacious", "Washable", "Whacky", "Whimsical", "Whole", "Wieldable", "Wish", "Wistful", "Wobble", "Worried", "Wrongful", "Yiddish", "Zealous", "Zippy"]; export const nouns = ["Abacus", "Ability", "Ablaze", "Abrasion", "Abreast", "Abridge", "Abroad", "Absence", "Absentee", "Absinthe", "Absolute", "Abstain", "Abstract", "Accent", "Acclaim", "Acclimate", "Accompany", "Account", "Accuracy", "Accurate", "Accustom", "Acetone", "Achiness", "Acid", "Acquaint", "Acquire", "Acre", "Acrobat", "Acronym", "Action", "Activate", "Activator", "Activism", "Activist", "Activity", "Actress", "Acuteness", "Aeration", "Aerosol", "Aerospace", "Afar", "Affair", "Affection", "Affidavit", "Affiliate", "Affirm", "Affix", "Affluent", "Afford", "Affront", "Aflame", "Afloat", "Aflutter", "Afoot", "Afterlife", "Aftermath", "Aftermost", "Afternoon", "Ageless", "Agency", "Agenda", "Agent", "Aggregate", "Aghast", "Agile", "Agility", "Agony", "Agreement", "Aground", "Ahead", "Ahoy", "Aide", "Aim", "Ajar", "Alabaster", "Alarm", "Album", "Alfalfa", "Algebra", "Algorithm", "Alibi", "Alienate", "Alkaline", "Almanac", "Almighty", "Aloe", "Aloha", "Aloof", "Alphabet", "Alright", "Altitude", "Alto", "Aluminum", "Amaze", "Ambiance", "Ambiguity", "Ambition", "Ambulance", "Ambush", "Amendment", "Amenity", "Amino", "Ammonium", "Amnesty", "Amount", "Amperage", "Ample", "Amplifier", "Amply", "Amuck", "Amulet", "Amusement", "Anaconda", "Anagram", "Anatomist", "Anatomy", "Anchor", "Anchovy", "Ancient", "Android", "Anemia", "Aneurism", "Anger", "Angler", "Angriness", "Animal", "Animate", "Animation", "Animator", "Anime", "Animosity", "Ankle", "Annex", "Annotate", "Announcer", "Annuity", "Anointer", "Antacid", "Anteater", "Antelope", "Antennae", "Anthem", "Anthill", "Anthology", "Antibody", "Antidote", "Antihero", "Antiquity", "Antirust", "Antitoxic", "Antivirus", "Antler", "Antonym", "Antsy", "Anvil", "Anybody", "Anyhow", "Anyone", "Anyplace", "Anything", "Aorta", "Apache", "Appease", "Appendage", "Appendix", "Appetite", "Appetizer", "Applaud", "Applause", "Appliance", "Applicant", "Appointee", "Appraisal", "Approach", "Approval", "Apricot", "Aptitude", "Aqua", "Aqueduct", "Arbitrate", "Area", "Arena", "Argue", "Arise", "Armadillo", "Armband", "Armchair", "Armhole", "Armless", "Armoire", "Armory", "Army", "Aroma", "Arousal", "Arrange", "Array", "Arrest", "Arrival", "Arrogance", "Arson", "Art", "Ascension", "Ascent", "Ascertain", "Ashen", "Ashy", "Askew", "Asparagus", "Aspect", "Aspirate", "Aspire", "Aspirin", "Astound", "Astrology", "Astronaut", "Astronomy", "Astute", "Atom", "Atop", "Atrium", "Atrophy", "Attach", "Attain", "Attempt", "Attendant", "Attendee", "Attention", "Attire", "Attitude", "Attractor", "Attribute", "Auction", "Audacity", "Audience", "Audio", "Audition", "Author", "Autism", "Autograph", "Automaker", "Autopilot", "Avalanche", "Avatar", "Avenge", "Avenue", "Aversion", "Avert", "Aviation", "Aviator", "Avid", "Avoid", "Await", "Award", "Awhile", "Awoke", "Awry", "Axis", "Baboon", "Backache", "Backdrop", "Backer", "Backfield", "Backfire", "Backhand", "Backlash", "Backless", "Backlight", "Backlog", "Backpack", "Backroom", "Backshift", "Backspace", "Backspin", "Backstab", "Backstage", "Backtalk", "Backtrack", "Backup", "Backwash", "Backwater", "Bacon", "Bacterium", "Badass", "Badge", "Badland", "Badness", "Baffle", "Baggage", "Bagginess", "Bagpipe", "Baguette", "Bakery", "Bakeshop", "Balance", "Balcony", "Balmy", "Bamboo", "Banister", "Banjo", "Bankbook", "Banker", "Banking", "Banknote", "Bankroll", "Banner", "Bannister", "Banshee", "Banter", "Barbecue", "Barcode", "Barge", "Bargraph", "Barista", "Baritone", "Barmaid", "Barn", "Barometer", "Barrack", "Barrel", "Barrette", "Barricade", "Barrier", "Barstool", "Bartender", "Barterer", "Bash", "Basil", "Basis", "Basket", "Batboy", "Batch", "Bath", "Battalion", "Battery", "Bazooka", "Bladder", "Blade", "Blah", "Blame", "Blandness", "Blank", "Blaspheme", "Blasphemy", "Blast", "Blatancy", "Blazer", "Bleach", "Bleep", "Bless", "Blimp", "Blinker", "Blip", "Blitz", "Blob", "Blog", "Blot", "Bluff", "Blunderer", "Blunt", "Blurb", "Blurt", "Blush", "Blustery", "Boaster", "Boat", "Bobcat", "Bobtail", "Body", "Boggle", "Boil", "Bok", "Bolster", "Bolt", "Bonanza", "Bondless", "Bonehead", "Boneless", "Boney", "Bonfire", "Bonnet", "Bonsai", "Bonus", "Bony", "Book", "Bootlace", "Bootleg", "Boozy", "Borax", "Borrower", "Botanist", "Botany", "Botch", "Bottle", "Bottom", "Bounce", "Bouncy", "Boundless", "Bovine", "Boxcar", "Boxer", "Breach", "Breath", "Breeder", "Breeze", "Breezy", "Brewery", "Brewing", "Briar", "Bribe", "Brick", "Brigade", "Brilliant", "Brink", "Brisket", "Briskness", "Broadband", "Broadcast", "Broadness", "Broiler", "Broker", "Bronco", "Bronze", "Brook", "Broom", "Browbeat", "Browse", "Brunch", "Brunette", "Brunt", "Brush", "Brute", "Buccaneer", "Bucket", "Buckle", "Buckwheat", "Buddhism", "Buddhist", "Buffalo", "Buffer", "Buffoon", "Bulb", "Bulge", "Bulginess", "Bulgur", "Bulk", "Bulldog", "Bulldozer", "Bullfight", "Bullfrog", "Bullion", "Bullseye", "Bullwhip", "Bunch", "Bungee", "Bunion", "Bunkmate", "Bunny", "Bunt", "Busboy", "Busload", "Bust", "Busybody", "Buzz", "Cabbage", "Cabbie", "Cabdriver", "Cache", "Cackle", "Cacti", "Cactus", "Caddie", "Cadet", "Cadillac", "Cadmium", "Cage", "Calamari", "Calamity", "Calcium", "Calculate", "Calculus", "Calibrate", "Calm", "Caloric", "Calorie", "Calzone", "Camcorder", "Cameo", "Camera", "Camisole", "Camper", "Campfire", "Campsite", "Campus", "Cancel", "Candle", "Candy", "Cane", "Canine", "Canister", "Cannabis", "Cannon", "Canola", "Canon", "Canopener", "Canopy", "Canteen", "Canyon", "Capacity", "Cape", "Capital", "Capitol", "Capricorn", "Capsule", "Caption", "Captivate", "Captivity", "Capture", "Caramel", "Carat", "Caravan", "Carbon", "Cardboard", "Cardiac", "Cardigan", "Cardstock", "Caregiver", "Careless", "Caress", "Caretaker", "Cargo", "Carless", "Carload", "Carmaker", "Carnage", "Carnation", "Carnival", "Carnivore", "Carol", "Carpentry", "Carpool", "Carport", "Carrot", "Carrousel", "Carry", "Cartel", "Cartload", "Carton", "Cartoon", "Cartridge", "Cartwheel", "Carve", "Carwash", "Cascade", "Case", "Cash", "Casino", "Casket", "Cassette", "Casualty", "Catacomb", "Catalog", "Catalyst", "Catalyze", "Catapult", "Cataract", "Catcall", "Catcher", "Catchy", "Caterer", "Catfight", "Cathouse", "Catlike", "Catnap", "Catnip", "Catsup", "Cattail", "Catty", "Catwalk", "Caucus", "Causal", "Causation", "Cause", "Caution", "Cavalier", "Cavalry", "Caviar", "Cavity", "Cedar", "Celery", "Celibacy", "Celibate", "Cement", "Census", "Ceremony", "Certainty", "Cesarean", "Cesspool", "Chafe", "Chain", "Chair", "Chalice", "Challenge", "Chamomile", "Champion", "Chance", "Change", "Chant", "Chaos", "Chaperone", "Chaplain", "Chapter", "Character", "Charbroil", "Charcoal", "Charger", "Chariot", "Charity", "Charm", "Charter", "Chase", "Chaste", "Chastise", "Chastity", "Chatroom", "Chatter", "Chatty", "Cheddar", "Cheek", "Cheer", "Cheese", "Cheesy", "Chef", "Chemist", "Chemo", "Cherisher", "Cherub", "Chess", "Chevy", "Chewer", "Chewy", "Chief", "Childcare", "Childhood", "Childless", "Childlike", "Chili", "Chill", "Chimp", "Chip", "Chirpy", "Chivalry", "Chloride", "Chlorine", "Choice", "Chokehold", "Chomp", "Chooser", "Choosy", "Chop", "Chowder", "Chowtime", "Chrome", "Chubby", "Chuck", "Chug", "Chummy", "Chump", "Chunk", "Churn", "Chute", "Cider", "Cilantro", "Cinch", "Cinema", "Cinnamon", "Circle", "Circulate", "Circus", "Citadel", "Citation", "Citizen", "Citric", "Citrus", "Civic", "Civil", "Clad", "Claim", "Clammy", "Clamor", "Clamp", "Clamshell", "Clang", "Clapper", "Clarinet", "Clarity", "Clash", "Clasp", "Class", "Clatter", "Clause", "Clavicle", "Claw", "Clay", "Cleat", "Cleaver", "Cleft", "Clench", "Clergyman", "Clerk", "Clever", "Clicker", "Client", "Climate", "Clinic", "Clip", "Clique", "Cloak", "Clock", "Clone", "Closure", "Clothing", "Cloud", "Clover", "Clubhouse", "Clump", "Clumsy", "Clunky", "Clutch", "Clutter", "Coach", "Coagulant", "Coaster", "Coastland", "Coastline", "Coat", "Coauthor", "Cobalt", "Cobbler", "Cobweb", "Cocoa", "Coconut", "Cod", "Coeditor", "Coerce", "Coexist", "Coffee", "Cofounder", "Cognition", "Cogwheel", "Coherence", "Coherent", "Coil", "Coke", "Cola", "Cold", "Coleslaw", "Coliseum", "Collage", "Collapse", "Collar", "Collector", "Collide", "Collision", "Colonial", "Colonist", "Colony", "Colossal", "Colt", "Coma", "Comfort", "Comfy", "Comma", "Commence", "Commend", "Comment", "Commode", "Commodity", "Commodore", "Common", "Commotion", "Commute", "Compacter", "Compactor", "Companion", "Company", "Compare", "Compel", "Compile", "Comply", "Component", "Composer", "Compost", "Composure", "Compound", "Compress", "Comrade", "Conceal", "Concept", "Concert", "Conch", "Concierge", "Concise", "Conclude", "Concrete", "Concur", "Condense", "Condiment", "Condition", "Condone", "Conductor", "Conduit", "Cone", "Confess", "Confidant", "Confider", "Configure", "Confirm", "Conflict", "Conform", "Confound", "Confront", "Confusion", "Conjure", "Conjuror", "Connector", "Consensus", "Consent", "Console", "Consonant", "Constrain", "Constrict", "Construct", "Consult", "Consumer", "Contact", "Container", "Contempt", "Contest", "Context", "Contort", "Contour", "Contrite", "Control", "Contusion", "Convene", "Convent", "Copartner", "Cope", "Copier", "Copilot", "Copper", "Copy", "Cork", "Cornball", "Cornbread", "Corncob", "Cornea", "Corner", "Cornfield", "Cornhusk", "Cornmeal", "Cornstalk", "Corny", "Coroner", "Correct", "Corridor", "Corrode", "Corsage", "Corset", "Cortex", "Cosigner", "Cosmos", "Cosponsor", "Cost", "Cottage", "Cotton", "Cough", "Countdown", "Countless", "Country", "Courier", "Covenant", "Cover", "Coyness", "Coziness", "Cozy", "Crabgrass", "Crablike", "Crabmeat", "Cradle", "Crafter", "Craftsman", "Craftwork", "Crafty", "Cramp", "Cranberry", "Cranium", "Crank", "Crate", "Crayon", "Craziness", "Crazy", "Creamer", "Creamlike", "Crease", "Create", "Creation", "Creature", "Credit", "Creed", "Creme", "Creole", "Crepe", "Crept", "Crescent", "Crestless", "Crevice", "Crewless", "Crewman", "Crewmate", "Crib", "Cricket", "Crier", "Crimp", "Cringe", "Crinkle", "Crinkly", "Crisply", "Crispness", "Crispy", "Critter", "Croak", "Crock", "Crook", "Croon", "Crop", "Cross", "Crouton", "Crowbar", "Crowd", "Crown", "Crudeness", "Cruelness", "Cruelty", "Crumb", "Crummy", "Crumpet", "Cruncher", "Crunchy", "Crusader", "Crusher", "Crust", "Crux", "Crystal", "Cubbyhole", "Cube", "Cubicle", "Cuddle", "Cufflink", "Culminate", "Culprit", "Cultivate", "Culture", "Cupbearer", "Curator", "Curdle", "Cure", "Curfew", "Curler", "Curliness", "Curry", "Curse", "Cursor", "Curtain", "Curtsy", "Curvature", "Curve", "Curvy", "Cushy", "Cusp", "Custard", "Custody", "Customer", "Cut", "Cycle", "Cycling", "Cyclist", "Cylinder", "Cymbal", "Cytoplasm", "Cytoplast", "Dab", "Dad", "Daffodil", "Dagger", "Dainty", "Dairy", "Daisy", "Dance", "Dandelion", "Dander", "Dandruff", "Danger", "Dangle", "Daredevil", "Darkness", "Darkroom", "Darn", "Dart", "Darwinism", "Dash", "Datebook", "Daughter", "Dawdler", "Dawn", "Daybreak", "Daycare", "Daydream", "Daylight", "Daylong", "Dayroom", "Daytime", "Dazzler", "Deacon", "Deafness", "Dealer", "Dealmaker", "Dealt", "Dean", "Debate", "Debit", "Debrief", "Debtless", "Debtor", "Debug", "Debunk", "Decade", "Decaf", "Decathlon", "Decay", "Deceit", "Deceiver", "Decency", "Decent", "Deception", "Decibel", "Decimal", "Decimeter", "Decipher", "Deck", "Decline", "Decode", "Decorator", "Decoy", "Decrease", "Decree", "Dedicate", "Dedicator", "Deduce", "Deduct", "Deed", "Deem", "Deeply", "Deepness", "Deface", "Defame", "Default", "Defeat", "Defection", "Defendant", "Defender", "Defense", "Defensive", "Defiance", "Defiant", "Defile", "Define", "Definite", "Deflate", "Deflation", "Deflator", "Deflector", "Defog", "Defraud", "Defrost", "Defuse", "Defy", "Degrease", "Degree", "Dehydrate", "Deity", "Delay", "Delegate", "Delegator", "Deletion", "Delicacy", "Delicate", "Delirium", "Deliverer", "Delivery", "Delouse", "Deluge", "Delusion", "Deluxe", "Demeanor", "Demise", "Democracy", "Demote", "Demotion", "Denim", "Denote", "Dense", "Density", "Dental", "Dentist", "Denture", "Deny", "Deodorant", "Departure", "Depict", "Depletion", "Deploy", "Deport", "Depravity", "Deprecate", "Depress", "Depth", "Deputy", "Derail", "Desecrate", "Deserve", "Designate", "Designer", "Deskbound", "Desktop", "Deskwork", "Desolate", "Despair", "Despise", "Destiny", "Destitute", "Destruct", "Detail", "Detection", "Detective", "Detector", "Detention", "Detergent", "Detonate", "Detonator", "Detract", "Deuce", "Devalue", "Deviancy", "Deviant", "Deviate", "Deviation", "Deviator", "Device", "Devotee", "Devotion", "Devourer", "Dexterity", "Diabolic", "Diagnosis", "Diagram", "Dial", "Diameter", "Diaper", "Diaphragm", "Dice", "Dictate", "Dictation", "Dictator", "Difficult", "Diffuser", "Diffusion", "Dig", "Dilation", "Diligence", "Diligent", "Dill", "Dilute", "Dime", "Dimmer", "Dimness", "Dimple", "Diner", "Dingbat", "Dinghy", "Dinginess", "Dingo", "Dingy", "Dinner", "Dioxide", "Diploma", "Dipper", "Direction", "Directive", "Directory", "Direness", "Dirtiness", "Disallow", "Disarm", "Disarray", "Disaster", "Disband", "Disbelief", "Disburse", "Discard", "Discern", "Discharge", "Discolor", "Discount", "Discourse", "Discover", "Discuss", "Disdain", "Disengage", "Disfigure", "Disgrace", "Dish", "Disinfect", "Disjoin", "Disk", "Dislike", "Dislocate", "Dislodge", "Disloyal", "Dismantle", "Dismay", "Dismount", "Disobey", "Disorder", "Disown", "Disparate", "Disparity", "Dispatch", "Dispense", "Dispersal", "Disperser", "Displace", "Display", "Displease", "Disposal", "Dispute", "Disregard", "Dissuade", "Distance", "Distant", "Distaste", "Distill", "Distort", "Distract", "Distress", "Distrust", "Ditch", "Ditto", "Ditzy", "Dividend", "Divinity", "Division", "Divorcee", "Dizziness", "Dizzy", "Docile", "Dock", "Doctrine", "Document", "Dodge", "Dodgy", "Dole", "Dollar", "Dollhouse", "Dolphin", "Domain", "Domelike", "Dominion", "Donation", "Donator", "Donor", "Donut", "Doodle", "Doorframe", "Doorknob", "Doorman", "Doormat", "Doornail", "Doorpost", "Doorstep", "Doorstop", "Doorway", "Doozy", "Dork", "Dormitory", "Dorsal", "Dosage", "Douche", "Dowry", "Doze", "Drab", "Dragonfly", "Dragster", "Drainage", "Drainer", "Drainpipe", "Drank", "Drapery", "Draw", "Dreadlock", "Dreamboat", "Dreamland", "Dreamless", "Dreamlike", "Dreamt", "Dreamy", "Drench", "Dress", "Drier", "Drift", "Driller", "Drilling", "Driver", "Driveway", "Drizzle", "Drizzly", "Drone", "Drool", "Droop", "Dropbox", "Dropkick", "Droplet", "Dropout", "Dropper", "Drown", "Drudge", "Drum", "Dry", "Duchess", "Duckbill", "Ducktail", "Ducky", "Duct", "Dude", "Duffel", "Dugout", "Duh", "Duller", "Dullness", "Duly", "Dumpster", "Duo", "Dupe", "Duplicate", "Duplicity", "Duration", "Duress", "Dusk", "Dust", "Duty", "Duvet", "Dwarf", "Dweeb", "Dweller", "Dwindle", "Dynamite", "Dynasty", "Dyslexia", "Dyslexic", "Eagle", "Earache", "Eardrum", "Earflap", "Earlobe", "Earmark", "Earmuff", "Earphone", "Earpiece", "Earshot", "Earthen", "Earthlike", "Earthworm", "Earwig", "Easel", "Easiness", "Eastbound", "Eastcoast", "Easter", "Eastward", "Eatery", "Ebay", "Ebony", "Ebook", "Ecard", "Echo", "Eclair", "Eclipse", "Ecologist", "Ecology", "Economist", "Economy", "Ecosystem", "Edge", "Edginess", "Edgy", "Edition", "Editor", "Education", "Educator", "Eel", "Efficient", "Effort", "Eggbeater", "Eggnog", "Eggplant", "Eggshell", "Egomaniac", "Egotism", "Eject", "Elaborate", "Elbow", "Eldercare", "Election", "Elephant", "Elevate", "Elevation", "Elevator", "Elf", "Eliminate", "Elitism", "Elixir", "Elk", "Ellipse", "Elm", "Elope", "Eloquence", "Eloquent", "Elude", "Email", "Embargo", "Embark", "Embassy", "Embezzle", "Emblaze", "Emblem", "Embody", "Embolism", "Emboss", "Embroider", "Emcee", "Emerald", "Emergency", "Emission", "Emit", "Emote", "Emoticon", "Emotion", "Empathy", "Emperor", "Emphasis", "Employee", "Employer", "Emporium", "Empower", "Emptier", "Emptiness", "Emu", "Enactment", "Enamel", "Enchilada", "Encircle", "Enclosure", "Encode", "Encore", "Encounter", "Encourage", "Encroach", "Encrust", "Encrypt", "Endanger", "Endless", "Endnote", "Endocrine", "Endorphin", "Endorse", "Endowment", "Endpoint", "Endurance", "Enforcer", "Engine", "Engorge", "Engraver", "Engross", "Engulf", "Enhance", "Enjoyer", "Enjoyment", "Enquirer", "Enrage", "Enrich", "Enroll", "Ensnare", "Ensure", "Entail", "Entertain", "Entire", "Entitle", "Entity", "Entomb", "Entourage", "Entrap", "Entree", "Entrench", "Entrust", "Entwine", "Enunciate", "Envelope", "Envision", "Envoy", "Envy", "Enzyme", "Epic", "Epidermis", "Epilepsy", "Epilogue", "Epiphany", "Episode", "Equate", "Equation", "Equator", "Equinox", "Equipment", "Equity", "Eradicate", "Eraser", "Erasure", "Errand", "Errant", "Error", "Escalate", "Escalator", "Escapade", "Escapist", "Eskimo", "Esophagus", "Espionage", "Espresso", "Esquire", "Essay", "Essence", "Estimate", "Estimator", "Estrogen", "Eternity", "Ethanol", "Ether", "Euphemism", "Evacuate", "Evacuee", "Evade", "Evaluate", "Evaluator", "Evaporate", "Evasion", "Everglade", "Evergreen", "Everybody", "Everyone", "Evict", "Evidence", "Evident", "Evil", "Evoke", "Evolution", "Evolve", "Exact", "Example", "Excavate", "Excavator", "Exception", "Excess", "Exclaim", "Exclude", "Exclusion", "Excretion", "Excretory", "Excursion", "Excuse", "Exemption", "Exerciser", "Exert", "Exfoliate", "Exhale", "Exhaust", "Exhume", "Exile", "Exit", "Exodus", "Exonerate", "Exorcism", "Exorcist", "Expand", "Expanse", "Expansion", "Expectant", "Expediter", "Expel", "Expend", "Expert", "Expire", "Explain", "Explicit", "Explode", "Explore", "Exponent", "Exporter", "Exposure", "Express", "Expulsion", "Exquisite", "Extent", "Extenuate", "Exterior", "Extinct", "Extortion", "Extradite", "Extrovert", "Extrude", "Exuberant", "Fabric", "Facebook", "Faceless", "Facelift", "Faceplate", "Facility", "Facsimile", "Faction", "Factoid", "Factor", "Factsheet", "Faculty", "Fade", "Falcon", "Fall", "Falsify", "Fame", "Familiar", "Famine", "Fanciness", "Fancy", "Fanfare", "Fang", "Fantasize", "Fantasy", "Fascism", "Fastball", "Faster", "Fastness", "Faucet", "Favorite", "Fax", "Feast", "Feed", "Feel", "Feisty", "Feline", "Felt-tip", "Feminine", "Feminism", "Feminist", "Feminize", "Femur", "Fence", "Fender", "Ferment", "Fernlike", "Ferocity", "Ferris", "Ferry", "Fervor", "Fester", "Festival", "Festivity", "Fetal", "Fetch", "Fever", "Fiction", "Fiddle", "Fidelity", "Fidgety", "Fifteen", "Fifty", "Figment", "Figure", "Figurine", "Filler", "Film", "Filter", "Filth", "Filtrate", "Finale", "Finalist", "Finalize", "Finance", "Finch", "Fineness", "Finer", "Finicky", "Finisher", "Finite", "Finless", "Finlike", "Fit", "Flaccid", "Flagman", "Flagpole", "Flagship", "Flagstick", "Flagstone", "Flail", "Flaky", "Flame", "Flap", "Flashback", "Flashbulb", "Flashcard", "Flask", "Flatfoot", "Flatness", "Flatterer", "Flattery", "Flattop", "Flatware", "Flatworm", "Flaxseed", "Fleshy", "Flick", "Flier", "Flight", "Flinch", "Flint", "Flip", "Flirt", "Float", "Flock", "Flop", "Florist", "Floss", "Flounder", "Flyaway", "Flyer", "Flyover", "Flypaper", "Foam", "Foe", "Fog", "Foil", "Folic", "Folk", "Follicle", "Fondness", "Font", "Food", "Fool", "Footage", "Football", "Footbath", "Footboard", "Footer", "Footgear", "Foothill", "Foothold", "Footless", "Footman", "Footnote", "Footpad", "Footpath", "Footprint", "Footsie", "Footsore", "Footwear", "Footwork", "Fossil", "Foster", "Founder", "Fountain", "Fox", "Foyer", "Fraction", "Fracture", "Fragile", "Fragility", "Fragment", "Fragrance", "Fragrant", "Frail", "Frame", "Freebase", "Freebee", "Freebie", "Freedom", "Freefall", "Freehand", "Freeload", "Freemason", "Freeness", "Freestyle", "Freeware", "Freeway", "Freewill", "Freight", "Frenzy", "Frequency", "Frequent", "Friction", "Fridge", "Friend", "Frigidity", "Frill", "Fringe", "Frisbee", "Frisk", "Fritter", "Frolic", "Front", "Frostbite", "Frostlike", "Frosty", "Fructose", "Frugality", "Fruit", "Frustrate", "Gab", "Gaffe", "Gag", "Gala", "Gallery", "Gallon", "Gallstone", "Galore", "Game", "Gamma", "Gander", "Gangway", "Gap", "Garage", "Garbage", "Garden", "Gargle", "Garland", "Garment", "Garnet", "Garter", "Gatherer", "Gauntlet", "Gauze", "Gawk", "Gecko", "Geek", "Geiger", "Gem", "Gender", "Genre", "Gentile", "Gentleman", "Geography", "Geologist", "Geology", "Geometry", "Geranium", "Gerbil", "Germicide", "Germinate", "Germless", "Germproof", "Gestate", "Gestation", "Gesture", "Getaway", "Getup", "Giant", "Giblet", "Giddiness", "Gift", "Gigabyte", "Gigahertz", "Giggle", "Gigolo", "Gimmick", "Giver", "Gizmo", "Glade", "Gladiator", "Glamour", "Glance", "Glandular", "Glare", "Glass", "Glaucoma", "Glider", "Glimmer", "Glimpse", "Glitch", "Glitter", "Glitzy", "Gloater", "Glory", "Gloss", "Glowworm", "Gnat", "Goal", "Goldmine", "Goldsmith", "Golf", "Goliath", "Gonad", "Gone", "Gooey", "Goofball", "Goofiness", "Goofy", "Google", "Goon", "Gopher", "Gore", "Gory", "Gossip", "Gout", "Grab", "Graceless", "Gradation", "Grader", "Gradient", "Graduate", "Grain", "Granddad", "Grandkid", "Grandma", "Grandpa", "Grandson", "Granny", "Grant", "Grape", "Graph", "Grapple", "Grasp", "Grass", "Gratitude", "Gratuity", "Gravel", "Graveness", "Graveyard", "Gravitate", "Gravity", "Gray", "Greedless", "Greedy", "Greeter", "Greyhound", "Grid", "Grief", "Grievance", "Grill", "Grimace", "Grime", "Griminess", "Grimy", "Grinch", "Grip", "Grit", "Groin", "Groom", "Groovy", "Grope", "Ground", "Grout", "Grower", "Growl", "Grub", "Grudge", "Gruffly", "Grunge", "Grunt", "Guacamole", "Guidance", "Guileless", "Guise", "Gulp", "Gumball", "Gumdrop", "Gumminess", "Gummy", "Gurgle", "Guru", "Gush", "Gusto", "Gusty", "Gutless", "Gutter", "Guy", "Guzzler", "Gyration", "Habitant", "Habitat", "Hacker", "Haggler", "Haiku", "Half", "Halogen", "Halt", "Hamburger", "Hamlet", "Hammock", "Hamper", "Hamster", "Handbag", "Handball", "Handbook", "Handbrake", "Handcart", "Handclap", "Handclasp", "Handcraft", "Handcuff", "Handful", "Handgrip", "Handgun", "Handiness", "Handiwork", "Handlebar", "Handler", "Handmade", "Handoff", "Handpick", "Handprint", "Handrail", "Handset", "Handsfree", "Handshake", "Handstand", "Handwash", "Handwork", "Handyman", "Hangnail", "Hangout", "Hangover", "Hangup", "Hankie", "Hanky", "Haphazard", "Happiness", "Harbor", "Hardcopy", "Hardcore", "Hardcover", "Harddisk", "Hardener", "Hardhead", "Hardiness", "Hardness", "Hardship", "Hardware", "Hardwood", "Hardy", "Harmless", "Harmony", "Harness", "Harpist", "Harsh", "Hash", "Hassle", "Haste", "Hastiness", "Hatbox", "Hatchback", "Hatchery", "Hatchet", "Hate", "Hatless", "Haunt", "Hazard", "Hazelnut", "Haziness", "Hazy", "Headache", "Headband", "Headboard", "Headcount", "Headdress", "Header", "Headgear", "Headlamp", "Headless", "Headlock", "Headphone", "Headpiece", "Headroom", "Headscarf", "Headset", "Headsman", "Headstand", "Headstone", "Headway", "Headwear", "Heat", "Heaviness", "Hedge", "Heftiness", "Helium", "Helmet", "Helper", "Helpless", "Helpline", "Hemlock", "Hemstitch", "Hence", "Henchman", "Herbicide", "Heritage", "Hermit", "Heroism", "Hesitancy", "Hesitant", "Hesitate", "Hexagon", "Hexagram", "Hubcap", "Huff", "Hug", "Hula", "Hulk", "Hull", "Humid", "Humiliate", "Humility", "Hummus", "Humorist", "Humorless", "Humpback", "Humvee", "Hunchback", "Hunger", "Hunk", "Hunter", "Huntress", "Huntsman", "Hurdle", "Hurler", "Hurray", "Hurry", "Husband", "Hush", "Huskiness", "Hut", "Hydrant", "Hydration", "Hydrogen", "Hydroxide", "Hyperlink", "Hypertext", "Hyphen", "Hypnosis", "Hypnotism", "Hypnotist", "Hypocrisy", "Ibuprofen", "Ice", "Iciness", "Icon", "Icy", "Idealism", "Idealist", "Idealness", "Identity", "Ideology", "Idiocy", "Idiom", "Igloo", "Ignition", "Ignore", "Illusion", "Image", "Imbecile", "Imitate", "Imitation", "Immature", "Immerse", "Immersion", "Imminent", "Immobile", "Immortal", "Immunity", "Impale", "Impart", "Impatient", "Impeach", "Imperfect", "Implant", "Implement", "Implicate", "Implicit", "Implode", "Implosion", "Imply", "Impolite", "Important", "Importer", "Impotence", "Impotency", "Impotent", "Impound", "Imprecise", "Imprint", "Imprison", "Impromptu", "Improvise", "Imprudent", "Impure", "Impurity", "Iodine", "Ion", "Ipad", "Iphone", "Ipod", "Irate", "Irk", "Iron", "Irrigate", "Irritant", "Irritate", "Islamist", "Isolation", "Isotope", "Issue", "Item", "Ivory", "Ivy", "Jab", "Jackal", "Jacket", "Jackknife", "Jackpot", "Jailbird", "Jailbreak", "Jailer", "Jailhouse", "Jalapeno", "Jam", "Janitor", "Jargon", "Jasmine", "Jaundice", "Jaunt", "Java", "Jawless", "Jawline", "Jaybird", "Jaywalker", "Jazz", "Jeep", "Jester", "Jet", "Jiffy", "Jigsaw", "Jimmy", "Jingle", "Jinx", "Jittery", "Job", "Jockey", "Jockstrap", "Jogger", "Jokester", "Jolliness", "Jolt", "Jot", "Joyride", "Joystick", "Jubilance", "Jubilant", "Judo", "Juggle", "Jugular", "Juice", "Juiciness", "Juicy", "Jukebox", "Jump", "Junction", "Juncture", "Juniper", "Junkie", "Junkman", "Junkyard", "Jurist", "Juror", "Jury", "Justice", "Justifier", "Justness", "Juvenile", "Keenness", "Kerchief", "Kilometer", "Kindness", "Kinship", "Knapsack", "Laborer", "Labrador", "Ladder", "Ladybug", "Ladylike", "Lagoon", "Lair", "Lance", "Landfall", "Landfill", "Landless", "Landline", "Landlord", "Landmark", "Landmass", "Landmine", "Landowner", "Landscape", "Landside", "Landslide", "Language", "Lankiness", "Lanky", "Lapdog", "Lard", "Lark", "Lash", "Lasso", "Latch", "Lather", "Latitude", "Latrine", "Latter", "Launch", "Launder", "Laundry", "Laurel", "Lavender", "Laziness", "Lazy", "Lecturer", "Legacy", "Lego", "Legroom", "Legwarmer", "Legwork", "Lemon", "Length", "Lent", "Leotard", "Lethargic", "Lethargy", "Letter", "Lettuce", "Level", "Leverage", "Levitate", "Levitator", "Liability", "Liberty", "Licorice", "Lid", "Life", "Lifter", "Liftoff", "Ligament", "Likeness", "Lilac", "Limb", "Limeade", "Limelight", "Limit", "Limpness", "Line", "Lingo", "Linguist", "Linoleum", "Lint", "Lion", "Lip", "Liquefy", "Liqueur", "Liquid", "Lisp", "List", "Litigate", "Litigator", "Litmus", "Litter", "Liver", "Livestock", "Lizard", "Lubricant", "Lubricate", "Lucid", "Luckiness", "Luckless", "Lukewarm", "Lullaby", "Luminance", "Lumpiness", "Lunacy", "Lunar", "Lunchbox", "Luncheon", "Lunchroom", "Lunchtime", "Lung", "Lurch", "Lure", "Luridness", "Lurk", "Lushness", "Luster", "Lustiness", "Lusty", "Luxury", "Lyricism", "Lyricist", "Macarena", "Macaroni", "Macaw", "Mace", "Machine", "Machinist", "Magazine", "Magenta", "Magma", "Magnesium", "Magnetism", "Magnify", "Magnitude", "Mahogany", "Majesty", "Majorette", "Majority", "Makeover", "Maker", "Makeshift", "Malt", "Mama", "Mammogram", "Manager", "Manatee", "Mandarin", "Mandate", "Mandatory", "Mandolin", "Manger", "Mangle", "Mango", "Mangy", "Manhandle", "Manhole", "Manhood", "Manhunt", "Manicure", "Manifesto", "Manila", "Mankind", "Manlike", "Manliness", "Manmade", "Manor", "Manpower", "Mantis", "Map", "Marathon", "Mardi", "Margarine", "Margin", "Marigold", "Marine", "Maritime", "Marlin", "Marmalade", "Maroon", "Marrow", "Marry", "Marshland", "Marshy", "Marxism", "Mascot", "Masculine", "Massager", "Mastiff", "Matador", "Matchbook", "Matchbox", "Matcher", "Matchless", "Material", "Maternity", "Math", "Matriarch", "Matrimony", "Matrix", "Matter", "Maturity", "Mauve", "Maverick", "Maximum", "Mayday", "Mayflower", "Moaner", "Mobile", "Mobility", "Mobster", "Mocha", "Mocker", "Mockup", "Modify", "Modular", "Modulator", "Module", "Moistness", "Moisture", "Molar", "Mold", "Molecule", "Molehill", "Mollusk", "Mom", "Monastery", "Moneyless", "Moneywise", "Mongoose", "Mongrel", "Monitor", "Monkhood", "Monogamy", "Monogram", "Monologue", "Monopoly", "Monorail", "Monotone", "Monotype", "Monoxide", "Monsieur", "Monsoon", "Monument", "Moocher", "Moodiness", "Moonbeam", "Moonlight", "Moonlike", "Moonlit", "Moonrise", "Moonscape", "Moonshine", "Moonstone", "Moonwalk", "Mop", "Morale", "Morality", "Morbidity", "Morphine", "Morse", "Mortality", "Mortify", "Mosaic", "Mossy", "Mothball", "Mothproof", "Motion", "Motivate", "Motivator", "Motive", "Motocross", "Motto", "Mountain", "Mourner", "Mouse", "Mousiness", "Moustache", "Mousy", "Mouth", "Move", "Movie", "Mower", "Muck", "Mud", "Mug", "Mulberry", "Mulch", "Mule", "Multiple", "Multiply", "Multitask", "Multitude", "Mumbo", "Mummify", "Mummy", "Mundane", "Muppet", "Murkiness", "Murky", "Museum", "Mushiness", "Mushroom", "Mushy", "Musket", "Muskiness", "Musky", "Mustang", "Mustard", "Muster", "Mustiness", "Musty", "Mutate", "Mutation", "Mute", "Mutilator", "Mutiny", "Mutt", "Muzzle", "Myspace", "Mystify", "Myth", "Nacho", "Nag", "Nail", "Name", "Nanny", "Nanometer", "Nape", "Narrow", "Nastiness", "Native", "Nativity", "Nature", "Naturist", "Navigate", "Navigator", "Nearness", "Neatness", "Nebula", "Nebulizer", "Nectar", "Negate", "Negation", "Neglector", "Negligee", "Negligent", "Negotiate", "Nemesis", "Neon", "Nephew", "Nerd", "Nervy", "Net", "Neurology", "Neuron", "Neurosis", "Neuter", "Neutron", "Nickname", "Nicotine", "Niece", "Nifty", "Nineteen", "Ninetieth", "Ninja", "Nintendo", "Nuclear", "Nuclei", "Nucleus", "Nugget", "Numbness", "Numerate", "Numerator", "Nursery", "Nursing", "Nurture", "Nutcase", "Nutlike", "Nutmeg", "Nutrient", "Nutshell", "Nuttiness", "Nuzzle", "Nylon", "Oaf", "Oak", "Oasis", "Oat", "Obedience", "Obedient", "Object", "Obligate", "Oblivion", "Oboe", "Obscure", "Obscurity", "Observant", "Observer", "Obsession", "Obsolete", "Obstacle", "Obstinate", "Obstruct", "Obtain", "Obtuse", "Occultist", "Occupancy", "Occupant", "Occupy", "Ocelot", "Octagon", "Octane", "Octopus", "Ogle", "Oil", "Oink", "Ointment", "Okay", "Old", "Omega", "Omission", "Omit", "Omnivore", "Onboard", "Onion", "Online", "Onlooker", "Onscreen", "Onset", "Onshore", "Onslaught", "Onstage", "Onward", "Onyx", "Ooze", "Oozy", "Opacity", "Opal", "Operate", "Operating", "Operation", "Operator", "Opium", "Opossum", "Opponent", "Oppose", "Opposite", "Oppressor", "Opt", "Osmosis", "Otter", "Ounce", "Outage", "Outback", "Outbid", "Outboard", "Outbound", "Outbreak", "Outcast", "Outclass", "Outcome", "Outer", "Outfield", "Outfit", "Outflank", "Outgrow", "Outhouse", "Outlast", "Outlet", "Outline", "Outlook", "Outmatch", "Outmost", "Outpost", "Outpour", "Output", "Outrage", "Outrank", "Outreach", "Outright", "Outscore", "Outsell", "Outshine", "Outshoot", "Outsider", "Outsmart", "Outsource", "Outthink", "Outward", "Outweigh", "Outwit", "Oval", "Overact", "Overarch", "Overbid", "Overbill", "Overbite", "Overboard", "Overbook", "Overbuilt", "Overcast", "Overcoat", "Overcome", "Overcook", "Overcrowd", "Overdraft", "Overdrawn", "Overdress", "Overeager", "Overeater", "Overexert", "Overfed", "Overfeed", "Overfill", "Overfull", "Overhand", "Overhang", "Overhaul", "Overhead", "Overheat", "Overhung", "Overkill", "Overlabor", "Overlaid", "Overlap", "Overlay", "Overload", "Overlook", "Overlord", "Overnight", "Overpass", "Overpay", "Overplant", "Overplay", "Overpower", "Overprice", "Overrate", "Overreach", "Overreact", "Overripe", "Overrule", "Overrun", "Overshoot", "Overshot", "Oversight", "Oversleep", "Oversold", "Overspend", "Overstate", "Overstay", "Overstep", "Overstock", "Overstuff", "Oversweet", "Overtake", "Overthrow", "Overtime", "Overtone", "Overture", "Overturn", "Overuse", "Overvalue", "Overview", "Overwrite", "Owl", "Oxford", "Oxidant", "Oxidation", "Oxygen", "Oxymoron", "Oyster", "Ozone", "Pacemaker", "Pacifier", "Pacifism", "Pacifist", "Padlock", "Pagan", "Pager", "Pajamas", "Palace", "Palm", "Palpitate", "Paltry", "Pamperer", "Pamphlet", "Panama", "Panda", "Pang", "Panic", "Panorama", "Panther", "Pantomime", "Pantry", "Paparazzi", "Papaya", "Paper", "Papyrus", "Parabola", "Parachute", "Parade", "Paradox", "Paragraph", "Parakeet", "Paralegal", "Paralysis", "Paralyze", "Paramedic", "Parameter", "Parasail", "Parasite", "Parcel", "Parchment", "Pardon", "Parka", "Parkway", "Parlor", "Parole", "Parrot", "Parsley", "Parsnip", "Partition", "Partner", "Partridge", "Passage", "Passcode", "Passenger", "Passion", "Passivism", "Passover", "Passport", "Password", "Pasta", "Pastel", "Pastime", "Pastor", "Pastrami", "Pasture", "Pasty", "Patchwork", "Patchy", "Paternity", "Path", "Patience", "Patient", "Patio", "Patriarch", "Patriot", "Patrol", "Patronage", "Pauper", "Pavement", "Paver", "Pavestone", "Pavilion", "Payback", "Paycheck", "Payday", "Payee", "Payer", "Payment", "Payphone", "Payroll", "Pectin", "Peculiar", "Pedicure", "Pedigree", "Pedometer", "Pegboard", "Pellet", "Pelt", "Pelvis", "Penalty", "Pencil", "Pendant", "Penholder", "Penknife", "Pennant", "Penniless", "Penny", "Penpal", "Pension", "Pentagram", "Pep", "Percent", "Perch", "Percolate", "Perfume", "Periscope", "Perjurer", "Perjury", "Perkiness", "Perky", "Perm", "Peroxide", "Persecute", "Persuader", "Pesky", "Peso", "Pessimism", "Pessimist", "Pester", "Pesticide", "Petal", "Petition", "Petri", "Petroleum", "Petticoat", "Pettiness", "Petty", "Petunia", "Phantom", "Phobia", "Phonebook", "Phoney", "Phoniness", "Phony", "Phosphate", "Photo", "Phrase", "Placard", "Placate", "Plank", "Planner", "Plant", "Plasma", "Plaster", "Plastic", "Platform", "Platinum", "Platonic", "Platter", "Platypus", "Playback", "Player", "Playful", "Playgroup", "Playhouse", "Playlist", "Playmaker", "Playmate", "Playoff", "Playroom", "Playset", "Playtime", "Plaza", "Pleat", "Pledge", "Plentiful", "Plenty", "Plod", "Plot", "Ploy", "Pluck", "Plug", "Plunder", "Plutonium", "Plywood", "Poach", "Pod", "Poem", "Poet", "Pogo", "Pointer", "Pointless", "Pointy", "Poise", "Poison", "Poker", "Polar", "Policy", "Polio", "Polka", "Polo", "Polyester", "Polygon", "Polygraph", "Polymer", "Poncho", "Pond", "Pony", "Popcorn", "Pope", "Poplar", "Popper", "Popsicle", "Populace", "Popular", "Populate", "Porcupine", "Pork", "Porridge", "Portal", "Portfolio", "Porthole", "Portion", "Portside", "Poser", "Posh", "Possum", "Postage", "Postbox", "Postcard", "Poster", "Postnasal", "Posture", "Pounce", "Pound", "Pout", "Powdery", "Power", "Powwow", "Pox", "Prance", "Pranker", "Prankster", "Prayer", "Preacher", "Preachy", "Precinct", "Precision", "Precook", "Precut", "Predator", "Predefine", "Predict", "Preface", "Prefix", "Preflight", "Pregame", "Pregnancy", "Prelaunch", "Prelaw", "Prelude", "Premium", "Prenatal", "Preoccupy", "Preorder", "Prepaid", "Prepay", "Preplan", "Preschool", "Prescribe", "Preseason", "Preset", "Preshow", "Presoak", "Press", "Presume", "Preteen", "Pretender", "Pretense", "Pretext", "Pretzel", "Prevail", "Prevalent", "Prevent", "Preview", "Prewar", "Prideful", "Primal", "Primate", "Primer", "Primp", "Princess", "Print", "Prism", "Prison", "Prissy", "Pristine", "Privacy", "Probation", "Probe", "Problem", "Procedure", "Process", "Proclaim", "Procreate", "Procurer", "Prodigal", "Prodigy", "Product", "Profane", "Profanity", "Profile", "Profound", "Progeny", "Prognosis", "Program", "Progress", "Projector", "Prologue", "Promenade", "Prominent", "Promoter", "Promotion", "Prompter", "Prone", "Prong", "Pronounce", "Pronto", "Proofread", "Proofs", "Propeller", "Property", "Proponent", "Proposal", "Prorate", "Protector", "Protegee", "Proton", "Prototype", "Protozoan", "Protract", "Protrude", "Proud", "Provider", "Province", "Provoke", "Provolone", "Prowess", "Prowler", "Proximity", "Proxy", "Prozac", "Prude", "Prune", "Pry", "Publisher", "Pucker", "Pueblo", "Pug", "Pull", "Pulp", "Pulsate", "Pulse", "Puma", "Pumice", "Pummel", "Punch", "Punctuate", "Pungent", "Punisher", "Punk", "Pupil", "Puppet", "Purchase", "Pureblood", "Pureness", "Purgatory", "Purge", "Purifier", "Purist", "Puritan", "Purity", "Purple", "Purr", "Purse", "Pursuant", "Pursuit", "Purveyor", "Pushcart", "Pushchair", "Pusher", "Pushiness", "Pushover", "Pushpin", "Pushup", "Pushy", "Putdown", "Putt", "Puzzle", "Pyramid", "Pyromania", "Python", "Quack", "Quadrant", "Quail", "Quake", "Qualifier", "Quality", "Qualm", "Quantum", "Quarrel", "Quarry", "Quartet", "Quench", "Query", "Quicken", "Quickness", "Quicksand", "Quickstep", "Quill", "Quilt", "Quintet", "Quintuple", "Quirk", "Quit", "Quiver", "Quotation", "Quote", "Rabid", "Race", "Racism", "Rack", "Racoon", "Radar", "Radiance", "Radiation", "Radiator", "Radio", "Raffle", "Raft", "Rage", "Ragweed", "Raider", "Railcar", "Railroad", "Railway", "Raisin", "Ramp", "Ramrod", "Ranch", "Rancidity", "Random", "Ranger", "Ransack", "Rarity", "Rash", "Ravage", "Ravine", "Ravioli", "Reabsorb", "Reach", "Reacquire", "Reaction", "Reactor", "Reaffirm", "Ream", "Reanalyze", "Reapply", "Reappoint", "Rearrange", "Rearview", "Reason", "Reassign", "Reassure", "Reattach", "Rebalance", "Rebate", "Rebel", "Rebirth", "Reboot", "Rebound", "Rebuff", "Rebuilt", "Recall", "Recant", "Recapture", "Recast", "Recede", "Recent", "Recess", "Recharger", "Recipient", "Recite", "Reclaim", "Recliner", "Recluse", "Recoil", "Recollect", "Recolor", "Reconcile", "Reconfirm", "Reconvene", "Recopy", "Record", "Recount", "Recoup", "Recovery", "Recreate", "Rectangle", "Recycler", "Reemerge", "Reenact", "Reenter", "Reentry", "Reexamine", "Reference", "Refill", "Refinance", "Refinery", "Reflector", "Reflux", "Refold", "Reformat", "Reformer", "Reformist", "Refract", "Refrain", "Refreeze", "Refresh", "Refund", "Refusal", "Refuse", "Refute", "Regain", "Reggae", "Regime", "Region", "Register", "Registrar", "Registry", "Regress", "Regroup", "Regulator", "Rehab", "Reheat", "Rehire", "Rehydrate", "Reimburse", "Reissue", "Reiterate", "Rejoice", "Rejoin", "Relapse", "Relation", "Relax", "Relay", "Relearn", "Release", "Reliance", "Reliant", "Relight", "Reload", "Relock", "Remark", "Remarry", "Rematch", "Remedy", "Reminder", "Remission", "Remix", "Remnant", "Remodeler", "Remold", "Remorse", "Removal", "Remover", "Rename", "Renderer", "Rendition", "Renegade", "Renewal", "Renounce", "Renovate", "Renovator", "Renter", "Reoccupy", "Reoccur", "Reorder", "Repackage", "Repaint", "Repair", "Repayment", "Repeal", "Repeater", "Repent", "Rephrase", "Replace", "Replay", "Replica", "Reply", "Reporter", "Repossess", "Repost", "Reprimand", "Reprint", "Reprise", "Reproach", "Reprocess", "Reprogram", "Reptile", "Repugnant", "Repulsion", "Request", "Require", "Requisite", "Reroute", "Resale", "Resample", "Rescuer", "Reseal", "Research", "Reselect", "Reseller", "Resent", "Reshape", "Reshoot", "Reshuffle", "Residence", "Residency", "Resident", "Resilient", "Resolute", "Resonant", "Resonate", "Resort", "Resource", "Respect", "Resubmit", "Result", "Resupply", "Resurface", "Resurrect", "Retainer", "Retaliate", "Retention", "Rethink", "Retiree", "Retold", "Retool", "Retrace", "Retract", "Retrain", "Retread", "Retreat", "Retrieval", "Retriever", "Retry", "Return", "Retype", "Reunion", "Reunite", "Reuse", "Reveal", "Reveler", "Revenge", "Revenue", "Reverb", "Reverence", "Reversal", "Reverse", "Reversion", "Revert", "Revise", "Revision", "Revisit", "Revival", "Reviver", "Revoke", "Revolt", "Revolver", "Reward", "Rewash", "Rewind", "Rewire", "Reword", "Rework", "Rewrap", "Rhyme", "Ribbon", "Ribcage", "Rice", "Richness", "Rickety", "Ricotta", "Riddance", "Ridden", "Ride", "Rift", "Rigor", "Rimless", "Rind", "Rinse", "Riot", "Ripcord", "Ripeness", "Ripple", "Riptide", "Rise", "Risk", "Risotto", "Ritalin", "Ritzy", "Riverbank", "Riverboat", "Riverside", "Riveter", "Roamer", "Roast", "Robe", "Robin", "Rockband", "Rocker", "Rocket", "Rockiness", "Rocklike", "Rockslide", "Rockstar", "Rogue", "Roman", "Romp", "Rope", "Roster", "Rotunda", "Roulette", "Roundness", "Roundup", "Roundworm", "Rover", "Royal", "Rubdown", "Ruby", "Ruckus", "Rudder", "Rug", "Rule", "Rummage", "Rumor", "Runaround", "Rundown", "Runner", "Runny", "Runt", "Rupture", "Ruse", "Rush", "Rust", "Rut", "Sabbath", "Sabotage", "Sacrament", "Sacrifice", "Sadden", "Saddlebag", "Sadness", "Safari", "Safeguard", "Safehouse", "Safeness", "Saffron", "Saga", "Sage", "Saggy", "Said", "Saint", "Sake", "Salad", "Salami", "Saline", "Salon", "Saloon", "Salsa", "Salt", "Salute", "Salvage", "Salvation", "Same", "Sample", "Sanction", "Sanctity", "Sandal", "Sandbag", "Sandbank", "Sandbar", "Sandblast", "Sandbox", "Sandlot", "Sandpaper", "Sandpit", "Sandstone", "Sandstorm", "Sandworm", "Sandy", "Sanitizer", "Sank", "Sappiness", "Sarcasm", "Sardine", "Sash", "Sasquatch", "Sassy", "Satchel", "Satin", "Satisfy", "Saturate", "Sauciness", "Saucy", "Sauna", "Savage", "Savanna", "Savor", "Saxophone", "Say", "Scabbed", "Scabby", "Scale", "Scallion", "Scallop", "Scam", "Scandal", "Scanner", "Scant", "Scapegoat", "Scarce", "Scarcity", "Scarecrow", "Scarf", "Scariness", "Scavenger", "Schedule", "Scheme", "Schnapps", "Scholar", "Science", "Scientist", "Scion", "Scoff", "Scone", "Scoop", "Scooter", "Scope", "Scorch", "Scorebook", "Scorecard", "Scoreless", "Scorer", "Scorn", "Scorpion", "Scotch", "Scoundrel", "Scrambler", "Scrap", "Scratch", "Scrawny", "Screen", "Scribe", "Scrimmage", "Script", "Scroll", "Scrooge", "Scrounger", "Scrubbed", "Scruffy", "Scrunch", "Scrutiny", "Scuba", "Scuff", "Sculptor", "Sculpture", "Scurvy", "Scuttle", "Seclusion", "Secrecy", "Sector", "Security", "Sedan", "Sedate", "Sedation", "Sediment", "Seduce", "Segment", "Seldom", "Selection", "Selector", "Self", "Seltzer", "Semester", "Semicolon", "Seminar", "Semisweet", "Senator", "Sensation", "Sepia", "Septum", "Sequel", "Sequence", "Sequester", "Sermon", "Serotonin", "Serpent", "Serve", "Sesame", "Setback", "Setup", "Sevenfold", "Seventeen", "Seventy", "Severity", "Shabby", "Shack", "Shadiness", "Shadow", "Shady", "Shaft", "Shakiness", "Shaky", "Shale", "Shallot", "Shallow", "Shame", "Shampoo", "Shamrock", "Shank", "Shanty", "Shape", "Share", "Sharpener", "Sharper", "Sharpie", "Sharply", "Sharpness", "Shawl", "Sheath", "Sheep", "Sheet", "Shelf", "Shell", "Shelter", "Shelve", "Sherry", "Shield", "Shifter", "Shiftless", "Shifty", "Shimmer", "Shimmy", "Shindig", "Shine", "Shingle", "Shininess", "Ship", "Shirt", "Shock", "Shone", "Shoplift", "Shopper", "Shoptalk", "Shore", "Shortage", "Shortcake", "Shortcut", "Shorter", "Shorthand", "Shortlist", "Shortness", "Shorty", "Shout", "Showbiz", "Showcase", "Showdown", "Shower", "Showgirl", "Showman", "Shown", "Showoff", "Showpiece", "Showplace", "Showroom", "Showy", "Shrank", "Shrapnel", "Shredder", "Shriek", "Shrill", "Shrimp", "Shrine", "Shrivel", "Shrubbery", "Shrug", "Shrunk", "Shudder", "Shuffle", "Shun", "Shush", "Shut", "Shy", "Sierra", "Siesta", "Sift", "Silencer", "Silent", "Silica", "Silicon", "Silk", "Silliness", "Silo", "Silt", "Silver", "Simile", "Simple", "Simply", "Sincerity", "Singer", "Single", "Sinister", "Sinless", "Sinner", "Sip", "Sister", "Sitcom", "Sitter", "Situation", "Sixfold", "Sixteen", "Sixtieth", "Sixtyfold", "Size", "Sizzle", "Skater", "Skedaddle", "Skeleton", "Sketch", "Skewer", "Skid", "Skier", "Skillet", "Skimmer", "Skincare", "Skinhead", "Skinless", "Skinny", "Skintight", "Skipper", "Skirmish", "Skirt", "Skittle", "Skydiver", "Skylight", "Skyline", "Skype", "Skyrocket", "Skyward", "Slab", "Slacker", "Slackness", "Slain", "Slam", "Slander", "Slang", "Slapstick", "Slate", "Slather", "Slaw", "Sleep", "Sleet", "Sleeve", "Slept", "Slicer", "Slick", "Slider", "Slideshow", "Slimness", "Slimy", "Slingshot", "Slinky", "Slip", "Slit", "Sliver", "Slobbery", "Slogan", "Slot", "Slouchy", "Sludge", "Slug", "Slum", "Slurp", "Slush", "Sly", "Small", "Smartness", "Smasher", "Smashup", "Smell", "Smile", "Smirk", "Smite", "Smock", "Smog", "Smokeless", "Smokiness", "Smoky", "Smolder", "Smother", "Smudge", "Smudgy", "Smuggler", "Smugness", "Snack", "Snap", "Snare", "Snazzy", "Sneak", "Sneer", "Sneeze", "Snide", "Sniff", "Snippet", "Snitch", "Snooper", "Snooze", "Snore", "Snorkel", "Snort", "Snout", "Snowbird", "Snowboard", "Snowbound", "Snowcap", "Snowdrift", "Snowdrop", "Snowfall", "Snowfield", "Snowflake", "Snowiness", "Snowless", "Snowman", "Snowplow", "Snowshoe", "Snowstorm", "Snowsuit", "Snowy", "Snub", "Snuff", "Snuggle", "Snugness", "Speak", "Spearhead", "Spearman", "Spearmint", "Spectacle", "Spectator", "Spectrum", "Speculate", "Speech", "Speed", "Spellbind", "Speller", "Spender", "Spending", "Spent", "Spew", "Sphinx", "Spider", "Spiffy", "Spill", "Spilt", "Spinach", "Spindle", "Spinner", "Spinout", "Spinster", "Spiny", "Spiritism", "Splashy", "Splatter", "Spleen", "Splendid", "Splendor", "Splice", "Splinter", "Splotchy", "Splurge", "Spoilage", "Spoiler", "Spokesman", "Sponge", "Spongy", "Sponsor", "Spoof", "Spooky", "Spool", "Spoon", "Spore", "Sporty", "Spotless", "Spotlight", "Spotter", "Spousal", "Spouse", "Spout", "Sprain", "Sprang", "Sprawl", "Spray", "Spree", "Sprig", "Spring", "Sprinkler", "Sprint", "Sprite", "Sprout", "Spruce", "Sprung", "Spry", "Spud", "Sputter", "Spyglass", "Squad", "Squall", "Squander", "Squash", "Squatter", "Squeak", "Squealer", "Squeegee", "Squeeze", "Squid", "Squiggle", "Squint", "Squire", "Squirt", "Squishier", "Squishy", "Stability", "Stack", "Stadium", "Staff", "Stage", "Stagnant", "Stagnate", "Stainless", "Stalemate", "Staleness", "Stallion", "Stammer", "Stamp", "Stand", "Stank", "Staple", "Starboard", "Starch", "Stardom", "Stardust", "Stargazer", "Stark", "Starless", "Starlet", "Starlight", "Starlit", "Starry", "Starship", "Starter", "Startle", "Startup", "Stash", "State", "Statue", "Stature", "Status", "Statute", "Statutory", "Staunch", "Steadfast", "Steam", "Steed", "Steersman", "Stegosaur", "Stem", "Stench", "Stencil", "Step", "Stereo", "Sterile", "Sterility", "Sterling", "Sternness", "Sternum", "Stew", "Stick", "Stiffen", "Stiffly", "Stiffness", "Stifle", "Stillness", "Stilt", "Stimulant", "Stimulate", "Stimulus", "Stinger", "Stingray", "Stingy", "Stinky", "Stipend", "Stipulate", "Stir", "Stitch", "Stock", "Stoic", "Stoke", "Stole", "Stomp", "Stonewall", "Stoneware", "Stonework", "Stony", "Stood", "Stooge", "Stool", "Stoop", "Stoplight", "Stoppage", "Stopper", "Stopwatch", "Storage", "Storeroom", "Storewide", "Storm", "Stout", "Stowaway", "Straddle", "Straggler", "Strainer", "Stranger", "Strangle", "Strategic", "Strategy", "Stratus", "Straw", "Stray", "Streak", "Stream", "Strength", "Strep", "Stress", "Stretch", "Strewn", "Strict", "Stride", "Strife", "Strike", "Strobe", "Strode", "Stroller", "Strongbox", "Strongman", "Struck", "Structure", "Strudel", "Struggle", "Strum", "Strung", "Strut", "Stubbed", "Stucco", "Stuck", "Student", "Studio", "Study", "Stuffed", "Stuffy", "Stump", "Stung", "Stunner", "Stunt", "Stupor", "Sturdy", "Stylist", "Stylus", "Subdivide", "Subfloor", "Subgroup", "Subheader", "Sublease", "Sublet", "Sublevel", "Sublime", "Submarine", "Submerge", "Submitter", "Subpanel", "Subpar", "Subplot", "Subprime", "Subscribe", "Subscript", "Subsector", "Subside", "Subsidy", "Subsoil", "Substance", "Subsystem", "Subtext", "Subtitle", "Subtract", "Subtype", "Suburb", "Subway", "Subwoofer", "Subzero", "Succulent", "Suction", "Sudoku", "Sufferer", "Suffice", "Suffix", "Suffocate", "Suffrage", "Sugar", "Suggest", "Suitcase", "Suitor", "Sulfate", "Sulfide", "Sulfite", "Sulfur", "Sulk", "Sullen", "Sulphate", "Sultry", "Superbowl", "Superglue", "Superhero", "Superjet", "Superman", "Supermom", "Supervise", "Supper", "Supplier", "Supply", "Support", "Supremacy", "Surcharge", "Sureness", "Surface", "Surfboard", "Surfer", "Surgery", "Surname", "Surpass", "Surplus", "Surprise", "Surreal", "Surrender", "Surrogate", "Surround", "Survey", "Survival", "Survivor", "Sushi", "Suspect", "Suspend", "Suspense", "Sustainer", "Swab", "Swagger", "Swampland", "Swan", "Swarm", "Sway", "Sweat", "Sweep", "Swept", "Swerve", "Swifter", "Swiftness", "Swimmer", "Swimsuit", "Swimwear", "Swinger", "Swipe", "Swirl", "Switch", "Swivel", "Swizzle", "Swoop", "Swore", "Swung", "Sycamore", "Sympathy", "Symphony", "Symptom", "Synapse", "Syndrome", "Synergy", "Synopsis", "Synthesis", "Syrup", "System", "T-shirt", "Tabasco", "Tabby", "Tableful", "Tablet", "Tableware", "Tabloid", "Tackiness", "Tackle", "Tacky", "Taco", "Tactful", "Tactile", "Tactless", "Tadpole", "Taekwondo", "Tag", "Talcum", "Talisman", "Tall", "Talon", "Tamale", "Tameness", "Tamer", "Tamper", "Tank", "Tannery", "Tantrum", "Tapeless", "Tapestry", "Tapioca", "Tarantula", "Target", "Tarmac", "Tarot", "Tartar", "Tartness", "Task", "Tassel", "Taste", "Tastiness", "Tasty", "Tattle", "Tattoo", "Taunt", "Tavern", "Thank", "Thaw", "Theater", "Thee", "Theft", "Theme", "Theology", "Thermal", "Thermos", "Thesaurus", "Thesis", "Thicken", "Thicket", "Thickness", "Thigh", "Thinner", "Thinness", "Thirsty", "Thirteen", "Thirty", "Thong", "Thorn", "Thrash", "Thread", "Threefold", "Thrift", "Thrill", "Thrive", "Throat", "Throng", "Throttle", "Throwaway", "Throwback", "Thrower", "Thud", "Thumb", "Tiara", "Tibia", "Tidal", "Tidbit", "Tidiness", "Tidy", "Tiger", "Tightness", "Tightrope", "Tightwad", "Tigress", "Tile", "Till", "Tilt", "Timid", "Timing", "Timothy", "Tinderbox", "Tinfoil", "Tingle", "Tinker", "Tinsel", "Tinsmith", "Tint", "Tinwork", "Tipoff", "Tipper", "Tiptop", "Tissue", "Trace", "Track", "Traction", "Tractor", "Trading", "Tradition", "Traffic", "Tragedy", "Trailside", "Train", "Traitor", "Trance", "Tranquil", "Transfer", "Transform", "Translate", "Transpire", "Transport", "Trapdoor", "Trapeze", "Trapezoid", "Trapper", "Trash", "Travel", "Traverse", "Travesty", "Tray", "Treachery", "Treadmill", "Treason", "Treat", "Tree", "Trekker", "Tremor", "Trench", "Trend", "Trespass", "Triage", "Trial", "Triangle", "Tribesman", "Tribune", "Tribute", "Trickery", "Trickle", "Trickster", "Tricky", "Tricolor", "Tricycle", "Trident", "Trifle", "Trillion", "Trilogy", "Trimester", "Trimmer", "Trimness", "Trinity", "Trio", "Tripod", "Triumph", "Trodden", "Trombone", "Trophy", "Trout", "Trowel", "Truce", "Truck", "Truffle", "Trump", "Trustee", "Trustful", "Trustless", "Truth", "Tubby", "Tubeless", "Tubular", "Tug", "Tuition", "Tulip", "Tumble", "Tummy", "Turban", "Turbine", "Turbofan", "Turbojet", "Turbulent", "Turf", "Turkey", "Turmoil", "Turret", "Turtle", "Tusk", "Tutor", "Tutu", "Tux", "Tweak", "Tweed", "Tweet", "Twelve", "Twentieth", "Twenty", "Twerp", "Twiddle", "Twig", "Twilight", "Twine", "Twirl", "Twister", "Twisty", "Twitch", "Twitter", "Tycoon", "Tyke", "Udder", "Ultimate", "Ultimatum", "Ultra", "Umbrella", "Umpire", "Unafraid", "Unaware", "Unbalance", "Unbend", "Unbent", "Unblock", "Unbridle", "Unbundle", "Unbutton", "Uncanny", "Uncertain", "Unchain", "Uncheck", "Uncivil", "Unclad", "Unclasp", "Uncle", "Unclip", "Uncloak", "Unclog", "Uncommon", "Uncork", "Uncouple", "Uncouth", "Uncover", "Uncut", "Undead", "Underage", "Underarm", "Undercoat", "Undercook", "Undercut", "Underdog", "Underdone", "Underfed", "Underfeed", "Underfoot", "Undergo", "Undergrad", "Underhand", "Underline", "Undermine", "Undermost", "Underpaid", "Underpass", "Underpay", "Underrate", "Undertone", "Undertook", "Undertow", "Underuse", "Underwent", "Underwire", "Undone", "Undress", "Unearth", "Unease", "Unfair", "Unfold", "Unfreeze", "Unglue", "Unheard", "Unhidden", "Unhinge", "Unholy", "Unhook", "Unicycle", "Unifier", "Uninstall", "Unison", "Unit", "Universal", "Universe", "Unkempt", "Unlatch", "Unleash", "Unlit", "Unloader", "Unmade", "Unnerve", "Unpack", "Unpaid", "Unplug", "Unquote", "Unread", "Unreal", "Unripe", "Unroll", "Unruly", "Unsaddle", "Unsaid", "Unsavory", "Unscrew", "Unseen", "Unselect", "Unsent", "Unshackle", "Unsheathe", "Unsnap", "Unsold", "Unstaffed", "Unstitch", "Unstuck", "Unstuffed", "Unsubtle", "Unsure", "Unthread", "Untidy", "Untie", "Untold", "Untrue", "Untruth", "Untwist", "Unwelcome", "Unwell", "Unwieldy", "Unworthy", "Unwound", "Unzip", "Upbeat", "Upchuck", "Upcountry", "Update", "Upfront", "Upgrade", "Upheaval", "Upheld", "Uphill", "Uphold", "Upload", "Upright", "Upriver", "Uproar", "Uproot", "Upscale", "Upstage", "Upstart", "Upstate", "Upstream", "Upstroke", "Uptight", "Uranium", "Urban", "Urethane", "Urgency", "Urgent", "Urologist", "Urology", "Usage", "User", "Usher", "Utensil", "Utility", "Utmost", "Utopia", "Utter", "Vacancy", "Vacant", "Vacate", "Vacation", "Vagabond", "Vagrancy", "Vagueness", "Valiant", "Valium", "Value", "Vanilla", "Vanity", "Vantage", "Vaporizer", "Variety", "Varmint", "Varsity", "Vaseline", "Vastness", "Veal", "Veggie", "Velcro", "Velocity", "Velvet", "Vendetta", "Vendor", "Ventricle", "Venture", "Venue", "Venus", "Verdict", "Verse", "Version", "Versus", "Vertebrae", "Vertigo", "Vessel", "Veteran", "Veto", "Viability", "Vicinity", "Victory", "Video", "Viewer", "Viewless", "Viewpoint", "Village", "Villain", "Vindicate", "Vineyard", "Vintage", "Violate", "Violation", "Violator", "Violet", "Violin", "Viper", "Virus", "Viscosity", "Viselike", "Vision", "Visitor", "Visor", "Vista", "Vitality", "Vividness", "Vixen", "Vocalist", "Vocation", "Voice", "Void", "Volatile", "Voltage", "Voter", "Voucher", "Vowel", "Voyage", "Wackiness", "Wad", "Wafer", "Waffle", "Wager", "Waggle", "Wagon", "Walk", "Walmart", "Walnut", "Walrus", "Waltz", "Wand", "Wannabe", "Wasabi", "Washbasin", "Washboard", "Washbowl", "Washday", "Washroom", "Washstand", "Washtub", "Wasp", "Watch", "Water", "Waviness", "Wharf", "Wheat", "Whiff", "Whinny", "Whiny", "Whoopee", "Wick", "Widow", "Width", "Wielder", "Wife", "Wifi", "Wildcard", "Wildcat", "Wilder", "Wildfire", "Wildfowl", "Wildland", "Wildlife", "Wildness", "Wilt", "Wimp", "Wince", "Winner", "Winter", "Wipe", "Wireless", "Wiry", "Wisdom", "Wise", "Wispy", "Wizard", "Wok", "Wolf", "Wolverine", "Womanhood", "Womanless", "Womb", "Woof", "Wool", "Woozy", "Word", "Work", "Worry", "Wow", "Wrangle", "Wrath", "Wreath", "Wreckage", "Wrecker", "Wrench", "Wriggle", "Wrinkle", "Wrist", "Wrongdoer", "Wrongness", "Xbox", "Xerox", "Yahoo", "Yam", "Yard", "Yarn", "Yeah", "Yearbook", "Yeast", "Yelp", "Yen", "Yesterday", "Yield", "Yin", "Yippee", "Yo-yo", "Yodel", "Yoga", "Yogurt", "Yonder", "Yoyo", "Yummy", "Zap", "Zebra", "Zen", "Zeppelin", "Zero", "Zesty", "Zipfile", "Zit", "Zodiac", "Zombie", "Zone", "Zookeeper", "Zoologist", "Zoology", "Zoom"]; ================================================ FILE: src/data/effWordlist.js ================================================ // From https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases // Used by github.com/cryptag/cryptag/share const effWordlist = [ "Aardvark", "Abandoned", "Abbreviate", "Abdomen", "Abhorrence", "Abiding", "Abnormal", "Abrasion", "Absorbing", "Abundant", "Abyss", "Academy", "Accountant", "Acetone", "Achiness", "Acid", "Acoustics", "Acquire", "Acrobat", "Actress", "Acuteness", "Aerosol", "Aesthetic", "Affidavit", "Afloat", "Afraid", "Aftershave", "Again", "Agency", "Aggressor", "Aghast", "Agitate", "Agnostic", "Agonizing", "Agreeing", "Aidless", "Aimlessly", "Ajar", "Alarmclock", "Albatross", "Alchemy", "Alfalfa", "Algae", "Aliens", "Alkaline", "Almanac", "Alongside", "Alphabet", "Already", "Also", "Altitude", "Aluminum", "Always", "Amazingly", "Ambulance", "Amendment", "Amiable", "Ammunition", "Amnesty", "Amoeba", "Amplifier", "Amuser", "Anagram", "Anchor", "Android", "Anesthesia", "Angelfish", "Animal", "Anklet", "Announcer", "Anonymous", "Answer", "Antelope", "Anxiety", "Anyplace", "Aorta", "Apartment", "Apnea", "Apostrophe", "Apple", "Apricot", "Aquamarine", "Arachnid", "Arbitrate", "Ardently", "Arena", "Argument", "Aristocrat", "Armchair", "Aromatic", "Arrowhead", "Arsonist", "Artichoke", "Asbestos", "Ascend", "Aseptic", "Ashamed", "Asinine", "Asleep", "Asocial", "Asparagus", "Astronaut", "Asymmetric", "Atlas", "Atmosphere", "Atom", "Atrocious", "Attic", "Atypical", "Auctioneer", "Auditorium", "Augmented", "Auspicious", "Automobile", "Auxiliary", "Avalanche", "Avenue", "Aviator", "Avocado", "Awareness", "Awhile", "Awkward", "Awning", "Awoke", "Axially", "Azalea", "Babbling", "Backpack", "Badass", "Bagpipe", "Bakery", "Balancing", "Bamboo", "Banana", "Barracuda", "Basket", "Bathrobe", "Bazooka", "Blade", "Blender", "Blimp", "Blouse", "Blurred", "Boatyard", "Bobcat", "Body", "Bogusness", "Bohemian", "Boiler", "Bonnet", "Boots", "Borough", "Bossiness", "Bottle", "Bouquet", "Boxlike", "Breath", "Briefcase", "Broom", "Brushes", "Bubblegum", "Buckle", "Buddhist", "Buffalo", "Bullfrog", "Bunny", "Busboy", "Buzzard", "Cabin", "Cactus", "Cadillac", "Cafeteria", "Cage", "Cahoots", "Cajoling", "Cakewalk", "Calculator", "Camera", "Canister", "Capsule", "Carrot", "Cashew", "Cathedral", "Caucasian", "Caviar", "Ceasefire", "Cedar", "Celery", "Cement", "Census", "Ceramics", "Cesspool", "Chalkboard", "Cheesecake", "Chimney", "Chlorine", "Chopsticks", "Chrome", "Chute", "Cilantro", "Cinnamon", "Circle", "Cityscape", "Civilian", "Clay", "Clergyman", "Clipboard", "Clock", "Clubhouse", "Coathanger", "Cobweb", "Coconut", "Codeword", "Coexistent", "Coffeecake", "Cognitive", "Cohabitate", "Collarbone", "Computer", "Confetti", "Copier", "Cornea", "Cosmetics", "Cotton", "Couch", "Coverless", "Coyote", "Coziness", "Crawfish", "Crewmember", "Crib", "Croissant", "Crumble", "Crystal", "Cubical", "Cucumber", "Cuddly", "Cufflink", "Cuisine", "Culprit", "Cup", "Curry", "Cushion", "Cuticle", "Cybernetic", "Cyclist", "Cylinder", "Cymbal", "Cynicism", "Cypress", "Cytoplasm", "Dachshund", "Daffodil", "Dagger", "Dairy", "Dalmatian", "Dandelion", "Dartboard", "Dastardly", "Datebook", "Daughter", "Dawn", "Daytime", "Dazzler", "Dealer", "Debris", "Decal", "Dedicate", "Deepness", "Defrost", "Degree", "Dehydrator", "Deliverer", "Democrat", "Dentist", "Deodorant", "Depot", "Deranged", "Desktop", "Detergent", "Device", "Dexterity", "Diamond", "Dibs", "Dictionary", "Diffuser", "Digit", "Dilated", "Dimple", "Dinnerware", "Dioxide", "Diploma", "Directory", "Dishcloth", "Ditto", "Dividers", "Dizziness", "Doctor", "Dodge", "Doll", "Dominoes", "Donut", "Doorstep", "Dorsal", "Double", "Downstairs", "Dozed", "Drainpipe", "Dresser", "Driftwood", "Droppings", "Drum", "Dryer", "Dubiously", "Duckling", "Duffel", "Dugout", "Dumpster", "Duplex", "Durable", "Dustpan", "Dutiful", "Duvet", "Dwarfism", "Dwelling", "Dwindling", "Dynamite", "Dyslexia", "Eagerness", "Earlobe", "Easel", "Eavesdrop", "Ebook", "Eccentric", "Echoless", "Eclipse", "Ecosystem", "Ecstasy", "Edged", "Editor", "Educator", "Eelworm", "Eerie", "Effects", "Eggnog", "Egomaniac", "Ejection", "Elastic", "Elbow", "Elderly", "Elephant", "Elfishly", "Eliminator", "Elk", "Elliptical", "Elongated", "Elsewhere", "Elusive", "Elves", "Emancipate", "Embroidery", "Emcee", "Emerald", "Emission", "Emoticon", "Emperor", "Emulate", "Enactment", "Enchilada", "Endorphin", "Energy", "Enforcer", "Engine", "Enhance", "Enigmatic", "Enjoyably", "Enlarged", "Enormous", "Enquirer", "Enrollment", "Ensemble", "Entryway", "Enunciate", "Envoy", "Enzyme", "Epidemic", "Equipment", "Erasable", "Ergonomic", "Erratic", "Eruption", "Escalator", "Eskimo", "Esophagus", "Espresso", "Essay", "Estrogen", "Etching", "Eternal", "Ethics", "Etiquette", "Eucalyptus", "Eulogy", "Euphemism", "Euthanize", "Evacuation", "Evergreen", "Evidence", "Evolution", "Exam", "Excerpt", "Exerciser", "Exfoliate", "Exhale", "Exist", "Exorcist", "Explode", "Exquisite", "Exterior", "Exuberant", "Fabric", "Factory", "Faded", "Failsafe", "Falcon", "Family", "Fanfare", "Fasten", "Faucet", "Favorite", "Feasibly", "February", "Federal", "Feedback", "Feigned", "Feline", "Femur", "Fence", "Ferret", "Festival", "Fettuccine", "Feudalist", "Feverish", "Fiberglass", "Fictitious", "Fiddle", "Figurine", "Fillet", "Finalist", "Fiscally", "Fixture", "Flashlight", "Fleshiness", "Flight", "Florist", "Flypaper", "Foamless", "Focus", "Foggy", "Folksong", "Fondue", "Footpath", "Fossil", "Fountain", "Fox", "Fragment", "Freeway", "Fridge", "Frosting", "Fruit", "Fryingpan", "Gadget", "Gainfully", "Gallstone", "Gamekeeper", "Gangway", "Garlic", "Gaslight", "Gathering", "Gauntlet", "Gearbox", "Gecko", "Gem", "Generator", "Geographer", "Gerbil", "Gesture", "Getaway", "Geyser", "Ghoulishly", "Gibberish", "Giddiness", "Giftshop", "Gigabyte", "Gimmick", "Giraffe", "Giveaway", "Gizmo", "Glasses", "Gleeful", "Glisten", "Glove", "Glucose", "Glycerin", "Gnarly", "Gnomish", "Goatskin", "Goggles", "Goldfish", "Gong", "Gooey", "Gorgeous", "Gosling", "Gothic", "Gourmet", "Governor", "Grape", "Greyhound", "Grill", "Groundhog", "Grumbling", "Guacamole", "Guerrilla", "Guitar", "Gullible", "Gumdrop", "Gurgling", "Gusto", "Gutless", "Gymnast", "Gynecology", "Gyration", "Habitat", "Hacking", "Haggard", "Haiku", "Halogen", "Hamburger", "Handgun", "Happiness", "Hardhat", "Hastily", "Hatchling", "Haughty", "Hazelnut", "Headband", "Hedgehog", "Hefty", "Heinously", "Helmet", "Hemoglobin", "Henceforth", "Herbs", "Hesitation", "Hexagon", "Hubcap", "Huddling", "Huff", "Hugeness", "Hullabaloo", "Human", "Hunter", "Hurricane", "Hushing", "Hyacinth", "Hybrid", "Hydrant", "Hygienist", "Hypnotist", "Ibuprofen", "Icepack", "Icing", "Iconic", "Identical", "Idiocy", "Idly", "Igloo", "Ignition", "Iguana", "Illuminate", "Imaging", "Imbecile", "Imitator", "Immigrant", "Imprint", "Iodine", "Ionosphere", "Ipad", "Iphone", "Iridescent", "Irksome", "Iron", "Irrigation", "Island", "Isotope", "Issueless", "Italicize", "Itemizer", "Itinerary", "Itunes", "Ivory", "Jabbering", "Jackrabbit", "Jaguar", "Jailhouse", "Jalapeno", "Jamboree", "Janitor", "Jarring", "Jasmine", "Jaundice", "Jawbreaker", "Jaywalker", "Jazz", "Jealous", "Jeep", "Jelly", "Jeopardize", "Jersey", "Jetski", "Jezebel", "Jiffy", "Jigsaw", "Jingling", "Jobholder", "Jockstrap", "Jogging", "John", "Joinable", "Jokingly", "Journal", "Jovial", "Joystick", "Jubilant", "Judiciary", "Juggle", "Juice", "Jujitsu", "Jukebox", "Jumpiness", "Junkyard", "Juror", "Justifying", "Juvenile", "Kabob", "Kamikaze", "Kangaroo", "Karate", "Kayak", "Keepsake", "Kennel", "Kerosene", "Ketchup", "Khaki", "Kickstand", "Kilogram", "Kimono", "Kingdom", "Kiosk", "Kissing", "Kite", "Kleenex", "Knapsack", "Kneecap", "Knickers", "Koala", "Krypton", "Laboratory", "Ladder", "Lakefront", "Lantern", "Laptop", "Laryngitis", "Lasagna", "Latch", "Laundry", "Lavender", "Laxative", "Lazybones", "Lecturer", "Leftover", "Leggings", "Leisure", "Lemon", "Length", "Leopard", "Leprechaun", "Lettuce", "Leukemia", "Levers", "Lewdness", "Liability", "Library", "Licorice", "Lifeboat", "Lightbulb", "Likewise", "Lilac", "Limousine", "Lint", "Lioness", "Lipstick", "Liquid", "Listless", "Litter", "Liverwurst", "Lizard", "Llama", "Luau", "Lubricant", "Lucidity", "Ludicrous", "Luggage", "Lukewarm", "Lullaby", "Lumberjack", "Lunchbox", "Luridness", "Luscious", "Luxurious", "Lyrics", "Macaroni", "Maestro", "Magazine", "Mahogany", "Maimed", "Majority", "Makeover", "Malformed", "Mammal", "Mango", "Mapmaker", "Marbles", "Massager", "Matchstick", "Maverick", "Maximum", "Mayonnaise", "Moaning", "Mobilize", "Moccasin", "Modify", "Moisture", "Molecule", "Momentum", "Monastery", "Moonshine", "Mortuary", "Mosquito", "Motorcycle", "Mousetrap", "Movie", "Mower", "Mozzarella", "Muckiness", "Mudflow", "Mugshot", "Mule", "Mummy", "Mundane", "Muppet", "Mural", "Mustard", "Mutation", "Myriad", "Myspace", "Myth", "Nail", "Namesake", "Nanosecond", "Napkin", "Narrator", "Nastiness", "Natives", "Nautically", "Navigate", "Nearest", "Nebula", "Nectar", "Nefarious", "Negotiator", "Neither", "Nemesis", "Neoliberal", "Nephew", "Nervously", "Nest", "Netting", "Neuron", "Nevermore", "Nextdoor", "Nicotine", "Niece", "Nimbleness", "Nintendo", "Nirvana", "Nuclear", "Nugget", "Nuisance", "Nullify", "Numbing", "Nuptials", "Nursery", "Nutcracker", "Nylon", "Oasis", "Oat", "Obediently", "Obituary", "Object", "Obliterate", "Obnoxious", "Observer", "Obtain", "Obvious", "Occupation", "Oceanic", "Octopus", "Ocular", "Office", "Oftentimes", "Oiliness", "Ointment", "Older", "Olympics", "Omissible", "Omnivorous", "Oncoming", "Onion", "Onlooker", "Onstage", "Onward", "Onyx", "Oomph", "Opaquely", "Opera", "Opium", "Opossum", "Opponent", "Optical", "Opulently", "Oscillator", "Osmosis", "Ostrich", "Otherwise", "Ought", "Outhouse", "Ovation", "Oven", "Owlish", "Oxford", "Oxidize", "Oxygen", "Oyster", "Ozone", "Pacemaker", "Padlock", "Pageant", "Pajamas", "Palm", "Pamphlet", "Pantyhose", "Paprika", "Parakeet", "Passport", "Patio", "Pauper", "Pavement", "Payphone", "Pebble", "Peculiarly", "Pedometer", "Pegboard", "Pelican", "Penguin", "Peony", "Pepperoni", "Peroxide", "Pesticide", "Petroleum", "Pewter", "Pharmacy", "Pheasant", "Phonebook", "Phrasing", "Physician", "Plank", "Pledge", "Plotted", "Plug", "Plywood", "Pneumonia", "Podiatrist", "Poetic", "Pogo", "Poison", "Poking", "Policeman", "Poncho", "Popcorn", "Porcupine", "Postcard", "Poultry", "Powerboat", "Prairie", "Pretzel", "Princess", "Propeller", "Prune", "Pry", "Pseudo", "Psychopath", "Publisher", "Pucker", "Pueblo", "Pulley", "Pumpkin", "Punchbowl", "Puppy", "Purse", "Pushup", "Putt", "Puzzle", "Pyramid", "Python", "Quarters", "Quesadilla", "Quilt", "Quote", "Racoon", "Radish", "Ragweed", "Railroad", "Rampantly", "Rancidity", "Rarity", "Raspberry", "Ravishing", "Rearrange", "Rebuilt", "Receipt", "Reentry", "Refinery", "Register", "Rehydrate", "Reimburse", "Rejoicing", "Rekindle", "Relic", "Remote", "Renovator", "Reopen", "Reporter", "Request", "Rerun", "Reservoir", "Retriever", "Reunion", "Revolver", "Rewrite", "Rhapsody", "Rhetoric", "Rhino", "Rhubarb", "Rhyme", "Ribbon", "Riches", "Ridden", "Rigidness", "Rimmed", "Riptide", "Riskily", "Ritzy", "Riverboat", "Roamer", "Robe", "Rocket", "Romancer", "Ropelike", "Rotisserie", "Roundtable", "Royal", "Rubber", "Rudderless", "Rugby", "Ruined", "Rulebook", "Rummage", "Running", "Rupture", "Rustproof", "Sabotage", "Sacrifice", "Saddlebag", "Saffron", "Sainthood", "Saltshaker", "Samurai", "Sandworm", "Sapphire", "Sardine", "Sassy", "Satchel", "Sauna", "Savage", "Saxophone", "Scarf", "Scenario", "Schoolbook", "Scientist", "Scooter", "Scrapbook", "Sculpture", "Scythe", "Secretary", "Sedative", "Segregator", "Seismology", "Selected", "Semicolon", "Senator", "Septum", "Sequence", "Serpent", "Sesame", "Settler", "Severely", "Shack", "Shelf", "Shirt", "Shovel", "Shrimp", "Shuttle", "Shyness", "Siamese", "Sibling", "Siesta", "Silicon", "Simmering", "Singles", "Sisterhood", "Sitcom", "Sixfold", "Sizable", "Skateboard", "Skeleton", "Skies", "Skulk", "Skylight", "Slapping", "Sled", "Slingshot", "Sloth", "Slumbering", "Smartphone", "Smelliness", "Smitten", "Smokestack", "Smudge", "Snapshot", "Sneezing", "Sniff", "Snowsuit", "Snugness", "Speakers", "Sphinx", "Spider", "Splashing", "Sponge", "Sprout", "Spur", "Spyglass", "Squirrel", "Statue", "Steamboat", "Stingray", "Stopwatch", "Strawberry", "Student", "Stylus", "Suave", "Subway", "Suction", "Suds", "Suffocate", "Sugar", "Suitcase", "Sulphur", "Superstore", "Surfer", "Sushi", "Swan", "Sweatshirt", "Swimwear", "Sword", "Sycamore", "Syllable", "Symphony", "Synagogue", "Syringes", "Systemize", "Tablespoon", "Taco", "Tadpole", "Taekwondo", "Tagalong", "Takeout", "Tallness", "Tamale", "Tanned", "Tapestry", "Tarantula", "Tastebud", "Tattoo", "Tavern", "Thaw", "Theater", "Thimble", "Thorn", "Throat", "Thumb", "Thwarting", "Tiara", "Tidbit", "Tiebreaker", "Tiger", "Timid", "Tinsel", "Tiptoeing", "Tirade", "Tissue", "Tractor", "Tree", "Tripod", "Trousers", "Trucks", "Tryout", "Tubeless", "Tuesday", "Tugboat", "Tulip", "Tumbleweed", "Tupperware", "Turtle", "Tusk", "Tutorial", "Tuxedo", "Tweezers", "Twins", "Tyrannical", "Ultrasound", "Umbrella", "Umpire", "Unarmored", "Unbuttoned", "Uncle", "Underwear", "Unevenness", "Unflavored", "Ungloved", "Unhinge", "Unicycle", "Unjustly", "Unknown", "Unlocking", "Unmarked", "Unnoticed", "Unopened", "Unpaved", "Unquenched", "Unroll", "Unscrewing", "Untied", "Unusual", "Unveiled", "Unwrinkled", "Unyielding", "Unzip", "Upbeat", "Upcountry", "Update", "Upfront", "Upgrade", "Upholstery", "Upkeep", "Upload", "Uppercut", "Upright", "Upstairs", "Uptown", "Upwind", "Uranium", "Urban", "Urchin", "Urethane", "Urgent", "Urologist", "Username", "Usher", "Utensil", "Utility", "Utmost", "Utopia", "Utterance", "Vacuum", "Vagrancy", "Valuables", "Vanquished", "Vaporizer", "Varied", "Vaseline", "Vegetable", "Vehicle", "Velcro", "Vendor", "Vertebrae", "Vestibule", "Veteran", "Vexingly", "Vicinity", "Videogame", "Viewfinder", "Vigilante", "Village", "Vinegar", "Violin", "Viperfish", "Virus", "Visor", "Vitamins", "Vivacious", "Vixen", "Vocalist", "Vogue", "Voicemail", "Volleyball", "Voucher", "Voyage", "Vulnerable", "Waffle", "Wagon", "Wakeup", "Walrus", "Wanderer", "Wasp", "Water", "Waving", "Wheat", "Whisper", "Wholesaler", "Wick", "Widow", "Wielder", "Wifeless", "Wikipedia", "Wildcat", "Windmill", "Wipeout", "Wired", "Wishbone", "Wizardry", "Wobbliness", "Wolverine", "Womb", "Woolworker", "Workbasket", "Wound", "Wrangle", "Wreckage", "Wristwatch", "Wrongdoing", "Xerox", "Xylophone", "Yacht", "Yahoo", "Yard", "Yearbook", "Yesterday", "Yiddish", "Yield", "Yo-yo", "Yodel", "Yogurt", "Yuppie", "Zealot", "Zebra", "Zeppelin", "Zestfully", "Zigzagged", "Zillion", "Zipping", "Zirconium", "Zodiac", "Zombie", "Zookeeper", "Zucchini", ]; export default effWordlist; ================================================ FILE: src/data/minishare.js ================================================ const crypto = window.crypto || window.msCrypto; import effWordlist from './effWordlist'; // Excluding max // // Mostly from https://github.com/chancejs/chancejs/issues/232#issuecomment-182500222 function randomIntsInRange(min, max, numInts){ let rand = new Uint32Array(numInts); crypto.getRandomValues(rand); let ints = new Uint32Array(numInts); var zeroToOne = 0.0; for(let i = 0; i < numInts; i++){ zeroToOne = rand[i]/(0xffffffff + 1); // TODO: Do security audit of this for timing attacks ints[i] = Math.floor(zeroToOne * (max - min)) + min; } return ints; } export function genPassphrase(numWords=25){ let words = new Array(numWords); let ndxs = randomIntsInRange(0, effWordlist.length, numWords); for(let i = 0; i < numWords; i++){ words[i] = effWordlist[ndxs[i]]; } return words.join(""); } ================================================ FILE: src/data/username.js ================================================ import { nouns, adjectives } from './constants'; function getRandomAdjective(){ return adjectives[Math.floor(Math.random()*adjectives.length)]; } function getRandomNoun(){ return nouns[Math.floor(Math.random()*nouns.length)]; } export function generateRandomUsername(){ return getRandomAdjective() + getRandomNoun(); } ================================================ FILE: src/index-template.ejs ================================================ <%= htmlWebpackPlugin.options.title %>
    ================================================ FILE: src/index.js ================================================ import React from 'react'; import { createRoot } from 'react-dom/client'; import { Provider } from 'react-redux'; import { createStore, compose, applyMiddleware } from 'redux'; import { createEpicMiddleware } from 'redux-observable'; import rootReducer from './store/reducers'; import rootEpic from './store/epics'; import 'bootstrap/dist/css/bootstrap.css'; import './utils/vh_fix'; import './static/sass/main.scss'; import './static/fonts/Lato.ttf'; import './static/audio/notification_gertz.wav'; import './utils/detect_browser'; import './utils/origin_polyfill'; import App from './components/App'; const composeEnhancers = typeof window === 'object' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({ // Specify extension’s options like name, actionsBlacklist, actionsCreators, serialize... }) : compose; const epicMiddleware = createEpicMiddleware(rootEpic); const enhancer = composeEnhancers( applyMiddleware(epicMiddleware) ); const store = createStore(rootReducer, enhancer); if (module.hot) { module.hot.accept('./reducers', () => store.replaceReducer(require('./reducers')) ); } const root = createRoot( document.getElementById('root') ); root.render( ); ================================================ FILE: src/static/assets.json ================================================ { "stylesheets": [ "static/lib/bootstrap/dist/css/bootstrap.css", "static/css/main.css" ], "scripts": [ "static/lib/jquery/dist/jquery.js", "static/lib/bootstrap/dist/js/bootstrap.js", "static/lib/fetch/fetch.js", "static/js/crypto/blake2s.js", "static/js/crypto/nacl.js", "static/js/crypto/nacl-stream.js", "static/js/crypto/scrypt.js", "static/js/base58.js", "static/js/miniLock.js", "static/js/ui.js", "build/app.js" ] } ================================================ FILE: src/static/css/Lato.css ================================================ @font-face { font-family: 'Lato'; font-style: normal; font-weight: 400; src: local("Lato Regular"), local("Lato-Regular"), url(Lato.ttf) format("truetype"); } ================================================ FILE: src/static/js/emoji-fixed.js ================================================ "use strict"; ;(function() { var root = this; var previous_emoji = root.EmojiConvertor; /** * @global * @namespace */ var emoji = function(){ var self = this; /** * The set of images to use for graphical emoji. * * @memberof emoji * @type {string} */ self.img_set = 'apple'; /** * Configuration details for different image sets. This includes a path to a directory containing the * individual images (`path`) and a URL to sprite sheets (`sheet`). All of these images can be found * in the [emoji-data repository]{@link https://github.com/iamcal/emoji-data}. Using a CDN for these * is not a bad idea. * * @memberof emoji * @type {object} */ self.img_sets = { 'apple' : {'path' : '/emoji-data/img-apple-64/', 'sheet' : '/emoji-data/sheet_apple_64.png', 'sheet_size' : 64, 'mask' : 1}, 'google' : {'path' : '/emoji-data/img-google-64/', 'sheet' : '/emoji-data/sheet_google_64.png', 'sheet_size' : 64, 'mask' : 2}, 'twitter' : {'path' : '/emoji-data/img-twitter-64/', 'sheet' : '/emoji-data/sheet_twitter_64.png', 'sheet_size' : 64, 'mask' : 4}, 'facebook' : {'path' : '/emoji-data/img-facebook-64/', 'sheet' : '/emoji-data/sheet_facebook_64.png', 'sheet_size' : 64, 'mask' : 8}, 'messenger' : {'path' : '/emoji-data/img-messenger-64/', 'sheet' : '/emoji-data/sheet_messenger_64.png', 'sheet_size' : 64, 'mask' : 16}, }; /** * Use a CSS class instead of specifying a sprite or background image for * the span representing the emoticon. This requires a CSS sheet with * emoticon data-uris. * * @memberof emoji * @type bool * @todo document how to build the CSS stylesheet self requires. */ self.use_css_imgs = false; /** * Instead of replacing emoticons with the appropriate representations, * replace them with their colon string representation. * @memberof emoji * @type bool */ self.colons_mode = false; self.text_mode = false; /** * If true, sets the "title" property on the span or image that gets * inserted for the emoticon. * @memberof emoji * @type bool */ self.include_title = false; /** * If true, sets the text of the span or image that gets inserted for the * emoticon. * @memberof emoji * @type bool */ self.include_text = false; /** * If the platform supports native emoticons, use those instead * of the fallbacks. * @memberof emoji * @type bool */ self.allow_native = true; /** * Wrap native with a to allow styling * @memberof emoji * @type bool */ self.wrap_native = false; /** * Set to true to use CSS sprites instead of individual images on * platforms that support it. * * @memberof emoji * @type bool */ self.use_sheet = false; /** * * Set to true to avoid black & white native Windows emoji being used. * * @memberof emoji * @type bool */ self.avoid_ms_emoji = true; /** * * Set to true to allow :CAPITALIZATION: * * @memberof emoji * @type bool */ self.allow_caps = false; /** * * Suffix to allow for individual image cache busting * * @memberof emoji * @type string */ self.img_suffix = ''; // Keeps track of what has been initialized. /** @private */ self.inits = {}; self.map = {}; // discover the environment settings self.init_env(); return self; }; emoji.prototype.noConflict = function(){ root.EmojiConvertor = previous_emoji; return emoji; }; /** * @memberof emoji * @param {string} str A string potentially containing ascii emoticons * (ie. `:)`) * * @returns {string} A new string with all emoticons in `str` * replaced by a representatation that's supported by the current * environtment. */ emoji.prototype.replace_emoticons = function(str){ var self = this; var colonized = self.replace_emoticons_with_colons(str); return self.replace_colons(colonized); }; /** * @memberof emoji * @param {string} str A string potentially containing ascii emoticons * (ie. `:)`) * * @returns {string} A new string with all emoticons in `str` * replaced by their colon string representations (ie. `:smile:`) */ emoji.prototype.replace_emoticons_with_colons = function(str){ var self = this; self.init_emoticons(); var _prev_offset = 0; var emoticons_with_parens = []; var str_replaced = str.replace(self.rx_emoticons, function(m, $1, emoticon, offset){ var prev_offset = _prev_offset; _prev_offset = offset + m.length; var has_open_paren = emoticon.indexOf('(') !== -1; var has_close_paren = emoticon.indexOf(')') !== -1; /* * Track paren-having emoticons for fixing later */ if ((has_open_paren || has_close_paren) && emoticons_with_parens.indexOf(emoticon) == -1) { emoticons_with_parens.push(emoticon); } /* * Look for preceding open paren for emoticons that contain a close paren * This prevents matching "8)" inside "(around 7 - 8)" */ if (has_close_paren && !has_open_paren) { var piece = str.substring(prev_offset, offset); if (piece.indexOf('(') !== -1 && piece.indexOf(')') === -1) return m; } /* * See if we're in a numbered list * This prevents matching "8)" inside "7) foo\n8) bar" */ if (m === '\n8)') { var before_match = str.substring(0, offset); if (/\n?(6\)|7\))/.test(before_match)) return m; } var val = self.data[self.map.emoticons[emoticon]][3][0]; return val ? $1+':'+val+':' : m; }); /* * Come back and fix emoticons we ignored because they were inside parens. * It's useful to do self at the end so we don't get tripped up by other, * normal emoticons */ if (emoticons_with_parens.length) { var escaped_emoticons = emoticons_with_parens.map(self.escape_rx); var parenthetical_rx = new RegExp('(\\(.+)('+escaped_emoticons.join('|')+')(.+\\))', 'g'); str_replaced = str_replaced.replace(parenthetical_rx, function(m, $1, emoticon, $2) { var val = self.data[self.map.emoticons[emoticon]][3][0]; return val ? $1+':'+val+':'+$2 : m; }); } return str_replaced; }; /** * @memberof emoji * @param {string} str A string potentially containing colon string * representations of emoticons (ie. `:smile:`) * * @returns {string} A new string with all colon string emoticons replaced * with the appropriate representation. */ emoji.prototype.replace_colons = function(str){ var self = this; self.init_colons(); return str.replace(self.rx_colons, function(m){ var idx = m.substr(1, m.length-2); if (self.allow_caps) idx = idx.toLowerCase(); // special case - an emoji with a skintone modified if (idx.indexOf('::skin-tone-') > -1){ var skin_tone = idx.substr(-1, 1); var skin_idx = 'skin-tone-'+skin_tone; var skin_val = self.map.colons[skin_idx]; idx = idx.substr(0, idx.length - 13); var val = self.map.colons[idx]; if (val){ return self.replacement(val, idx, ':', { 'idx' : skin_val, 'actual' : skin_idx, 'wrapper' : ':' }); }else{ return ':' + idx + ':' + self.replacement(skin_val, skin_idx, ':'); } }else{ var val = self.map.colons[idx]; return val ? self.replacement(val, idx, ':') : m; } }); }; /** * @memberof emoji * @param {string} str A string potentially containing unified unicode * emoticons. (ie. 😄) * * @returns {string} A new string with all unicode emoticons replaced with * the appropriate representation for the current environment. */ emoji.prototype.replace_unified = function(str){ var self = this; self.init_unified(); return str.replace(self.rx_unified, function(m, p1, p2){ var val = self.map.unified[p1]; if (val){ var idx = null; if (p2 == '\uD83C\uDFFB') idx = '1f3fb'; if (p2 == '\uD83C\uDFFC') idx = '1f3fc'; if (p2 == '\uD83C\uDFFD') idx = '1f3fd'; if (p2 == '\uD83C\uDFFE') idx = '1f3fe'; if (p2 == '\uD83C\uDFFF') idx = '1f3ff'; if (idx){ return self.replacement(val, null, null, { idx : idx, actual : p2, wrapper : '' }); } return self.replacement(val); } val = self.map.unified_vars[p1]; if (val){ return self.replacement(val[0], null, null, { 'idx' : val[1], 'actual' : '', 'wrapper' : '', }); } return m; }); }; emoji.prototype.addAliases = function(map){ var self = this; self.init_colons(); for (var i in map){ self.map.colons[i] = map[i]; } }; emoji.prototype.removeAliases = function(list){ var self = this; for (var i=0; i'+text+'
    '+extra; }else if (self.use_css_imgs){ return ''+text+''+extra; }else{ return ''+text+''+extra; } } return ''+extra; }; // Wraps the output of a native endpoint, if configured /** @private */ emoji.prototype.format_native = function(native, allow_wrap){ var self = this; if (self.wrap_native && allow_wrap){ return ''+ native + ''; } return native; }; // Finds the best image to display, taking into account image set precedence and obsoletes /** @private */ emoji.prototype.find_image = function(idx, var_idx){ var self = this; // set up some initial state var out = { 'path' : '', 'sheet' : '', 'sheet_size' : 0, 'px' : self.data[idx][4], 'py' : self.data[idx][5], 'full_idx' : idx, 'is_var' : false, 'unified' : self.data[idx][0][0] }; var use_mask = self.data[idx][6]; // can we use a variation? if (var_idx && self.variations_data[idx] && self.variations_data[idx][var_idx]){ var var_data = self.variations_data[idx][var_idx]; out.px = var_data[1]; out.py = var_data[2]; out.full_idx = var_data[0]; out.is_var = true; out.unified = var_data[4]; use_mask = var_data[3]; } // this matches `build/build_image.php` `in emoji-data`, so that sheet and images modes always // agree about the image to use. var try_order = [self.img_set, 'apple', 'google', 'twitter', 'facebook', 'messenger']; // for each image set, see if we have the image we need. otherwise check for an obsolete in // that image set for (var j=0; j/g, '>'); if (!self.map.colons[self.emoticons_data[i]]) continue; self.map.emoticons[emoticon] = self.map.colons[self.emoticons_data[i]]; a.push(self.escape_rx(emoticon)); } self.rx_emoticons = new RegExp(('(^|\\s)('+a.join('|')+')(?=$|[\\s|\\?\\.,!])'), 'g'); }; // Initializes the colon string data /** @private */ emoji.prototype.init_colons = function(){ var self = this; if (self.inits.colons) return; self.inits.colons = 1; self.rx_colons = new RegExp('\:[a-zA-Z0-9-_+]+\:(\:skin-tone-[2-6]\:)?', 'g'); self.map.colons = {}; for (var i in self.data){ for (var j=0; j