A space where socially-distanced ringers can practice together.
Get the CSS set up with sass:
- Install dart-sass (e.g.
brew install sass/sass/sass
); https://github.com/sass/dart-sass/releases - (Optional) Create & activate a virtual environment;
- Install python dependencies
pip install -r requirements.txt
- In the project root, run
sass app/static/sass/:app/static/css/
. This will compile the sass to css.
Get the DB set up with Flask:
- In the project root, run
flask db upgrade
You are now ready to run the server:
- In the project root, run
flask run
- This will give you a local address where you can access the app
A feature flag is an environment variable used to enable or disable features at runtime.
They enable the a given feature if they are set to 1
.
If they are not set or set to anything other than 1
, the feature will be disabled.
-
RR_ENABLE_WHEATLEY
If set to
1
Wheatley will be enabled, otherwise Wheatley will be disabled. -
RR_WHEATLEY_METHOD_EXTENSION
If the stage is changed and this is set to
1
, Wheatley will try to load the same method on the new stage. This is disabled by default, because this check sends a request to Bob Wallis' blueline website which can block the thread for a long time and therefore cause the method change to be too late (if it happens at all).
-
RR_WHEATLEY_PATH
Set this if you want to run a version of Wheatley that isn't the latest stable version. This has to point to the file called
run-wheatley
inside the Wheatley repo.For example:
export RR_WHEATLEY_PATH=/path/to/wheatley/run-wheatley flask run
-
RR_SOCKETIO_PORT
Currently used only by Wheatley; defaults to 5000 (the same as Flask), but should be set to 8080 for a production server.
Ringing Room supplies a basic API for use in 3rd-party apps.
Endpoint | Method | Description |
---|---|---|
/api/version |
GET |
Get API version information |
/api/tokens |
POST |
Get bearer token |
/api/tokens |
DELETE |
Revoke bearer token |
/api/user |
GET |
Get current user details |
/api/user |
POST |
Register new user |
/api/user |
PUT |
Modify user settings |
/api/user |
DELETE |
Delete user account |
/api/user/reset_password |
POST |
Trigger password reset email |
/api/user/keybindings |
GET |
Get keybindings |
/api/user/keybindings |
POST |
Update keybinding |
/api/user/keybindings |
DELETE |
Reset keybindings |
/api/user/controllers |
GET |
Get motion controller parameters |
/api/user/controllers |
POST |
Update motion controller parameters |
/api/user/controllers |
DELETE |
Reset motion controller parameters |
/api/my_towers |
GET |
Get all towers related to current user |
/api/my_towers/<tower_id> |
PUT |
Toggle bookmark for tower_id |
/api/my_towers/<tower_id> |
DELETE |
Remove tower_id from recent towers |
/api/tower/<tower_id>/settings |
GET |
Get tower settings (if permitted) |
/api/tower/<tower_id>/settings |
PUT |
Modify tower settings (if permitted) |
/api/tower/<tower_id>/hosts |
POST |
Add hosts (if permitted) |
/api/tower/<tower_id>/hosts |
DELETE |
Remove hosts (if permitted) |
/api/tower/<tower_id> |
GET |
Get connection details for tower_id |
/api/tower |
POST |
Create new tower |
/api/tower/<tower_id> |
DELETE |
Delete tower (if permitted) |
GET /api/version
: Gets version information. Responds with the fields:
version
: the overall RR version (which takes the formYY.WW
, for year and week of release)api-version
: the api version, which is semantically versionedsocketio-version
: the socketio version, which is semantically versioned
Initial authorization uses HTTP Basic Auth: POST
to /api/tokens
with the header Authorization: Basic <credentials>
, where <credentials>
is a base-64-encoded email:password
. The response will include a bearer token valid for 24 hours.
All other endpoints (except POST /api/user
for registering new users) require the header Authorization: Bearer <token>
.
GET /api/user
: Gets user details. Responds with a JSON including the fields username
& email
.
POST /api/user
: Registers new user. Request must include a JSON with fields username
, email
, & password
. Responds as per GET /api/user
. (Does not require Bearer token.)
PUT /api/user
: Modifies user details Request JSON may include new_username
, new_email
, new_password
. Responds as per GET /api/user
.
DELETE /api/user
: Deletes user.
POST /api/user/reset_password
: Must include an email
field in the request JSON; if that email is associated with an account, an email will be sent to reset the password to that account. (Note: For security reasons, this endpoint will always respond with code 200 OK, no matter what email address was included.)
GET /api/user/keybindings
: Gets all keybindings. Responds with a JSON including fields for each function (e.g "Call Bob" or "Ring left-hand bell"), with values a list of keybindings. (See config.py
for the default keybindings and a list of all fields this will return.)
POST /api/user/keybindings
: Update a specific keybinding. Request must include a JSON with a single field named for one of the functions where the value is the complete list of keys bound to that function. Responds as per GET /api/user/keybindings
.
DELETE /api/user/keybindings
: Reset either a specific keybinding or all keybindings. Request must include a string which is either a) the name of a specific keybinding function or b) the special key ALL_KEYBINDINGS
. Responds as per GET /api/user/keybindings
.
GET /api/user/controllers
: Gets all controller parameters. Responds with a JSON including fields for each parameter. See config.py
for the default parameters and a list of all fields this will return.
POST /api/user/controllers
: Update a specific controller parameter. Request must include a JSON with a single field named for one of the controller parameters. Responds as per GET /api/user/controllers
.
DELETE /api/user/controllers
: Resets all controller parameters. Requet body is ignored. Responds as per GET /api/user/controllers
.
GET /api/my_towers
: Gets all related towers:
{
"928134567": {
"bookmark": 0,
"creator": 1,
"host": 1,
"recent": 1,
"tower_id": 928134567,
"tower_name": "Advent",
"visited": "Mon, 31 Aug 2020 15:45:54 GMT"
},
"987654321": {
"bookmark": 0,
"creator": 0,
"host": 0,
"recent": 1,
"tower_id": 987654321,
"tower_name": "Old North",
"visited": "Mon, 31 Aug 2020 15:44:40 GMT"
}
}
PUT /my_towers/<tower_id>
: Toggles the bookmark
value for that tower. Responds as per GET /api/my_towers
but with only the details for the requested tower.
DELETE /my_towers/<tower_id>
: Removes the tower from the current user's recent towers. Responds as per GET /api/my_towers
but with only the details for the requested tower.
GET /api/tower/<tower_id>
: Gets connection information for the tower, including tower settings. Response JSON includes tower_id
, tower_name
, server_address
, additional_sizes_enabled
, host_mode_permitted
, half_muffled
, and fully_muffled
.
POST /api/tower
: Creates a new tower. Request JSON should include tower_name
. Responds as per GET /api/tower/<tower_id>
.
DELETE /api/tower/<tower_id>
: Deletes the tower, if the current user has permission to do so.
GET /api/tower/<tower_id>/settings
: Gets tower settings, if the current user has permission to modify them. Response JSON includes host_mode_enabled
, tower_id
, tower_name
, and hosts
, a list of objects containing email
& username
.
PUT /api/tower/<tower_id>/settings
: Modifies tower settings, if the current user has permission to do so. Request JSON may include tower_name
, permit_host_mode
, additional_sizes_enabled
, half_muffled
, fully_muffled
. Responds as per GET /api/tower/<tower_id>/settings
.
POST /api/tower/<tower_id>/hosts
: Adds new hosts, if the current user has permission to do so. Request JSON must include new_hosts
, a list of email addresses. Responds as per GET /api/tower/<tower_id>/settings
.
DELETE /api/tower/<tower_id>/hosts
: Remove hosts, if the current user has permission to do so. Request JSON must include hosts
, a list of email addresses. Responds as per GET /api/tower/<tower_id>/settings
.
All communication between the API consumer and an individual tower should take place through SocketIO. The basic workflow for setting up communication is:
- Establish a connection with the
server_address
returned byGET /api/tower/<tower_id>
. - Emit
c_join
with a JSON payload containingtower_id
,user_token
(the Bearer token), andanonymous_user
. At present, our API doesn't support anonymous users, so this should always have the valuefalse
. - Listen for
s_set_userlist
,s_size_change
,s_audio_change
,s_host_mode
,s_user_entered
, ands_assign_user
to set up the tower. - Once you've set up the rope circle, emit
c_request_global_state
and listen fors_global_state
to set the bells at back/hand. - Ring!
- Emit
c_user_left
when leaving.
Communication between client & server is handled by Socket.IO events.
Events are prefixed by origin:
c_
for clients_
for server
What follows is a incomplete list of events — these should be only the events relevant to an API consumer (i.e. where functionality is not duplicated elsewhere).
Event | Payload | Description |
---|---|---|
c_join |
{tower_id: Int, user_token: Str, anonymous_user: Bool} |
User joined a tower. |
s_user_entered |
{user_id: Int, username: Str} |
Server relayed user entering. |
c_user_left |
{user_name: Str, user_token: Str, anonymous_user: Bool, tower_id: Int} |
User left a tower. |
s_user_left |
{user_id: Int, username: Str} (username is depricated) |
Server relayed user leaving. |
c_request_global_state |
{tower_id: Int} |
Client requested tower state. |
s_global_state |
{global_bell_state: [Bool]} |
Server sent current tower state. |
s_set_userlist |
{user_list: [{user_id: Int, username: Str}]} |
Server set list of users in tower. |
c_bell_rung |
{bell: Int, stroke: Bool, tower_id: Int} |
User rang a bell. |
s_bell_rung |
{global_bell_state: [Bool], who_rang: Int, disagreement: Bool} |
Server relayed bell ringing. |
c_assign_user |
{bell: Int, user: Int, tower_id: Int} |
User assigned someone to a bell. |
s_assign_user |
{bell: Int, user: Int} |
Server sent bell assignment. |
c_audio_change |
{new_audio: ("Tower" | "Hand"), tower_id: Int} |
User changed audio type. |
s_audio_change |
{new_audio: ("Tower" | "Hand")} |
Server sent audio state. |
c_host_mode |
{new_mode: Bool, tower_id: Int} |
User toggled host mode. |
s_host_mode |
{tower_id: Int, new_mode: Bool} |
Server sent host mode. |
c_size_change |
{new_size: Int, tower_id: Int} |
User changed tower size. |
s_size_change |
{size: Int} |
Server sent tower size. |
c_msg_sent |
{user: Str, email: Str, msg: Str, time: Date, tower_id: Int} |
User sent a chat. |
s_msg_sent |
{user: Str, email: Str, msg: Str, time: Date, tower_id: Int} |
Server relayed chat. |
c_call |
{call: Str, tower_id: Int} |
User made a call. |
s_call |
{call: Str, tower_id: Int} |
Server relayed user call. |
c_set_bells |
{tower_id: Int} |
User set all bells at hand. |
s_bad_token |
(variable) | The user send a bad bearer token. (Payload repeats whatever triggered this response.) |
The integration of Wheatley into Ringing Room has added a number of extra SocketIO signals, used
for keeping Wheatley in sync with the rest of Ringing Room. Some of these signals have custom types
(RowGen
and Settings
), which are described in detail below the table.
Event | Payload | Description |
---|---|---|
s_set_wheatley_enabledness |
{enabled: Bool} |
Emitted by the server to the current users of a tower whenever the "Wheatley enabled" switch is changed in the tower settings |
c_wheatley_setting |
{tower_id: Int, settings: Settings} |
(from a client) tells Wheatley to change one of its settings |
s_wheatley_setting |
Settings |
(from the server) tells Wheatley to change one of its settings, and for all the clients to update their views of that setting. To prevent rubber-banding of controls, this signal will not be sent to the client that emitted the corresponding c_wheatley_setting signal. |
c_wheatley_row_gen |
{tower_id: Int, row_gen: RowGen} |
(from a client) tells Wheatley to use different Row Generation settings next time a Look to is called. |
s_wheatley_row_gen |
RowGen |
(from the server) tells Wheatley to use a new Row Generator, and for all the clients to update their views of that setting. |
c_wheatley_is_ringing |
{tower_id: Int, is_ringing: Bool} |
sent from Wheatley to inform the other clients whether or not Wheatley thinks that people are ringing. This also locks or unlocks the row gen box. |
s_wheatley_is_ringing |
{is_ringing: Bool} |
broadcast from the server after Wheatley sends c_wheatley_is_ringing |
c_wheatley_stop_touch |
{tower_id: Int} |
tells the server to broadcast s_wheatley_stop_touch |
s_wheatley_stop_touch |
{} |
broadcast by the server to tell Wheatley to stop ringing |
c_reset_wheatley |
{tower_id: Int} |
tells the server to kill the current Wheatley instance(s). Used as a last-ditch way to reset Wheatley if he gets his knickers in a twist. |
c_roll_call |
{tower_id: Int, instance_id: Int} |
sent by Wheatley instances in reply to Look To to say that they are ready to ring |
The Settings type is an object with 0 or more of the following properties:
sensitivity : float; 0 <= x <= 1 (currently unused)
use_up_down_in : Bool
stop_at_rounds : Bool
peal_speed : int; x >= 0
call_composition : Bool
fixed_striking_interval : Bool (ignored by Wheatley, changes `peal_speed` when the tower size is changed)
The RowGen type is a JSON representation of the following structured enum
(it's either a Method
with a title
, a stage
, etc.
or it's a Composition
with a url
and title
):
enum RowGen {
Method {
title: String,
stage: Int,
notation: String,
url: String,
bob: Map<Int, String>,
single: Map<Int, String>
},
Composition {
url: String,
title: String
}
}
The Int
s in the call maps correspond to indices within the lead, and the String
s are the place notations
that should be made at that position. In JSON, the RowGen
type corresponds to one of the following
objects:
{
type: "method",
title: String,
stage: Int,
notation: String,
url: String,
bob: { Int: String },
single: { Int: String }
}
/* or */
{
type: "composition",
url: String,
title: String
}
.
├── LICENSE
├── README.md
├── app/
│ ├── __init__.py
│ ├── api/
│ │ ├── __init__.py
│ │ ├── auth.py
│ │ ├── errors.py
│ │ ├── tokens.py
│ │ └── routes.py
│ ├── email.py
│ ├── forms.py
│ ├── listeners.py
│ ├── models.py
│ ├── routes.py
│ ├── static
│ │ ├── audio/
│ │ │ ├── hand.ac3
│ │ │ ├── hand.json
│ │ │ ├── hand.m4a
│ │ │ ├── hand.mp3
│ │ │ ├── hand.ogg
│ │ │ ├── processed_audio/
│ │ │ ├── raw_audio/
│ │ │ ├── tower.ac3
│ │ │ ├── tower.json
│ │ │ ├── tower.m4a
│ │ │ ├── tower.mp3
│ │ │ └── tower.ogg
│ │ ├── audio.js
│ │ ├── bootstrap/
│ │ ├── dark-mode-switch/
│ │ ├── downloads/
│ │ ├── howler.core.min.js
│ │ ├── images/
│ │ │ ├── dncb.png
│ │ │ ├── favicon
│ │ │ ├── h-backstroke.png
│ │ │ ├── h-handstroke-treble.png
│ │ │ ├── h-handstroke.png
│ │ │ ├── logo.png
│ │ │ ├── t-backstroke.png
│ │ │ ├── t-handstroke-treble.png
│ │ │ ├── t-handstroke.png
│ │ ├── landing.js
│ │ ├── my_towers.js
│ │ ├── ringing_room.js
│ │ └── sass/
│ │ ├── circle.scss
│ │ ├── global_design.scss
│ │ ├── ringing_room.scss
│ │ └── static.scss
│ └── templates/
│ ├── _code_of_conduct.html
│ ├── _news_toast.html
│ ├── _privacy_policy.html
│ ├── _user_menu.html
│ ├── about.html
│ ├── authenticate.html
│ ├── base.html
│ ├── blog.html
│ ├── code_of_conduct.html
│ ├── contact.html
│ ├── donate.html
│ ├── email/
│ │ ├── reset_password.html
│ │ └── reset_password.txt
│ ├── help.html
│ ├── landing_page.html
│ ├── my_towers.html
│ ├── news/
│ ├── reset_password.html
│ ├── reset_password_request.html
│ ├── ringing_room.html
│ ├── tower_settings.html
│ └── user_settings.html
├── config.py
├── logs/
├── migrations/
├── requirements.txt
└── ringingroom.py