Showing preview only (619K chars total). Download the full file or copy to clipboard to get everything.
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 <http://www.gnu.org/licenses/>.
================================================
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 <http://localhost:8080>!
#### 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 <http://localhost:8080> !
#### 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 <http://www.wassenaar.org/> 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 <steve@tryingtobeawesome.com>",
"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 (
<div id="page">
<Header
username={username}
onToggleModalVisibility={this.onToggleModalVisibility} />
{pincodeRequired && <PincodeModal
showModal={pincodeRequired}
onSetPincode={this.onSetPincode}
onToggleModalVisibility={this.onToggleModalVisibility} />}
{showUsernameModal && <UsernameModal
previousUsername={previousUsername}
username={username}
isVisible={showUsernameModal}
isNewRoom={isNewRoom}
setUsername={this.props.setUsername}
connecting={connecting}
connected={connected}
onToggleModalVisibility={this.onToggleModalVisibility} />}
<main className="encloser">
<ChatContainer
messageInputFocus={chatInputFocus}
onToggleModalVisibility={this.onToggleModalVisibility} />
</main>
</div>
);
}
}
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 (
<div className="suggestions-container">
<div className="suggestions-header">
{ isMentions
? 'User'
:'Emoji'
} matching <strong>"{chat.suggestionWord}"</strong>
<span className="header-help">
<strong>↑</strong><strong>↓ </strong> to navigate
<span className="inline-margin"><strong> ↵ </strong> to select</span>
</span>
</div>
{ isMentions
? <MentionSuggestions />
: <EmojiSuggestions />
}
</div>
);
};
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 (
<div className="content">
<AlertContainer />
<MessageBox />
{suggestions.length > 0 && <AutoSuggest />}
<MessageForm
shouldHaveFocus={messageInputFocus}
onToggleModalVisibility={onToggleModalVisibility} />
</div>
);
};
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 (
<li>
<a href="#" onClick={this.onSelectRoom.bind(this)}>{chatRoom.roomname}</a>
</li>
);
}
}
// class ChatRoom extends Component {
// render(){
// let username = this.props.username;
// let room = this.props.room;
//
// return (
// <div className="chatroom">
// {(room.messages || []).map(message => {
// let fromMe = (message.from === username);
// return (
// <div key={message.key} className={fromMe ? 'chat-outgoing' : 'chat-incoming'}>
// {message.from}: {message.msg}
// </div>
// )
// })}
// </div>
// )
// }
// }
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 }) => (
<ul>
{chat.suggestions.map((emoj, i) => {
const suggestion = emoji.replace_colons(emoj.name) + emoj.name;
const suggestionMD = suggestion.replace(/<span class="emoji emoji-sizer" style="background-image:url\((\/static\/img\/emoji\/apple\/64\/.*?)\)" data-codepoints="(?:.*?)"><\/span>/g, '');
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 (<li {...props} dangerouslySetInnerHTML={{__html: renderItem}}></li>);
})}
</ul>
);
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 }) => (
<ul>
{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 <li {...props}>
<UserStatusIconBubble status={user.status} />
{mention.slice(1)}
</li>;
})}
</ul>
);
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 <span> tags to Markdown
let emojiMD = emojified.replace(
/<span class="emoji emoji-sizer" style="background-image:url\((\/static\/img\/emoji\/apple\/64\/)(.*?)(\.png)\)" data-codepoints="(?:.*?)"><\/span>/g,
(match, $1, $2, $3) => {
// Example:
//
// $1 == /static/img/emoji/apple/64/
// $2 == 1f604
// $3 == .png
// emoji.data[$2][3][0] == smile
// return ''
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 '';
}
);
// Render escaped HTML/Markdown
let linked = md.render(emojiMD);
return (
<li className={'chat-message ' + messageClass} key={message.key}>
<span className="username">{message.from}</span>
<div dangerouslySetInnerHTML={{__html: linked}}>
</div>
</li>
);
}
}
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 (
<div className="message-box" onClick={closePicker}>
<div className="message-list">
<MessageList
onNewMessages={() => this.onNewMessages()}
messages={messages}
username={username} />
</div>
<div style={{float: "left", clear: "both"}}
ref={this.setMessagesEndRef}>
</div>
</div>
);
}
}
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 <enter> unless <shift-enter> 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 (
<div className="message-form">
<form role="form" className="form" onSubmit={this.onSendMessage}>
{showEmojiPicker && <Picker
emojiSize={24}
perLine={9}
skin={1}
set={'apple'}
autoFocus={false}
include={[]}
exclude={['nature', 'places', 'flags']}
emoji={""}
title={"LeapChat"}
backgroundImageFn={this.backgroundImageFn}
onClick={this.addEmoji} />}
<div>
<div className="chat-icons">
<FaSmile size={24}
className="emoji-picker-icon"
onClick={togglePicker} />
<ToggleAudioIcon
isAudioEnabled={isAudioEnabled}
onSetIsAudioEnabled={onSetIsAudioEnabled} />
<OpenSearchIcon
username={username}
messages={messages} />
<InviteIcon />
<div className="right-chat-icons"></div>
</div>
<div className="message" onKeyDown={this.handleKeyDown}>
<textarea
className="form-control"
onChange={this.onMessageUpdate}
onKeyPress={this.onKeyPress}
onKeyDown={this.onKeyDown}
name="message"
value={message}
ref={this.messageInput}
placeholder="Enter message">
</textarea>
<Button variant="default" onClick={this.onSendMessage}>
<FaArrowAltCircleRight size={30} />
</Button>
</div>
</div>
</form>
</div>
);
}
}
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 (
<ul>
{messages.map((message, i) => {
return (
<Message
key={i}
message={message}
username={username} />
);
})}
</ul>
);
}
}
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 (
<div className="users-icon">
<FaUsers size={30} onClick={onToggleUserList} />
</div>
);
};
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 (
<div className="users-list" style={styleUserList()}>
<ul>
{userStatuses.map((userStatus, i) => {
return (
<li key={i}>
<UserStatusIcon
username={userStatus.username}
status={userStatus.status}
isCurrentUser={userStatus.username === currentUsername}
onToggleModalVisibility={onToggleModalVisibility} />
</li>
);
})}
</ul>
<div className="invite-users" >
<Button
className="icon-button"
variant="link"
onClick={() => setShowSharingModal(true)}>
Invite Users <FaShareAlt size={15} />
</Button>
{showSharingModal && <SharingModal
isVisible={showSharingModal}
onClose={() => setShowSharingModal(false)} />}
</div>
</div>
);
};
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 = (
<Tooltip id="edit-username-tooltip">Edit Username</Tooltip>
);
export const UserStatusIconBubble = ({ status }) => {
let statusIcon = <FaMinusCircle style={styleOffline} />;
if (status === 'viewing') {
statusIcon = <FaCircle style={styleViewing} />;
} else if (status === 'online') {
statusIcon = <FaCircle style={styleOnline} />;
}
return (
<>
{statusIcon}
</>
);
};
UserStatusIconBubble.propTypes = {
status: PropTypes.string.isRequired,
};
export const UserStatusIcon = ({
username,
status,
isCurrentUser,
onToggleModalVisibility
}) => {
const onShowUsernameModal = () => {
onToggleModalVisibility('username', true);
};
return (
<div style={styleUserStatus}>
<UserStatusIconBubble status={status} />
{username}
{isCurrentUser && <span> (me)</span> }
<span style={styleEditUsername} data-testid="edit-username">
{isCurrentUser &&
// <OverlayTrigger placement="bottom" overlay={editUsernameTooltip} delayShow={300} delayHide={150}>
// </OverlayTrigger>
<FaEdit onClick={onShowUsernameModal} size={19} />
}
</span>
</div>
);
};
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 = (
<Tooltip id="share-chat">Invite to Chat</Tooltip>
);
const InviteIcon = () => {
const [showSharingModal, setShowSharingModal] = useState(false);
return (
<div className="sharing">
{/* <OverlayTrigger overlay={shareChatTooltip} placement="top" delayShow={300} delayHide={150}> */}
{/* </OverlayTrigger> */}
<FaShareAltSquare size={24} onClick={() => setShowSharingModal(true)} />
{showSharingModal && <SharingModal
isVisible={showSharingModal}
onClose={() => setShowSharingModal(false)} />}
</div>
);
};
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 = (
<Tooltip id="open-message-search">Search Content</Tooltip>
);
return (
<div className="open-message-search">
{/* <OverlayTrigger overlay={openTooltip} placement="top" delayShow={300} delayHide={150}> */}
{/* </OverlayTrigger> */}
<FaSearch
size={24}
onClick={() => setShowSearchModal(true)} />
{showSearchModal && <SearchModal
username={username}
isVisible={showSearchModal}
messages={messages}
onClose={() => setShowSearchModal(false)} />}
</div>
);
};
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 = (
<Tooltip id="mute-audio">Mute Audio</Tooltip>
);
const enableAudioTooltip = (
<Tooltip id="enable-audio">Enable Audio</Tooltip>
);
const DisableAudioIcon = ({ onSetIsAudioEnabled }) => (
// <OverlayTrigger overlay={disableAudioTooltip} placement="top" delayShow={300} delayHide={150}>
// </OverlayTrigger>
<FaVolumeUp size={24} onClick={() => disableAudio()} />
);
const EnableAudioIcon = ({ onSetIsAudioEnabled }) => (
// <OverlayTrigger overlay={enableAudioTooltip} placement="top" delayShow={300} delayHide={150}>
// </OverlayTrigger>
<FaVolumeMute size={24} onClick={() => enableAudio()} />
);
const ToggleAudioIcon = ({
isAudioEnabled,
enableAudio,
disableAudio,
}) => {
return (
<div className="toggle-audio">
{isAudioEnabled && <FaVolumeUp size={24} onClick={() => disableAudio()} />}
{!isAudioEnabled && <FaVolumeMute size={24} onClick={() => enableAudio()} />}
</div>
);
};
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 (
<div className="alert-container" style={{marginRight: '10px'}}>
{alertMessage && <Alert
variant={alertStyle}
onClose={dismissAlert}
dismissible>
{alertMessage}
</Alert>}
</div>
);
};
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 (
<div className="row">
<div className="col-md-12 throbber" title="Your content is loading...">
<div style={throbberDotStyles} ref="firstDot"></div>
<div style={throbberDotStyles} ref="secondDot"></div>
<div style={throbberDotStyles} ref="thirdDot"></div>
</div>
</div>
);
}
}
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 (
<div key={this.props.room.key} className="chatroom">
{(this.props.room.messages || []).map(message => {
let fromMe = (message.from === username);
return (
<div key={message.key} className={fromMe ? 'chat-outgoing' : 'chat-incoming'}>
{message.from}: {message.msg}
</div>
);
})}
</div>
);
}
}
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 onClick={closePicker}>
<div className="logo-container">
<div id="logo-info">
<Logo />
<Info />
</div>
<Settings />
</div>
<UserIcon onToggleUserList={onToggleUserList} />
<UserList
username={username}
displayUserList={showUserList}
onToggleModalVisibility={onToggleModalVisibility} />
</header>
);
};
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 = (
<Tooltip id="open-info-tooltip">Open LeapChat Info</Tooltip>
);
const Info = () => {
const [showInfoModal, setShowInfoModal] = useState(false);
return (
<div className="info">
<FaInfoCircle onClick={() => setShowInfoModal(true)} size={19}/>
{showInfoModal && <InfoModal
isVisible={showInfoModal}
onClose={() => setShowInfoModal(false)} />}
</div>
);
};
Info.propTypes = {};
export default Info;
================================================
FILE: src/components/layout/Logo.js
================================================
import React from 'react';
const Logo = () => {
return (
<div className="logo">
LeapChat
</div>
);
};
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 = (
<Tooltip id="open-settings-tooltip">Open Settings</Tooltip>
);
const Settings = () => {
const [showSettingsModal, setShowSettingsModal] = useState(false);
return (
<div className="settings" >
{/* <OverlayTrigger placement="bottom" overlay={settingsTooltip} delayShow={300} delayHide={150}> */}
{/* </OverlayTrigger> */}
<FaCog size={27} onClick={() => setShowSettingsModal(true)} />
{showSettingsModal && <SettingsModal
isVisible={showSettingsModal}
onClose={() => setShowSettingsModal(false)} />}
</div>
);
};
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 (
<Modal size="lg" show={isVisible} onHide={onClose}>
<Modal.Header closeButton>
<Modal.Title>
<h3>Welcome to LeapChat!</h3>
</Modal.Title>
</Modal.Header>
<Modal.Body>
<h4>LeapChat: encrypted, ephemeral, in-browser chat.</h4>
<p>Just visit <a href="https://www.leapchat.org/" target="_blank" rel="nofollow noreferrer noopener">leapchat.org</a> 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!</p>
<h3>
Why LeapChat?
</h3>
<p>
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 <em>does</em> encrypt your messages (even we can't see them!), <em>doesn't</em> 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 <a href="https://github.com/cryptag/leapchat" target="_blank" rel="nofollow noreferrer noopener">open source</a>!
</p>
<h3>
How does it work?
</h3>
<div>
When you click on a link to a LeapChat room:
<ol>
<li>
Your browser loads the HTML, CSS, and JavaScript from the server (e.g., leapchat.org)
</li>
<li>
That JavaScript code then grabs the long passphrase at the end of the URL (the "URL hash" -- everything after the `#`), then passes it to <a href="https://web.archive.org/web/20180508023310/https://minilock.io/" target="_blank" rel="nofollow noreferrer noopener">miniLock</a>, which then deterministically generates a keypair from that passphrase
</li>
<li>
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
</li>
</ol>
The server can't even see your username! That's encrypted, too, and is attached to the messages you send.
</div>
<h3>
Can I type markdown in my messages?
</h3>
<p>
<em>Yup!</em> To learn about Markdown syntax, like surrounding words with **double asterisks** to make them <b>bold</b>, or with _underscores_ to make them <i>italicized</i>, <a href="https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet" target="_blank" rel="nofollow noreferrer noopener">check out this guide</a>.
</p>
</Modal.Body>
</Modal>
);
};
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 (
<div>
<Modal size="lg" show={showModal} onHide={this.onClose}>
<Modal.Header>
<Modal.Title>Set Pincode</Modal.Title>
</Modal.Header>
<Modal.Body>
<div className="form-group">
<input
type="text"
className="form-control"
ref={(input) => { this.pincodeInput = input; }}
defaultValue={pincode}
placeholder="Enter pincode or password"
onKeyPress={this.onPincodeKeyPress}
autoFocus={true} />
<br />
<Button size="sm" variant="primary" onClick={this.setRandomPincodeInForm}>Generate Random Pincode</Button>
</div>
</Modal.Body>
<Modal.Footer>
{pincode && <Button onClick={this.onClose}>Cancel</Button>}
<Button onClick={this.onSetPincodeClick} variant="primary">Set Pincode</Button>
</Modal.Footer>
</Modal>
</div>
);
}
}
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 = (
<Popover id="search-info-popover" title="Search Message History">
Your messages are encrypted in transit and in storage on our servers. So
how can you search them?
<br /><br />
Simple! The search happens entirely in the browser. So you can rest easy.
</Popover>
);
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 (
<div>
<Modal size="lg" show={isVisible} onHide={this.props.onClose}>
<Modal.Header closeButton>
Search{' '}
<FaInfoCircle size="15" />
{/* <OverlayTrigger trigger="click" placement="bottom" overlay={searchInfoPopover}> */}
{/* </OverlayTrigger> */}
</Modal.Header>
<Modal.Body>
<div className="form-group">
<form>
<label htmlFor="message-search">Search messages</label>
<input
id="message-search"
type="search"
className="form-control"
placeholder="Enter search text"
onChange={this.getSearchResults}
autoFocus={true} />
</form>
</div>
<hr />
{isLoadingResults && <div>
<Throbber />
<h4>Loading results...</h4>
</div>}
{searchResults.length > 0 && <div className="search-results">
<ul>
{searchResults.map((result, i) => {
return <Message key={i} username={username} message={result} />;
})}
</ul>
</div>}
</Modal.Body>
</Modal>
</div>
);
}
}
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 (
<div>
<Modal size="lg" show={isVisible} onHide={onClose}>
<Modal.Header closeButton>
<Modal.Title>Settings</Modal.Title>
</Modal.Header>
<Modal.Body>
<h3>Feedback</h3>
<p>
Do you have feedback or suggestions on how we can improve LeapChat? We're listening!{' '}
<a href="https://github.com/cryptag/leapchat/issues" target="_blank" rel="nofollow noreferrer noopener">
Share your feedback here.{' '}<FaExternalLinkAlt size={15} />
</a>
</p>
<hr />
<h3 style={{ color: 'red' }}>Danger Zone</h3>
<p>
By clicking here, you will delete all messages in this chat from the server, for all users, forever.
</p>
<Button onClick={onDeleteAllMsgs} variant="danger">
Delete All Messages Forever
</Button>
</Modal.Body>
<Modal.Footer>
</Modal.Footer>
</Modal>
</div>
);
};
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 = (
<Tooltip id="confirm-copy">
<strong>Link copied!</strong>
</Tooltip>
);
const SharingModal = ({
isVisible,
onClose
}) => {
return (
<div>
<Modal size="lg" show={isVisible} onHide={onClose}>
<Modal.Header closeButton>
<Modal.Title>Invite to Chat</Modal.Title>
</Modal.Header>
<Modal.Body>
{ navigator.share && <div>
<h3>Share Link</h3>
<p>
Invite with a link shared via SMS, Email, etc.
</p>
<Button className="icon-button" onClick={onShareLink} variant="primary">
Share Link <FaShareAltSquare />
</Button>
<hr />
</div>
}
<h3>Copy Link</h3>
<p>Invite with a link copied to your clipboard.</p>
<div className="input-group share-copy-link">
<input className="form-control current-href" type="text" readOnly value={window.location.href} />
<OverlayTrigger
trigger="click"
overlay={copyLinkTooltip}
placement="top"
delay={{ show: 300, hide: 150 }}>
<Button className="icon-button" variant="primary" onClick={onCopyShareLink}>
Copy to Clipboard
<FaShareAlt size={15} />
</Button>
</OverlayTrigger>
</div>
</Modal.Body>
</Modal>
</div>
);
};
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 = (
<p>Cranking a bunch of gears.</p>
);
if (connecting) {
progress = 50;
statusMessage = <p>Creating a secure connection with LeapChat servers.</p>;
} else if (connected) {
progress = 95;
statusMessage = <p>Connected!</p>;
}
return (
<div>
<Modal size="lg" show={isVisible} onHide={onClose}>
<Modal.Header>
<Modal.Title>Set Username</Modal.Title>
</Modal.Header>
<Modal.Body>
<div data-testid="set-username-form" className="form-group">
{/* username is empty on initial page load, not on subsequent 'edit username' opens */}
{!username && isNewRoom && <Alert
variant="success">
New room created!
</Alert>}
{!username && !isNewRoom && <Alert
variant="success">
Successfully joined room!
</Alert>}
<label className="form-label" htmlFor="username">Username</label>
<input
style={{marginBottom: "10px"}}
id="username"
type="text"
className="form-control"
ref={usernameInput}
defaultValue={ username || previousUsername }
placeholder="Enter username (e.g., trinity)"
onKeyUp={onUsernameKeyUp}
autoFocus={true}
autoComplete="off" />
{failMessage && <div className="alert alert-danger" role="alert" >
<br />
<strong>Invalid Username: </strong>
{failMessage}
</div>}
<Button size="sm" variant="primary" onClick={setRandomUsernameInForm}>Generate Random Username</Button>
</div>
{!connected && <div className="progress-indicator" style={{marginTop: '20px'}}>
{statusMessage}
<ProgressBar animated now={progress} label={`${progress}%`} />
</div>}
</Modal.Body>
<Modal.Footer>
{username && <Button variant="light" onClick={onClose}>Cancel</Button>}
<Button data-testid="set-username" onClick={onSetUsername} variant="primary" disabled={!connected}>Set Username</Button>
</Modal.Footer>
</Modal>
</div>
);
};
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
================================================
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
<meta name="viewport" content="width=device-width">
<title>
<%= htmlWebpackPlugin.options.title %>
</title>
</head>
<body>
<div id="root">
</div>
<audio id="notification-audio" src="/notification_gertz.wav"></audio>
</body>
</html>
================================================
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(
<Provider store={store}>
<App />
</Provider>
);
================================================
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_
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
SYMBOL INDEX (235 symbols across 46 files)
FILE: db/sql/migration003.sql
function delete_expired_messages (line 1) | CREATE FUNCTION delete_expired_messages() RETURNS void AS $$
FILE: db/sql/table01_rooms.sql
type rooms (line 1) | CREATE TABLE rooms (
FILE: db/sql/table02_messages.sql
type messages (line 1) | CREATE TABLE messages (
FILE: gzip.go
type gzipResponseWriter (line 14) | type gzipResponseWriter struct
method Write (line 21) | func (w gzipResponseWriter) Write(b []byte) (int, error) {
function inAnyStr (line 25) | func inAnyStr(s string, container []string) bool {
function gzipHandler (line 34) | func gzipHandler(h http.Handler) http.Handler {
FILE: json.go
constant contentTypeJSON (line 14) | contentTypeJSON = "application/json; charset=utf-8"
function WriteError (line 16) | func WriteError(w http.ResponseWriter, errStr string, secretErr error) e...
function WriteErrorStatus (line 20) | func WriteErrorStatus(w http.ResponseWriter, errStr string, secretErr er...
function WSWriteError (line 32) | func WSWriteError(wsConn *websocket.Conn, errStr string, secretErr error...
FILE: leapchat.go
function init (line 17) | func init() {
function main (line 27) | func main() {
FILE: messages.go
type Message (line 12) | type Message
type OutgoingPayload (line 14) | type OutgoingPayload struct
type FromServer (line 19) | type FromServer struct
type ToServer (line 23) | type ToServer struct
type IncomingPayload (line 28) | type IncomingPayload struct
function WSMessagesHandler (line 33) | func WSMessagesHandler(rooms *RoomManager) func(w http.ResponseWriter, r...
function messageReader (line 51) | func messageReader(room *Room, client *Client) {
FILE: miniware/miniware.go
constant MINILOCK_ID_KEY (line 20) | MINILOCK_ID_KEY = "minilock_id"
constant MINILOCK_KEYPAIR_KEY (line 21) | MINILOCK_KEYPAIR_KEY = "minilock_keypair"
constant WEBSOCKET_CONNECTION (line 22) | WEBSOCKET_CONNECTION = "websocket_connection"
constant AuthError (line 24) | AuthError = "Error authorizing you"
type Mapper (line 32) | type Mapper struct
method GetMinilockID (line 41) | func (m *Mapper) GetMinilockID(authToken string) (string, error) {
method SetMinilockID (line 52) | func (m *Mapper) SetMinilockID(authToken, mID string) error {
function NewMapper (line 37) | func NewMapper() *Mapper {
function Auth (line 78) | func Auth(h http.Handler, m *Mapper) func(w http.ResponseWriter, req *ht...
function GetMinilockID (line 160) | func GetMinilockID(req *http.Request) (string, error) {
function GetWebsocketConn (line 169) | func GetWebsocketConn(req *http.Request) (*websocket.Conn, error) {
function writeWSError (line 178) | func writeWSError(wsConn *websocket.Conn, errStr string) error {
function writeError (line 186) | func writeError(w http.ResponseWriter, errStr string, statusCode int) {
FILE: pg_types.go
type PGClient (line 18) | type PGClient struct
method Post (line 28) | func (cl *PGClient) Post(urlSuffix string, payload interface{}) (*http...
method PostWanted (line 49) | func (cl *PGClient) PostWanted(urlSuffix string, payload interface{}, ...
method Get (line 69) | func (cl *PGClient) Get(urlSuffix string) (*http.Response, error) {
method Delete (line 80) | func (cl *PGClient) Delete(urlSuffix string) (*http.Response, error) {
method GetInto (line 91) | func (cl *PGClient) GetInto(urlSuffix string, respobj interface{}) err...
function NewPGClient (line 22) | func NewPGClient(baseURL string) *PGClient {
type PGRoom (line 113) | type PGRoom struct
method Create (line 117) | func (room PGRoom) Create(cl *PGClient) error {
type PGMessage (line 121) | type PGMessage struct
method MarshalJSON (line 132) | func (msg *PGMessage) MarshalJSON() ([]byte, error) {
type pgPostMessage (line 130) | type pgPostMessage
type PGMessages (line 139) | type PGMessages
method Create (line 141) | func (msgs PGMessages) Create(pgClient *PGClient) error {
FILE: room.go
constant POSTGREST_BASE_URL (line 17) | POSTGREST_BASE_URL = "http://localhost:3000"
type RoomManager (line 27) | type RoomManager struct
method GetRoom (line 55) | func (rm *RoomManager) GetRoom(roomID string) *Room {
function NewRoomManager (line 34) | func NewRoomManager(pgClient *PGClient) *RoomManager {
type Room (line 67) | type Room struct
method GetMessages (line 85) | func (r *Room) GetMessages() ([]Message, error) {
method AddMessages (line 112) | func (r *Room) AddMessages(msgs []Message, ttlSecs *int) error {
method AddClient (line 149) | func (r *Room) AddClient(c *Client) {
method RemoveClient (line 156) | func (r *Room) RemoveClient(c *Client) {
method BroadcastMessages (line 172) | func (r *Room) BroadcastMessages(sender *Client, msgs ...Message) {
method DeleteAllMessages (line 186) | func (r *Room) DeleteAllMessages() error {
method BroadcastDeleteSignal (line 205) | func (r *Room) BroadcastDeleteSignal() {
function NewRoom (line 76) | func NewRoom(roomID string, pgClient *PGClient) *Room {
function byteaToBytes (line 126) | func byteaToBytes(hexdata string) ([]byte, error) {
type Client (line 219) | type Client struct
method SendMessages (line 226) | func (c *Client) SendMessages(msgs ...Message) error {
method SendDeleteSignal (line 247) | func (c *Client) SendDeleteSignal() error {
method SendError (line 273) | func (c *Client) SendError(errStr string, secretErr error) error {
FILE: room_test.go
function TestRoomManager (line 11) | func TestRoomManager(t *testing.T) {
function TestRoomMessages (line 33) | func TestRoomMessages(t *testing.T) {
function TestRoomClients (line 77) | func TestRoomClients(t *testing.T) {
function TestRoomBroadcastMessages (line 109) | func TestRoomBroadcastMessages(t *testing.T) {}
FILE: server.go
constant MINILOCK_ID_KEY (line 28) | MINILOCK_ID_KEY = "minilock_id"
function NewRouter (line 31) | func NewRouter(m *miniware.Mapper) *mux.Router {
function NewServer (line 49) | func NewServer(m *miniware.Mapper, httpAddr string) *http.Server {
function ProductionServer (line 61) | func ProductionServer(srv *http.Server, httpsAddr, domain string, manage...
function Login (line 74) | func Login(m *miniware.Mapper, pgClient *PGClient) func(w http.ResponseW...
function parseMinilockID (line 124) | func parseMinilockID(req *http.Request) (string, *taber.Keys, error) {
function redirectToHTTPS (line 136) | func redirectToHTTPS(httpAddr, httpsPort, domain string, manager *autoce...
function getAutocertManager (line 155) | func getAutocertManager(domain string) *autocert.Manager {
FILE: server_test.go
function TestParseMinilockID (line 12) | func TestParseMinilockID(t *testing.T) {
FILE: src/components/App.js
class App (line 19) | class App extends Component {
method constructor (line 20) | constructor(props) {
method componentDidMount (line 36) | componentDidMount() {
method componentDidUpdate (line 41) | componentDidUpdate(prevProps, prevState) {
method connectIfNeeded (line 45) | connectIfNeeded() {
method createDeviceSession (line 59) | createDeviceSession(passphrase) {
method onInitConnection (line 63) | onInitConnection(pincode='') {
method render (line 82) | render() {
FILE: src/components/chat/ChatRoom.js
class ChatRoom (line 3) | class ChatRoom extends Component {
method onSelectRoom (line 4) | onSelectRoom(){
method render (line 9) | render(){
FILE: src/components/chat/Message.js
class Message (line 5) | class Message extends Component {
method render (line 7) | render() {
FILE: src/components/chat/MessageBox.js
class MessageBox (line 8) | class MessageBox extends Component {
method constructor (line 9) | constructor(props){
method render (line 35) | render(){
FILE: src/components/chat/MessageForm.js
class MessageForm (line 30) | class MessageForm extends Component {
method constructor (line 31) | constructor(props) {
method componentDidMount (line 37) | componentDidMount() {
method componentDidUpdate (line 41) | componentDidUpdate() {
method resolveFocus (line 45) | resolveFocus() {
method isPayloadValid (line 89) | isPayloadValid(message) {
method render (line 143) | render() {
FILE: src/components/chat/MessageList.js
class MessageList (line 6) | class MessageList extends Component {
method shouldComponentUpdate (line 8) | shouldComponentUpdate(nextProps, nextState) {
method componentDidUpdate (line 22) | componentDidUpdate(){
method render (line 26) | render() {
FILE: src/components/general/Throbber.js
class Throbber (line 15) | class Throbber extends Component {
method componentDidMount (line 16) | componentDidMount(){
method animateLoadingDots (line 24) | animateLoadingDots(){
method render (line 50) | render(){
FILE: src/components/layout/ChatRoom.js
class ChatRoom (line 3) | class ChatRoom extends Component {
method render (line 4) | render(){
FILE: src/components/modals/PincodeModal.js
class PincodeModal (line 8) | class PincodeModal extends PureComponent {
method componentDidMount (line 10) | componentDidMount() {
method componentDidUpdate (line 14) | componentDidUpdate(){
method isPincodeValid (line 26) | isPincodeValid(pincode) {
method render (line 51) | render() {
FILE: src/components/modals/SearchModal.js
class SearchModal (line 21) | class SearchModal extends Component {
method constructor (line 22) | constructor(props) {
method indexAllMessages (line 36) | indexAllMessages(messages) {
method render (line 61) | render() {
FILE: src/components/modals/Username.js
constant MAX_USERNAME_LENGTH (line 11) | const MAX_USERNAME_LENGTH = 45;
FILE: src/constants/messaging.js
constant SERVER_ERROR_PREFIX (line 1) | const SERVER_ERROR_PREFIX = "Error from server: ";
constant AUTH_ERROR (line 2) | const AUTH_ERROR = "Error authorizing you";
constant ON_CLOSE_RECONNECT_MESSAGE (line 3) | const ON_CLOSE_RECONNECT_MESSAGE = "Message WebSocket closed. Reconnecti...
constant ONE_MINUTE (line 4) | const ONE_MINUTE = 60 * 1000;
constant USER_STATUS_DELAY_MS (line 6) | const USER_STATUS_DELAY_MS = 10 * ONE_MINUTE;
constant PARANOID_USERNAME (line 8) | const PARANOID_USERNAME = ' ';
constant USERNAME_KEY (line 9) | const USERNAME_KEY = 'username';
FILE: src/data/minishare.js
function randomIntsInRange (line 8) | function randomIntsInRange(min, max, numInts){
function genPassphrase (line 24) | function genPassphrase(numWords=25){
FILE: src/data/username.js
function getRandomAdjective (line 3) | function getRandomAdjective(){
function getRandomNoun (line 7) | function getRandomNoun(){
function generateRandomUsername (line 11) | function generateRandomUsername(){
FILE: src/store/actions/alertActions.js
constant ALERT_DISPLAY (line 1) | const ALERT_DISPLAY = 'ALERT_DISPLAY';
constant ALERT_DISMISS (line 2) | const ALERT_DISMISS = 'ALERT_DISMISS';
FILE: src/store/actions/chatActions.js
constant CHAT_INIT_CHAT (line 2) | const CHAT_INIT_CHAT = 'CHAT_INIT_CHAT';
constant CHAT_INIT_CONNECTION (line 3) | const CHAT_INIT_CONNECTION = 'CHAT_INIT_CONNECTION';
constant CHAT_DISCONNECTED (line 4) | const CHAT_DISCONNECTED = 'CHAT_DISCONNECTED';
constant CHAT_CONNECTION_INITIATED (line 5) | const CHAT_CONNECTION_INITIATED = 'CHAT_CONNECTION_INITIATED';
constant CHAT_SEND_MESSAGE (line 6) | const CHAT_SEND_MESSAGE = 'CHAT_SEND_MESSAGE';
constant CHAT_MESSAGE_SENT (line 7) | const CHAT_MESSAGE_SENT = 'CHAT_MESSAGE_SENT';
constant CHAT_ADD_MESSAGE (line 8) | const CHAT_ADD_MESSAGE = 'CHAT_ADD_MESSAGE';
constant CHAT_SET_USER_STATUS (line 9) | const CHAT_SET_USER_STATUS = 'CHAT_SET_USER_STATUS';
constant CHAT_USER_STATUS_SENT (line 10) | const CHAT_USER_STATUS_SENT = 'CHAT_USER_STATUS_SENT';
constant CHAT_SET_USERNAME (line 11) | const CHAT_SET_USERNAME = 'CHAT_SET_USERNAME';
constant CHAT_USERNAME_SET (line 12) | const CHAT_USERNAME_SET = 'CHAT_USERNAME_SET';
FILE: src/store/actions/settingsActions.js
constant ENABLE_AUDIO (line 1) | const ENABLE_AUDIO = 'ENABLE_AUDIO';
constant DISABLE_AUDIO (line 2) | const DISABLE_AUDIO = 'DISABLE_AUDIO';
FILE: src/store/epics/chatEpics.js
function createKeyPairObservable (line 72) | function createKeyPairObservable({ createDeviceSession, urlHash }) {
function createDecryptMessageObservable (line 99) | function createDecryptMessageObservable({ message, mID, secretKey }) {
function getAuthRequestSettings (line 115) | function getAuthRequestSettings({ mID }) {
FILE: src/store/epics/helpers/ChatHandler.js
class ChatHandler (line 19) | class ChatHandler {
method constructor (line 21) | constructor(wsUrl) {
method send (line 117) | send({ contents = {}, tags, ttl = 0 }) {
method sendMessage (line 137) | sendMessage(message, username) {
FILE: src/store/epics/helpers/createDetectPageVisibilityObservable.js
function createDetectPageVisibilityObservable (line 7) | function createDetectPageVisibilityObservable() {
FILE: src/store/reducers/alertReducer.js
function alertReducer (line 13) | function alertReducer(state = initialState, action) {
FILE: src/store/reducers/chatReducer.js
function chatReducer (line 41) | function chatReducer(state = initialState, action) {
FILE: src/store/reducers/settingsReducer.js
function settingsReducer (line 13) | function settingsReducer(state = initialState, action) {
FILE: src/utils/audio.js
function playNotification (line 1) | function playNotification(){
FILE: src/utils/chat.js
function extractMessageMetadata (line 3) | function extractMessageMetadata(tags) {
FILE: src/utils/crypto/nacl.js
function ts64 (line 34) | function ts64(x, i, h, l) {
function vn (line 45) | function vn(x, xi, y, yi, n) {
function crypto_verify_16 (line 51) | function crypto_verify_16(x, xi, y, yi) {
function crypto_verify_32 (line 55) | function crypto_verify_32(x, xi, y, yi) {
function core_salsa20 (line 59) | function core_salsa20(o, p, k, c) {
function core_hsalsa20 (line 252) | function core_hsalsa20(o, p, k, c) {
function crypto_core_salsa20 (line 389) | function crypto_core_salsa20(out, inp, k, c) {
function crypto_core_hsalsa20 (line 393) | function crypto_core_hsalsa20(out, inp, k, c) {
function crypto_stream_salsa20_xor (line 400) | function crypto_stream_salsa20_xor(c, cpos, m, mpos, b, n, k) {
function crypto_stream_salsa20 (line 425) | function crypto_stream_salsa20(c, cpos, b, n, k) {
function crypto_stream (line 449) | function crypto_stream(c, cpos, d, n, k) {
function crypto_stream_xor (line 457) | function crypto_stream_xor(c, cpos, m, mpos, d, n, k) {
function crypto_onetimeauth (line 822) | function crypto_onetimeauth(out, outpos, m, mpos, n, k) {
function crypto_onetimeauth_verify (line 829) | function crypto_onetimeauth_verify(h, hpos, m, mpos, n, k) {
function crypto_secretbox (line 835) | function crypto_secretbox(c, m, d, n, k) {
function crypto_secretbox_open (line 844) | function crypto_secretbox_open(m, c, d, n, k) {
function set25519 (line 855) | function set25519(r, a) {
function car25519 (line 860) | function car25519(o) {
function sel25519 (line 870) | function sel25519(p, q, b) {
function pack25519 (line 879) | function pack25519(o, n) {
function neq25519 (line 903) | function neq25519(a, b) {
function par25519 (line 910) | function par25519(a) {
function unpack25519 (line 916) | function unpack25519(o, n) {
function A (line 922) | function A(o, a, b) {
function Z (line 926) | function Z(o, a, b) {
function M (line 930) | function M(o, a, b) {
function S (line 1301) | function S(o, a) {
function inv25519 (line 1305) | function inv25519(o, i) {
function pow2523 (line 1316) | function pow2523(o, i) {
function crypto_scalarmult (line 1327) | function crypto_scalarmult(q, n, p) {
function crypto_scalarmult_base (line 1380) | function crypto_scalarmult_base(q, n) {
function crypto_box_keypair (line 1384) | function crypto_box_keypair(y, x) {
function crypto_box_beforenm (line 1389) | function crypto_box_beforenm(k, y, x) {
function crypto_box (line 1398) | function crypto_box(c, m, d, n, y, x) {
function crypto_box_open (line 1404) | function crypto_box_open(m, c, d, n, y, x) {
function crypto_hashblocks_hl (line 1453) | function crypto_hashblocks_hl(hh, hl, m, n) {
function crypto_hash (line 1814) | function crypto_hash(out, m, n) {
function add (line 1854) | function add(p, q) {
function cswap (line 1880) | function cswap(p, q, b) {
function pack (line 1887) | function pack(r, p) {
function scalarmult (line 1896) | function scalarmult(p, q, s) {
function scalarbase (line 1911) | function scalarbase(p, s) {
function crypto_sign_keypair (line 1920) | function crypto_sign_keypair(pk, sk, seeded) {
function modL (line 1940) | function modL(r, x) {
function reduce (line 1965) | function reduce(r) {
function crypto_sign (line 1973) | function crypto_sign(sm, m, n, sk) {
function unpackneg (line 2008) | function unpackneg(r, p) {
function crypto_sign_open (line 2046) | function crypto_sign_open(m, sm, n, pk) {
function checkLengths (line 2141) | function checkLengths(k, n) {
function checkBoxLengths (line 2146) | function checkBoxLengths(pk, sk) {
function checkArrayTypes (line 2151) | function checkArrayTypes() {
function incrementChunkCounter (line 2398) | function incrementChunkCounter(fullNonce) {
function setLastChunkFlag (line 2405) | function setLastChunkFlag(fullNonce) {
function clean (line 2409) | function clean() {
function readChunkLength (line 2416) | function readChunkLength(data, offset) {
function checkArgs (line 2424) | function checkArgs(key, nonce, maxChunkLength) {
function StreamEncryptor (line 2431) | function StreamEncryptor(key, nonce, maxChunkLength) {
function StreamDecryptor (line 2467) | function StreamDecryptor(key, nonce, maxChunkLength) {
FILE: src/utils/crypto/scrypt.js
function scrypt (line 26) | function scrypt(password, salt, logN, r, dkLen, interruptStep, callback,...
FILE: src/utils/encrypter.js
function getEmail (line 7) | function getEmail(passphrase){
function getPassphrase (line 11) | function getPassphrase(documentHash){
function generateMessageKey (line 31) | function generateMessageKey(i){
FILE: src/utils/pagevisibility.js
function detectPageVisible (line 3) | function detectPageVisible(onVisible, onHidden, onClose){
FILE: src/utils/sessions.js
function connectWithAuthRequest (line 21) | async function connectWithAuthRequest(initiateConnection, mID, secretKey...
FILE: src/utils/tags.js
function tagByPrefix (line 5) | function tagByPrefix(plaintags, ...prefixes) {
function tagByPrefixStripped (line 18) | function tagByPrefixStripped(plaintags, ...prefixes) {
function tagsByPrefix (line 30) | function tagsByPrefix(plaintags, prefix) {
function tagsByPrefixStripped (line 40) | function tagsByPrefixStripped(plaintags, prefix) {
function sortRowByCreated (line 51) | function sortRowByCreated(row, nextRow){
function parseJSON (line 58) | function parseJSON(str){
function encodeObjForPost (line 62) | function encodeObjForPost(obj){
function cleanedFields (line 66) | function cleanedFields(s){
FILE: src/utils/time.js
function nowUTC (line 1) | function nowUTC(){
FILE: src/utils/vh_fix.js
function vh (line 3) | function vh() {
Condensed preview — 122 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (671K chars).
[
{
"path": ".babelrc",
"chars": 208,
"preview": "{\n \"presets\": [\n \"@babel/preset-react\",\n \"@babel/preset-env\"\n ],\n \"plugins\": [\n \"system-import-transformer\","
},
{
"path": ".editorconfig",
"chars": 97,
"preview": "indent_style = space\nindent_size = 2\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n"
},
{
"path": ".eslintrc",
"chars": 1015,
"preview": "{\n \"parser\": \"@babel/eslint-parser\",\n \"plugins\": [\n \"@babel\",\n \"react\"\n ],\n \"parserOptions\": {\n \"ecmaFeature"
},
{
"path": ".github/workflows/pull_request_javascript_check.yml",
"chars": 451,
"preview": "name: JavaScript Lint and Test Check\n\non:\n pull_request:\n branches: [develop]\n\njobs:\n run_eslinter:\n name: Runs "
},
{
"path": ".gitignore",
"chars": 203,
"preview": "*~\nnode_modules/\nbuild/\nleapchat\n\n# bower static assets\nstatic/lib/\n\n# docker container volumes\n_docker-volumes/\n\n# Edit"
},
{
"path": ".nvmrc",
"chars": 8,
"preview": "v14.0.0\n"
},
{
"path": "LICENSE.md",
"chars": 661,
"preview": "Copyright (C) 2017 CrypTag\n\nThis program is free software: you can redistribute it and/or modify\nit under the terms of t"
},
{
"path": "Makefile",
"chars": 920,
"preview": "build:\n\tgo build\n\tnpm run build\n\nrelease: check-env\n\t@echo 'Hopefully \"git diff\" is empty!'\n\t@echo 'Creating release for"
},
{
"path": "README.md",
"chars": 9198,
"preview": "# LeapChat\n\nLeapChat is an ephemeral chat application. LeapChat uses\n[miniLock](https://web.archive.org/web/20180508023"
},
{
"path": "db/init_sql.sh",
"chars": 449,
"preview": "#!/bin/bash\n\nset -euo pipefail\n\n# Create 'leapchat' database, associated role\npsql -d postgres < sql/pre.sql\n\nexport pg_"
},
{
"path": "db/migrate.sh",
"chars": 171,
"preview": "#!/bin/bash\n# Steve Phillips / elimisteve\n# 2017.05.18\n\nset -euo pipefail\n\n# Run migrations\nfor file in $*; do\n psql "
},
{
"path": "db/postgrest.conf",
"chars": 173,
"preview": "db-uri = \"postgres://superuser:superuser@localhost:5432/leapchat\"\ndb-schema = \"public\"\ndb-anon-role = \"superuser\"\ndb-poo"
},
{
"path": "db/sql/init001.sql",
"chars": 44,
"preview": "create extension if not exists \"uuid-ossp\";\n"
},
{
"path": "db/sql/migration001.sql",
"chars": 58,
"preview": "ALTER TABLE messages ALTER COLUMN message SET DEFAULT '';\n"
},
{
"path": "db/sql/migration002.sql",
"chars": 57,
"preview": "ALTER TABLE messages ALTER COLUMN ttl_secs SET NOT NULL;\n"
},
{
"path": "db/sql/migration003.sql",
"chars": 160,
"preview": "CREATE FUNCTION delete_expired_messages() RETURNS void AS $$\n DELETE FROM messages WHERE created + interval '1s' * ttl_"
},
{
"path": "db/sql/pre.sql",
"chars": 183,
"preview": "CREATE USER superuser WITH PASSWORD 'superuser';\nCREATE DATABASE leapchat OWNER superuser ENCODING 'UTF8';\nGRANT ALL ON "
},
{
"path": "db/sql/table01_rooms.sql",
"chars": 163,
"preview": "CREATE TABLE rooms (\n room_id text NOT NULL UNIQUE PRIMARY KEY CHECK (40 <= LENGTH(room_id) AND LENGTH(room_id) <= 55"
},
{
"path": "db/sql/table02_messages.sql",
"chars": 438,
"preview": "CREATE TABLE messages (\n message_id uuid NOT NULL UNIQUE PRIMARY KEY DEFAULT uuid_generate_v4(),\n room_id "
},
{
"path": "docker-compose.yml",
"chars": 714,
"preview": "version: '3.1'\n\nservices:\n postgres:\n image: postgres:latest\n ports:\n - 127.0.0.1:5432:5432\n"
},
{
"path": "fedora_install.sh",
"chars": 107,
"preview": "#!/bin/bash\n# Matthew Leeds\n# 2017.07.08\n\nsudo dnf install postgresql postgresql-server postgresql-contrib\n"
},
{
"path": "go.mod",
"chars": 895,
"preview": "module github.com/cryptag/leapchat\n\ngo 1.14\n\nrequire (\n\tgithub.com/cathalgarvey/base58 v0.0.0-20150930172411-5e83fd6f66e"
},
{
"path": "go.sum",
"chars": 9123,
"preview": "github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=\ngithub.com/alec"
},
{
"path": "gzip.go",
"chars": 975,
"preview": "package main\n\nimport (\n\t\"compress/gzip\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n)\n\n// GZip solution derived from\n// https://www.lem"
},
{
"path": "json.go",
"chars": 1034,
"preview": "// Steve Phillips / elimisteve\n// 2017.01.16\n\npackage main\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/gorilla/websocket\""
},
{
"path": "leapchat.go",
"chars": 1779,
"preview": "package main\n\nimport (\n\t\"flag\"\n\t\"strings\"\n\n\t\"github.com/cryptag/go-minilock/taber\"\n\t\"github.com/cryptag/leapchat/miniwar"
},
{
"path": "messages.go",
"chars": 2628,
"preview": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/cryptag/leapchat/miniware\"\n\t\"github.com/gorilla/websoc"
},
{
"path": "miniware/miniware.go",
"chars": 4632,
"preview": "// Steve Phillips / elimisteve\n// 2017.04.01\n\npackage miniware\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t"
},
{
"path": "package.json",
"chars": 3188,
"preview": "{\n \"name\": \"LeapChat\",\n \"version\": \"0.7.8\",\n \"description\": \"Self-destructing, encrypted, in-browser chat\",\n \"main\":"
},
{
"path": "pg_types.go",
"chars": 3627,
"preview": "// Steve Phillips / elimisteve\n// 2017.05.18\n\npackage main\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\""
},
{
"path": "playwright.config.js",
"chars": 672,
"preview": "const { defineConfig, devices } = require('@playwright/test');\n\nmodule.exports = defineConfig({\n testDir: \"./test/playw"
},
{
"path": "room.go",
"chars": 5551,
"preview": "package main\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github"
},
{
"path": "room_test.go",
"chars": 2012,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"sync\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestRoomManager(t *testi"
},
{
"path": "server.go",
"chars": 4590,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/cryptag/gosecure/canary\"\n\t\"github.com/cryptag"
},
{
"path": "server_test.go",
"chars": 1460,
"preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc"
},
{
"path": "src/components/App.js",
"chars": 3808,
"preview": "import React, { Component } from 'react';\nimport { connect } from 'react-redux';\nimport {\n setUsername,\n initConnectio"
},
{
"path": "src/components/chat/AutoSuggest.js",
"chars": 957,
"preview": "import React from 'react';\nimport { connect } from 'react-redux';\nimport emoji from '../../utils/emoji_convertor';\nimpor"
},
{
"path": "src/components/chat/ChatContainer.js",
"chars": 980,
"preview": "import React, { Component } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { connect } from 'react-redux';"
},
{
"path": "src/components/chat/ChatRoom.js",
"chars": 936,
"preview": "import React, { Component } from 'react';\n\nclass ChatRoom extends Component {\n onSelectRoom(){\n let roomKey = this.p"
},
{
"path": "src/components/chat/EmojiSuggestions.js",
"chars": 1260,
"preview": "import React from 'react';\nimport { connect } from 'react-redux';\nimport { addSuggestion } from '../../store/actions/cha"
},
{
"path": "src/components/chat/MentionSuggestions.js",
"chars": 964,
"preview": "import React from 'react';\nimport { connect } from 'react-redux';\nimport { addSuggestion } from '../../store/actions/cha"
},
{
"path": "src/components/chat/Message.js",
"chars": 1925,
"preview": "import React, { Component } from 'react';\nimport emoji from '../../utils/emoji_convertor';\nimport md from '../../utils/l"
},
{
"path": "src/components/chat/MessageBox.js",
"chars": 1546,
"preview": "import React, { Component } from 'react';\n\nimport MessageList from './MessageList';\nimport { connect } from 'react-redux"
},
{
"path": "src/components/chat/MessageForm.js",
"chars": 6511,
"preview": "import React, { Component } from 'react';\nimport { PropTypes } from 'prop-types';\nimport { connect } from 'react-redux';"
},
{
"path": "src/components/chat/MessageList.js",
"chars": 938,
"preview": "import React, { Component } from 'react';\nimport { findDOMNode } from 'react-dom';\n\nimport Message from './Message';\n\ncl"
},
{
"path": "src/components/chat/UserIcon.js",
"chars": 370,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { FaUsers } from 'react-icons/fa';\n\nconst UserIco"
},
{
"path": "src/components/chat/UserList.js",
"chars": 1952,
"preview": "import React, { useState } from 'react';\nimport PropTypes from 'prop-types';\nimport { connect } from \"react-redux\";\n\nimp"
},
{
"path": "src/components/chat/UserStatusIcons.js",
"chars": 2231,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Tooltip, OverlayTrigger } from 'react-bootstrap"
},
{
"path": "src/components/chat/toolbar/InviteIcon.js",
"chars": 890,
"preview": "import React, { useState } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Tooltip, OverlayTrigger } from 'r"
},
{
"path": "src/components/chat/toolbar/OpenSearchIcon.js",
"chars": 1069,
"preview": "import React, { useState } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Tooltip, OverlayTrigger } from 'r"
},
{
"path": "src/components/chat/toolbar/ToggleAudioIcon.js",
"chars": 1709,
"preview": "import React, { Component } from 'react';\nimport PropTypes from 'prop-types';\nimport { connect } from 'react-redux';\n\nim"
},
{
"path": "src/components/general/AlertContainer.js",
"chars": 1240,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\nimport { connect } from 'react-redux';\n\nimport { Alert } "
},
{
"path": "src/components/general/Throbber.js",
"chars": 1479,
"preview": "import React, { Component } from 'react';\nimport ReactDOM from 'react-dom';\n\nconst throbberDotStyles = {\n 'margin': '10"
},
{
"path": "src/components/layout/ChatRoom.js",
"chars": 598,
"preview": "import React, { Component } from 'react';\n\nclass ChatRoom extends Component {\n render(){\n let username = this.props."
},
{
"path": "src/components/layout/Header.js",
"chars": 1169,
"preview": "import React, { Component, useState } from 'react';\nimport PropTypes from 'prop-types';\nimport { connect } from 'react-r"
},
{
"path": "src/components/layout/Info.js",
"chars": 696,
"preview": "import React, { useState } from 'react';\nimport PropTypes from 'prop-types';\nimport { Tooltip, OverlayTrigger } from 're"
},
{
"path": "src/components/layout/Logo.js",
"chars": 142,
"preview": "import React from 'react';\n\nconst Logo = () => {\n return (\n <div className=\"logo\">\n LeapChat\n </div>\n );\n};"
},
{
"path": "src/components/layout/Settings.js",
"chars": 885,
"preview": "import React, { useState } from 'react';\nimport { PropTypes } from 'prop-types';\n\nimport { Tooltip, OverlayTrigger } fro"
},
{
"path": "src/components/modals/InfoModal.js",
"chars": 3163,
"preview": "import React, { Component } from 'react';\nimport PropTypes from 'prop-types';\nimport { Modal } from 'react-bootstrap';\n\n"
},
{
"path": "src/components/modals/PincodeModal.js",
"chars": 2303,
"preview": "import React, { PureComponent } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Modal, Button } from 'react-"
},
{
"path": "src/components/modals/SearchModal.js",
"chars": 3315,
"preview": "import React, { Component } from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Modal, Popover, OverlayTrigger }"
},
{
"path": "src/components/modals/SettingsModal.js",
"chars": 1709,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Modal, Button, OverlayTrigger, Tooltip } from '"
},
{
"path": "src/components/modals/SharingModal.js",
"chars": 2075,
"preview": "import React from 'react';\nimport PropTypes from 'prop-types';\n\nimport { Modal, Button, OverlayTrigger, Tooltip } from '"
},
{
"path": "src/components/modals/Username.js",
"chars": 4670,
"preview": "import React, { useRef, useState } from 'react';\nimport PropTypes from 'prop-types';\nimport { connect } from 'react-redu"
},
{
"path": "src/constants/emoji.js",
"chars": 131,
"preview": "exports.EMOJI_APPLE_64_PATH = 'static/img/emoji/apple/64/';\nexports.EMOJI_APPLE_64_SHEET = 'static/img/emoji/apple/sheet"
},
{
"path": "src/constants/messaging.js",
"chars": 404,
"preview": "export const SERVER_ERROR_PREFIX = \"Error from server: \";\nexport const AUTH_ERROR = \"Error authorizing you\"; // Must mat"
},
{
"path": "src/data/constants.js",
"chars": 61279,
"preview": "export const adjectives = [\"Abdominal\", \"Able\", \"Abnormal\", \"Abrasive\", \"Active\", \"Affected\", \"Agnostic\", \"Agreeable\", \""
},
{
"path": "src/data/effWordlist.js",
"chars": 14845,
"preview": "// From https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases\n// Used by github.com/cryptag/cryptag/sha"
},
{
"path": "src/data/minishare.js",
"chars": 847,
"preview": "const crypto = window.crypto || window.msCrypto;\n\nimport effWordlist from './effWordlist';\n\n// Excluding max\n//\n// Mostl"
},
{
"path": "src/data/username.js",
"chars": 327,
"preview": "import { nouns, adjectives } from './constants';\n\nfunction getRandomAdjective(){\n return adjectives[Math.floor(Math.ran"
},
{
"path": "src/index-template.ejs",
"chars": 354,
"preview": "<!DOCTYPE html>\n<html>\n\n<head>\n <meta http-equiv=\"Content-type\" content=\"text/html; charset=utf-8\" />\n <meta name=\"vie"
},
{
"path": "src/index.js",
"chars": 1307,
"preview": "import React from 'react';\nimport { createRoot } from 'react-dom/client';\nimport { Provider } from 'react-redux';\n\nimpor"
},
{
"path": "src/static/assets.json",
"chars": 453,
"preview": "{\n\t\"stylesheets\": [\n\t\t\"static/lib/bootstrap/dist/css/bootstrap.css\",\n\t\t\"static/css/main.css\"\n\t],\n\t\"scripts\": [\n\t\t\"static"
},
{
"path": "src/static/css/Lato.css",
"chars": 175,
"preview": "@font-face {\n font-family: 'Lato';\n font-style: normal;\n font-weight: 400;\n src: local(\"Lato Regular\"), loca"
},
{
"path": "src/static/js/emoji-fixed.js",
"chars": 241903,
"preview": "\"use strict\";\n\n;(function() {\n\n var root = this;\n var previous_emoji = root.EmojiConvertor;\n\n\n /**\n\t * @global\n\t * @n"
},
{
"path": "src/static/sass/_emojiPicker.scss",
"chars": 5620,
"preview": ".emoji-mart,\n.emoji-mart * {\n box-sizing: border-box;\n line-height: 1.15;\n}\n\n.emoji-mart {\n font-family: -apple-syste"
},
{
"path": "src/static/sass/_layout.scss",
"chars": 8253,
"preview": "html, body {\n /* iOS viewport bug fix (bottom of screen covered by control bar) */\n /* CSS var `--vh` set in JS; see `"
},
{
"path": "src/static/sass/_suggestions.scss",
"chars": 1076,
"preview": ".suggestions-container {\n left: 12px;\n top: auto;\n bottom: 95px;\n width: 94%;\n border: 1px solid #ccc;\n border-bot"
},
{
"path": "src/static/sass/_variables.scss",
"chars": 19,
"preview": "$base-color: #111;\n"
},
{
"path": "src/static/sass/main.scss",
"chars": 85,
"preview": "@import 'variables';\n@import 'layout';\n@import 'emojiPicker';\n@import 'suggestions';\n"
},
{
"path": "src/store/actions/alertActions.js",
"chars": 569,
"preview": "export const ALERT_DISPLAY = 'ALERT_DISPLAY';\nexport const ALERT_DISMISS = 'ALERT_DISMISS';\n\nexport const alertSuccess ="
},
{
"path": "src/store/actions/chatActions.js",
"chars": 2711,
"preview": "\nexport const CHAT_INIT_CHAT = 'CHAT_INIT_CHAT';\nexport const CHAT_INIT_CONNECTION = 'CHAT_INIT_CONNECTION';\nexport cons"
},
{
"path": "src/store/actions/settingsActions.js",
"chars": 260,
"preview": "export const ENABLE_AUDIO = 'ENABLE_AUDIO';\nexport const DISABLE_AUDIO = 'DISABLE_AUDIO';\n\nexport const enableAudio = ()"
},
{
"path": "src/store/epics/chatEpics.js",
"chars": 6935,
"preview": "\nimport {\n CHAT_SET_USERNAME,\n CHAT_INIT_CONNECTION,\n CHAT_INIT_CHAT,\n CHAT_SEND_MESSAGE,\n connectionInitiated,\n d"
},
{
"path": "src/store/epics/helpers/ChatHandler.js",
"chars": 7346,
"preview": "import atob from 'atob';\nimport btoa from 'btoa';\nimport guid from 'guid';\nimport miniLock from '../../../utils/miniLock"
},
{
"path": "src/store/epics/helpers/createDetectPageVisibilityObservable.js",
"chars": 1299,
"preview": "import { Observable } from 'rxjs/Observable';\nimport 'rxjs/add/operator/map';\nimport 'rxjs/add/observable/fromEvent';\nim"
},
{
"path": "src/store/epics/helpers/urls.js",
"chars": 349,
"preview": "\nlet hostname;\nif (typeof window.location !== \"undefined\") {\n hostname = window.location.origin;\n} else {\n // hard-cod"
},
{
"path": "src/store/epics/index.js",
"chars": 131,
"preview": "import { combineEpics } from 'redux-observable';\nimport chatEpics from './chatEpics';\n\nexport default combineEpics(\n ch"
},
{
"path": "src/store/reducers/alertReducer.js",
"chars": 526,
"preview": "import {\n ALERT_DISPLAY,\n ALERT_DISMISS\n} from '../actions/alertActions';\n\n\nconst initialState = {\n alertMessage: '',"
},
{
"path": "src/store/reducers/chatReducer.js",
"chars": 4319,
"preview": "import {\n CHAT_INIT_CONNECTION,\n CHAT_CONNECTION_INITIATED,\n CHAT_DISCONNECTED,\n CHAT_ADD_MESSAGE,\n CHAT_SET_USER_S"
},
{
"path": "src/store/reducers/helpers/deviceState.js",
"chars": 707,
"preview": "import { USERNAME_KEY } from \"../../../constants/messaging\";\n\n\nexport const persistUsername = (username) => {\n if (type"
},
{
"path": "src/store/reducers/index.js",
"chars": 285,
"preview": "import { combineReducers } from 'redux';\nimport chatReducer from './chatReducer';\nimport alertReducer from './alertReduc"
},
{
"path": "src/store/reducers/settingsReducer.js",
"chars": 732,
"preview": "import {\n ENABLE_AUDIO,\n DISABLE_AUDIO,\n} from '../actions/settingsActions';\n\n\nconst initialState = {\n // Default to "
},
{
"path": "src/utils/audio.js",
"chars": 94,
"preview": "export function playNotification(){\n document.getElementById('notification-audio').play();\n}\n"
},
{
"path": "src/utils/chat.js",
"chars": 583,
"preview": "import { tagByPrefix, tagByPrefixStripped, parseJSON, sortRowByCreated } from './tags';\n\nexport function extractMessageM"
},
{
"path": "src/utils/crypto/nacl.js",
"chars": 66960,
"preview": "import crypto from 'crypto';\n\n// Ported in 2014 by Dmitry Chestnykh and Devi Mandiri.\n// Public domain.\n//\n// Implementa"
},
{
"path": "src/utils/crypto/scrypt.js",
"chars": 14383,
"preview": "/*!\n * Fast \"async\" scrypt implementation in JavaScript.\n * Copyright (c) 2013-2014 Dmitry Chestnykh | BSD License\n * ht"
},
{
"path": "src/utils/detect_browser.js",
"chars": 371,
"preview": "if (!window.browser) {\n const ua = navigator.userAgent;\n const browsers = ['Safari', 'MSIE', 'Firefox'];\n for (var i "
},
{
"path": "src/utils/emoji_convertor.js",
"chars": 324,
"preview": "import { EmojiConvertor } from 'emoji-js';\nimport { EMOJI_APPLE_64_PATH, EMOJI_APPLE_64_SHEET } from '../constants/emoji"
},
{
"path": "src/utils/encrypter.js",
"chars": 811,
"preview": "import { genPassphrase } from '../data/minishare';\n\nconst sha384 = require('js-sha512').sha384;\n\nconst emailDomain = '@c"
},
{
"path": "src/utils/link_attr_blank.js",
"chars": 890,
"preview": "const md = require('markdown-it')({\n html: false,\n linkify: true,\n typographer: false\n});\n\nconst assignAttributes = ("
},
{
"path": "src/utils/miniLock.js",
"chars": 20541,
"preview": "import BLAKE2s from 'blake2s';\nimport Base58 from 'bs58';\nimport nacl from './crypto/nacl';\nimport scrypt from './crypto"
},
{
"path": "src/utils/origin_polyfill.js",
"chars": 280,
"preview": "// window.location.origin polyfill, as per\n// https://stackoverflow.com/a/25495161/197160 --\nif (!window.location.origin"
},
{
"path": "src/utils/pagevisibility.js",
"chars": 1195,
"preview": "// Derived from https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API\n\nexport function detectPageVisible("
},
{
"path": "src/utils/sessions.js",
"chars": 2196,
"preview": "\n/**\n * Module intended to replace the init connection epic in chatEpics.js\n * Currently lacking connection retry capabi"
},
{
"path": "src/utils/suggestions.js",
"chars": 1337,
"preview": "import { emojiIndex } from 'emoji-mart';\n\nexport const emojiSuggestions = (cursorStart, value) => {\n const input = valu"
},
{
"path": "src/utils/tags.js",
"chars": 1696,
"preview": "const utf8 = require('utf8');\nconst atob = require('atob');\nconst btoa = require('btoa');\n\nexport function tagByPrefix(p"
},
{
"path": "src/utils/time.js",
"chars": 87,
"preview": "export function nowUTC(){\n return new Date(new Date().toUTCString().substr(0, 25));\n}\n"
},
{
"path": "src/utils/vh_fix.js",
"chars": 643,
"preview": "import { Capacitor } from '@capacitor/core';\n\nfunction vh() {\n return (window.innerHeight * 0.01) + 'px';\n}\ndocument.do"
},
{
"path": "test/.setup.js",
"chars": 239,
"preview": "'use strict';\n\nrequire('@babel/register')();\n\nconst jsdom = require(\"jsdom\");\nconst { JSDOM } = jsdom;\n\n\nglobal.document"
},
{
"path": "test/playwright/ChangeUsername.spec.js",
"chars": 573,
"preview": "import { test, expect } from '@playwright/test';\n\nconst username = \"LeapChatUser\";\n\ntest.beforeEach(async ({ page }) => "
},
{
"path": "test/playwright/InfoModal.spec.js",
"chars": 782,
"preview": "import { test, expect } from '@playwright/test';\n\nconst username = \"LeapChatUser\";\n\n\ntest.beforeEach(async ({ page }) =>"
},
{
"path": "test/playwright/InviteUsers.spec.js",
"chars": 1781,
"preview": "import { test, expect } from '@playwright/test';\n\nconst username = \"LeapChatUser\";\n\ntest.beforeEach(async ({ page }) => "
},
{
"path": "test/playwright/Message.spec.js",
"chars": 0,
"preview": ""
},
{
"path": "test/playwright/SearchModal.spec.js",
"chars": 1422,
"preview": "import { test, expect } from '@playwright/test';\n\nconst username = \"LeapChatUser\";\n\nconst messages = [\n \"Hey\",\n \"Baa b"
},
{
"path": "test/playwright/SetUsername.spec.js",
"chars": 1748,
"preview": "import { test, expect } from '@playwright/test';\n\nimport { MAX_USERNAME_LENGTH } from '../../src/components/modals/Usern"
},
{
"path": "test/playwright/SettingsModal.spec.js",
"chars": 989,
"preview": "import { test, expect } from '@playwright/test';\n\nconst username = \"LeapChatUser\";\n\ntest.beforeEach(async ({ page }) => "
},
{
"path": "test/playwright/Welcome.spec.js",
"chars": 391,
"preview": "import { test, expect } from '@playwright/test';\n\ntest.beforeEach(async ({ page }) => {\n await page.goto(\"http://localh"
},
{
"path": "test/utils/tags.test.js",
"chars": 1568,
"preview": "import { expect } from 'chai';\n\nimport { tagByPrefix, cleanedFields } from '../../src/utils/tags';\n\ndescribe('tags', fun"
},
{
"path": "webpack.config.base.js",
"chars": 1945,
"preview": "const path = require('path');\nconst MiniCssExtractPlugin = require(\"mini-css-extract-plugin\");\nconst HtmlWebpackPlugin ="
},
{
"path": "webpack.config.dev.js",
"chars": 555,
"preview": "const config = require('./webpack.config.base');\n\nconst MiniCssExtractPlugin = require(\"mini-css-extract-plugin\");\n\nconf"
},
{
"path": "webpack.config.prod.js",
"chars": 927,
"preview": "const webpack = require('webpack');\nconst config = require('./webpack.config.base');\nconst MiniCssExtractPlugin = requir"
}
]
About this extraction
This page contains the full source code of the cryptag/leapchat GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 122 files (583.7 KB), approximately 252.4k tokens, and a symbol index with 235 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.