Kokkoja: first public assembly
Assembled from the development repository by tools/make_release.sh. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
commit
95c80d04c9
39 files changed
+9905
No files matched your search
+244
@@ -0,0 +1,244 @@
|
||||
# A guest's Kokkoja
|
||||
|
||||
This part describes Kokkoja as a guest sees it: someone invited to a
|
||||
session by its host. In a lesson, the guest is the student. The host's
|
||||
screen is the same with a few more buttons, described in
|
||||
[A host's Kokkoja](host.md).
|
||||
|
||||
## The invitation
|
||||
|
||||
The host sends you an invitation by whatever means you both use: email, a
|
||||
chat message, a text message. It is a link of this shape:
|
||||
|
||||
https://kokkoja.andykopra.com/#join=wss://kokkoja.andykopra.com/relay%23uQ5c__CGB4FvN64Qiv3jGA.aS_1O51Umk1YHXwgqBZANw
|
||||
|
||||
The part after `join=` is the invitation itself, which the host's Kokkoja
|
||||
may also give you on its own, without the link around it. Both forms
|
||||
work wherever an invitation is asked for. The invitation carries the key
|
||||
that protects the session, so pass it on only to people who belong in the
|
||||
session, and treat it as you would a meeting link. An invitation is good
|
||||
for one session: the host's next session has a new one.
|
||||
|
||||
## Joining
|
||||
|
||||
Open the link in a web browser. Firefox, Brave and Chrome are known to
|
||||
work; other browsers built on the same engines very probably do. You see
|
||||
Kokkoja's start screen with the invitation already filled in. Enter the
|
||||
name the others should see and press **Join**. That is all: nothing is
|
||||
installed, and there is no account to make.
|
||||
|
||||
Your name is remembered by the browser for next time. So is a second
|
||||
field, **Notebook for the pages of this session**, which you can leave as
|
||||
it is for now; what it does is explained under "Your notebook" below.
|
||||
|
||||
If you have the Kokkoja program installed on your computer (see
|
||||
[A host's Kokkoja](host.md) for what that is and why you might want it),
|
||||
the start screen also offers to open the invitation in it instead. If
|
||||
you have not, ignore that line.
|
||||
|
||||
If the host has not started yet, or has left, you are still in the
|
||||
session. The page is shared among whoever is present, and the host's
|
||||
place in it waits for the host to return.
|
||||
|
||||
## The screen
|
||||
|
||||
Across the top is the toolbar. On the left are the tools and their
|
||||
settings; on the right, the zoom and, for the host, the buttons that
|
||||
start pages. Below it, the page fills most of the window, with a panel on
|
||||
the right. Every button explains itself: rest the pointer on it for a
|
||||
second and its explanation appears beneath it.
|
||||
|
||||
### The page
|
||||
|
||||
The page is an A4 sheet, shown so that it fits the window. Write
|
||||
anywhere on it. When your writing comes near an edge, the sheet grows in
|
||||
that direction, in small steps, so there is always room, and the view
|
||||
keeps its scale and extends beyond the window; scroll to reach the rest.
|
||||
An image the host puts on the page lies at the top, or below the images
|
||||
already there.
|
||||
|
||||
The page as a whole is shared: everyone sees the same marks in the same
|
||||
places. Your view of it is your own: how far it is zoomed and where it
|
||||
is scrolled to concern nobody else.
|
||||
|
||||
### Pen
|
||||
|
||||
**Pen** is chosen when you arrive. Draw with a pen on a tablet, a mouse,
|
||||
or a finger on a touch screen. With a pen, pressing harder makes the
|
||||
line wider. The colours are to the right of the tools: black, blue, red
|
||||
and green, and **+** opens a palette of more. A colour chosen from the
|
||||
palette joins the row of colours and stays there in this browser. The
|
||||
three lines to the right of the colours are the line widths: 0.4, 0.7
|
||||
and 1.2 mm at ordinary pressure.
|
||||
|
||||
If a pen is in use, a hand resting on a touch screen is ignored.
|
||||
|
||||
### Text
|
||||
|
||||
**Text** types on the page. Tap where the text should begin and type; a
|
||||
text field appears there and the text is laid out on the page as you
|
||||
type. Enter begins a new line. Press Escape, or tap somewhere else, to
|
||||
finish; a block with nothing in it disappears.
|
||||
|
||||
A block made by a tap does not wrap: its lines are as long as you type
|
||||
them, and the page grows to hold them. To make the text wrap at a chosen
|
||||
width, drag along where its top edge should run instead of tapping: the
|
||||
block begins at the left end of the drag and wraps at the right end.
|
||||
|
||||
Tap your own text to change it. While a block of yours is being edited
|
||||
it is drawn grey, and three handles appear: the one at its top-left
|
||||
corner and the line along its top edge move it, the one at the right end
|
||||
of that line changes its width. You cannot change anyone else's text.
|
||||
|
||||
With the Text tool, the row of colours stays and the line widths give way
|
||||
to the font, one of **Sans**, **Serif** and **Mono**, and the size in
|
||||
points, 14 unless you change it. Both apply to the block you are typing
|
||||
and to the next ones.
|
||||
|
||||
### Eraser
|
||||
|
||||
**Eraser** removes your own writing. Move it across a stroke and the
|
||||
whole stroke goes; across a text block and the block goes. It removes
|
||||
nothing of anyone else's, and nobody else's eraser can touch yours. An
|
||||
erased stroke is gone from the page for everyone.
|
||||
|
||||
### Zoom
|
||||
|
||||
**Fit** shows the whole page in the window, and keeps doing so as the
|
||||
page grows by writing, except while you are in the middle of a stroke of
|
||||
your own. **+** and **−** zoom in and out by a tenth. Holding Ctrl and
|
||||
turning the scroll wheel zooms about the pointer. When the page is
|
||||
larger than the window, scroll it as you would any page. When the host
|
||||
adds an image, the view keeps its scale rather than shrinking to fit; a
|
||||
new image at the bottom of a long page is reached by scrolling.
|
||||
|
||||
If you reload the browser tab, it comes back to the page as you last saw
|
||||
it: same zoom, same place.
|
||||
|
||||
### The panel
|
||||
|
||||
**Participants** lists everyone in the session, with *you* and *host*
|
||||
marked, and *away* beside anyone who has left or lost their connection;
|
||||
an absent participant's writing stays on the page. The box in front of
|
||||
each name shows or hides that participant's writing on your screen alone.
|
||||
Hiding your teacher's corrections to try an exercise again, or hiding
|
||||
your own answers to see the exercise as it was, changes nothing for
|
||||
anyone else.
|
||||
|
||||
**Invitation** shows the invitation of this session, with a button to
|
||||
copy it, in case someone else should be invited.
|
||||
|
||||
**Current notebook**, **Keep page** and **Export** concern your notebook,
|
||||
below. Under them, a line reports what was last saved, and whether
|
||||
anything went wrong.
|
||||
|
||||
The last line of the panel reports what your pen or mouse is delivering:
|
||||
the kind of device, the pressure, and how many positions per second it
|
||||
sends. It is there for curiosity and for finding faults.
|
||||
|
||||
### Status
|
||||
|
||||
At the right end of the toolbar, **Page 3** is the number of the page you
|
||||
are on, counted from the start of the session, and beside it *Connected*,
|
||||
*Connecting…* or *Disconnected* says how you stand with the relay. If
|
||||
the connection drops, Kokkoja reconnects by itself; what you wrote while
|
||||
disconnected is sent when it returns. Until the host has started a
|
||||
page, the toolbar says *Waiting for the host's page*.
|
||||
|
||||
## Your notebook
|
||||
|
||||
Every page of the session is saved for you, as it changes, without your
|
||||
doing anything. Where it is saved depends on how you joined.
|
||||
|
||||
### In the browser
|
||||
|
||||
When you joined by opening the link, your notebook is kept inside the
|
||||
browser, on the computer you are using. It stays there between sessions,
|
||||
but it belongs to that browser: another browser, or another computer,
|
||||
has its own. The page you are on is saved there continuously. When the
|
||||
session moves to another page, the page you are leaving is kept under a
|
||||
dated name, such as `2026_10_06_01.svg`, the date being the day it was
|
||||
last changed and the number counting the pages kept that day.
|
||||
|
||||
Two buttons bring pages out of the browser:
|
||||
|
||||
* **Keep page** keeps a dated copy of the page as it is now, and
|
||||
downloads it, so you find it among your browser's downloads. Pressing
|
||||
it again before anything has changed makes no second copy.
|
||||
* **Export** downloads a picture of the page as you see it, as a PNG
|
||||
file; writing you have hidden is left out. Its name is the date and
|
||||
the page number.
|
||||
|
||||
A page file is an SVG file. Any web browser displays it, and vector
|
||||
drawing programs such as Inkscape open it. Kokkoja itself can load one
|
||||
again, writing and all, when someone with the right to start pages
|
||||
chooses it (see "When the host gives you more" below, and the host's
|
||||
part).
|
||||
|
||||
The notebook field on the start screen names the notebook within the
|
||||
browser's storage. Giving each course or each colleague a notebook of
|
||||
their own keeps their pages apart; the name proposed is simply
|
||||
*Notebook*. **Current notebook** in the panel shows the notebook in use
|
||||
and lets you change it during the session.
|
||||
|
||||
### On your computer
|
||||
|
||||
If you would rather have your pages as files in a directory on your own
|
||||
computer, where you can open, copy and back them up as you do any file,
|
||||
install the Kokkoja program. It is small and needs only Python, which
|
||||
Linux and macOS have or can quickly get; [A host's Kokkoja](host.md)
|
||||
tells how, since every host needs it. Then join from the program's own
|
||||
start screen, pasting the invitation, or follow the start screen's offer
|
||||
to open the invitation in the installed program.
|
||||
|
||||
With the program, a notebook is a directory in `Kokkoja` in your
|
||||
Documents directory. The page you are on is `Current.svg` there, and
|
||||
kept pages lie beside it under their dated names. **Keep page** makes
|
||||
the dated copy; **Export** opens Kokkoja's own file chooser, in which you
|
||||
pick a directory and a name for the PNG file.
|
||||
|
||||
The program also keeps your identity, so that you are the same
|
||||
participant in every session and from any browser on that computer, and
|
||||
writing from an earlier lesson is still yours when its page is loaded
|
||||
again. In the browser, your identity belongs to that browser.
|
||||
|
||||
## Leaving and returning
|
||||
|
||||
Close the tab and you have left. Nothing needs saving. If you come back
|
||||
to the same session by opening the invitation again, your writing is
|
||||
still on the page, from the others' copies and from your notebook, and
|
||||
it is still yours to erase or change. A tab that was in a session and is
|
||||
reloaded goes straight back in.
|
||||
|
||||
## When the host gives you more
|
||||
|
||||
Normally only the host puts images on the page, starts new pages, or
|
||||
loads pages from a notebook. The host can give you the same right, for
|
||||
instance in a discussion among colleagues where everyone brings
|
||||
material. When that happens, three more buttons appear at the right of
|
||||
your toolbar:
|
||||
|
||||
* **New** starts a new blank page for everyone. The page being left is
|
||||
kept in each person's notebook.
|
||||
* **Image** puts an image file on the page for everyone, below any
|
||||
images already there. Your browser's file chooser asks for the file
|
||||
(with the Kokkoja program, Kokkoja's own chooser does, beginning where
|
||||
the notebooks are, with a choice whether an image wider than the page
|
||||
is shrunk to fit it). A PNG or JPEG file keeps its exact pixels unless
|
||||
it is very large; other formats and very large images are converted
|
||||
and reduced before they are shared. An image is placed at its natural
|
||||
size, centred, 8 mm from the top or from the image above; one wider
|
||||
than the sheet is scaled down to fit between the margins. To put an
|
||||
image on a page of its own, press **New** first.
|
||||
* **Notebook** starts a new page for everyone from a page kept in one of
|
||||
your notebooks, with everything that was written on it. A chooser
|
||||
shows your notebooks and small pictures of their kept pages. The kept
|
||||
page itself is not changed: the session works on a copy.
|
||||
|
||||
An image cannot be moved or removed once anyone has written on it or
|
||||
added anything after it, since the marks on it would make no sense
|
||||
without it. The one exception is a mistake caught at once: the person
|
||||
who added an image can take it back with a tap of the eraser on it, as
|
||||
long as nothing else has happened on the page since.
|
||||
|
||||
The right stays with you until the host withdraws it or the session ends.
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
# A host's Kokkoja
|
||||
|
||||
The host starts a session, invites the guests and drives the session:
|
||||
puts images on the page, starts new pages, loads pages from a notebook,
|
||||
and decides which guests may do the same. In a lesson, the teacher is
|
||||
the host. Everything in [A guest's Kokkoja](guest.md) applies to the host
|
||||
too; this part describes what is added, and how to install the Kokkoja
|
||||
program, which a host needs.
|
||||
|
||||
## Installing the Kokkoja program
|
||||
|
||||
A guest can join from a web site with nothing installed. A host runs the
|
||||
Kokkoja program on their own computer. It is a small Python program with
|
||||
two jobs: it shows Kokkoja in your web browser, and it reads and writes
|
||||
your notebooks as ordinary directories and files. It listens only to
|
||||
your own computer and talks to nothing else but the relay you name.
|
||||
|
||||
You need:
|
||||
|
||||
* **Linux or macOS.** Windows has not been tried yet.
|
||||
* **Python 3.** Linux has it. On macOS, open Terminal and type
|
||||
`python3 --version`; if Python is not installed, macOS offers to
|
||||
install the command line developer tools, which include it. Any Python
|
||||
from 3.9 on will do.
|
||||
* **A web browser.** Firefox, Brave or Chrome; other browsers built on
|
||||
the same engines very probably work.
|
||||
* **A pen tablet**, if you want to write by hand. A mouse works for
|
||||
pointing and for rough marks.
|
||||
|
||||
Get Kokkoja from its repository, either by downloading it as a zip file
|
||||
and unpacking it, or with git:
|
||||
|
||||
git clone https://git.andykopra.com/ack/kokkoja-rel.git kokkoja
|
||||
|
||||
Nothing else is installed and nothing is changed on your system. To
|
||||
update later, download again or run `git pull` in that directory. To
|
||||
remove Kokkoja, delete the directory; your notebooks are elsewhere and
|
||||
stay.
|
||||
|
||||
## Starting Kokkoja
|
||||
|
||||
Open a terminal in the Kokkoja directory and run
|
||||
|
||||
python3 kokkoja.py --relay wss://kokkoja.andykopra.com/relay
|
||||
|
||||
The program prints an address beginning `http://localhost:8760/` and
|
||||
opens your browser there. Leave the terminal open: the program must run
|
||||
as long as you are in a session. Ctrl-C stops it.
|
||||
|
||||
The address the program prints contains a secret token. Your browser
|
||||
remembers it, so afterwards `http://localhost:8760/` alone brings Kokkoja
|
||||
back; but if the browser ever says that the notebooks are locked, open
|
||||
the full address the program printed. The token is what stops any other
|
||||
web page you might have open from reading your notebooks.
|
||||
|
||||
The relay address names the *Kokkoja server* you use. A server does two
|
||||
things: it passes the participants' messages, encrypted so that it
|
||||
cannot read them, and it serves the web site that your guests join from.
|
||||
The address above is Andy's server; whoever runs a server for you gives
|
||||
you its address. If you give the program no relay, it expects one on
|
||||
your own computer, which is useful only for trying Kokkoja alone.
|
||||
|
||||
Other options, all optional:
|
||||
|
||||
* `--notebooks DIRECTORY` keeps the notebooks somewhere other than
|
||||
`Kokkoja` in your Documents directory.
|
||||
* `--port NUMBER` uses another port than 8760, if that one is taken.
|
||||
* `--no-browser` does not open a browser; open the printed address
|
||||
yourself.
|
||||
* `--site ADDRESS` names the web site that invitations link to. You
|
||||
need it only if your server's web site is not at the relay's host.
|
||||
|
||||
## Starting a session
|
||||
|
||||
On the start screen, enter your name and the notebook for this session,
|
||||
and press **Start a session**. You are on page 1, a blank sheet, and the
|
||||
panel on the right shows the invitation.
|
||||
|
||||
### The notebook
|
||||
|
||||
The notebook you name is a directory under `Kokkoja` in your Documents
|
||||
directory, made when it is first needed, and the start screen offers the
|
||||
names of the notebooks you already have. For teaching, a notebook per
|
||||
student works well: everything a student is given, with everything they
|
||||
and you wrote on it, gathers in that student's notebook, and the next
|
||||
lesson with that student starts by naming it again. Your library of
|
||||
exercises can live in notebooks of its own, such as *Exercises A1*, filled
|
||||
by keeping pages in a session of your own with nobody else present.
|
||||
|
||||
The notebook can be changed during a session, in **Current notebook** in
|
||||
the panel. The page you are on then continues in the new notebook.
|
||||
|
||||
### The invitation
|
||||
|
||||
**Invitation** in the panel shows a link. **Copy** copies it; send it to
|
||||
your guests by email, in a chat message, or however you reach them. A
|
||||
guest opens it, enters a name and is in. A guest with the Kokkoja
|
||||
program installed may paste it into their own program instead. The link
|
||||
carries the secret that protects the session, so send it only to those
|
||||
who should be there; anyone with the link can join and read the page.
|
||||
Each session you start has a new invitation.
|
||||
|
||||
An invitation is good as long as you keep the session open. A guest can
|
||||
join at any time, see everything written so far, and leave and return.
|
||||
|
||||
## Driving the session
|
||||
|
||||
Three buttons at the right of the toolbar are the host's.
|
||||
|
||||
**New** starts a new blank page for everyone. Pages are numbered from 1
|
||||
through the session. The page being left is kept in every participant's
|
||||
notebook, by itself, under a dated name; nobody has to remember to save.
|
||||
|
||||
**Image** puts an image file on the page for everyone. Kokkoja's own
|
||||
file chooser opens, beginning where your notebooks are and remembering
|
||||
where you last chose from; a directory path can also be typed into it.
|
||||
It shows small pictures of the image files it finds. Choose one and it
|
||||
is placed on the page: the first at the top of the sheet, each further
|
||||
one 8 mm below the lowest image there, centred, at its natural size. An
|
||||
image wider than the sheet is scaled down to fit between the margins, so
|
||||
a full-page A4 scan appears at about 92%; the chooser has a setting to
|
||||
turn that off, in which case the sheet widens to the image instead. PNG
|
||||
and JPEG files are shared as they are unless very large; other formats,
|
||||
and images over 2400 pixels on a side or 1.5 MB, are converted to JPEG
|
||||
and reduced first. A page holds up to 12 images.
|
||||
|
||||
For a page that begins with an image, press **New** and then **Image**.
|
||||
For a series of exercises, keep pressing **Image** on the same page: the
|
||||
sheet grows downward and everyone scrolls through them. A new image does
|
||||
not change anyone's zoom; it appears below, to be scrolled to.
|
||||
|
||||
If you chose the wrong file, a tap of the eraser on the image takes it
|
||||
back, as long as nothing else has happened on the page since: no stroke,
|
||||
no text, no erasure, no further image, by anyone. After that an image
|
||||
stays, because the marks made on it would make no sense without it.
|
||||
|
||||
**Notebook** starts a new page for everyone from a page kept in one of
|
||||
your notebooks, with everything that was written on it. A chooser shows
|
||||
your notebooks on the left and small pictures of the chosen notebook's
|
||||
kept pages on the right. The loaded page is a copy: the kept page is
|
||||
not changed by anything that happens next. Each layer on it still
|
||||
belongs to the participant who wrote it, so a student's answers from an
|
||||
earlier lesson are still theirs, and your corrections still yours.
|
||||
|
||||
This is also how you return to an earlier page of the session: the page
|
||||
was kept when you left it, and **Notebook** brings it back as a new page.
|
||||
A lesson might go: **New**, **Image** for the exercise, work, **New**
|
||||
for a point of grammar, work, **Notebook** to return to the exercise.
|
||||
|
||||
## Guests' rights
|
||||
|
||||
In the **Participants** list, each guest's name has a button **pages** in
|
||||
front of it. Pressing it lets that guest start new pages, put images on
|
||||
the page and load pages from their notebook, exactly as you can; the
|
||||
three buttons appear on their toolbar. Pressing it again withdraws the
|
||||
right, which otherwise lasts until the session ends. Guests can never erase or
|
||||
change anyone's writing but their own; that needs no setting.
|
||||
|
||||
## Hiding writing
|
||||
|
||||
The box in front of each participant's name shows or hides that
|
||||
participant's writing on your screen only. Hide a student's answers to
|
||||
see the exercise clean, hide your own corrections to see what the
|
||||
student sees, show them again; the others notice nothing. **Export**
|
||||
leaves hidden writing out of the picture it makes, which is how to make a
|
||||
picture of the exercise with the corrections alone.
|
||||
|
||||
## Keeping and exporting
|
||||
|
||||
The page you are on is written continuously to `Current.svg` in the
|
||||
current notebook, and copied to a dated file whenever the session moves
|
||||
on. **Keep page** makes such a copy now, named for the day and a serial
|
||||
number, such as `2026_10_06_02.svg`; pressing it again before anything
|
||||
has changed makes no second copy. Use it to mark a state worth keeping
|
||||
on its own, or to add a finished page to a notebook of exercises.
|
||||
|
||||
**Export** saves a picture of the page as you see it, as a PNG file at
|
||||
200 dots per inch, in a directory and under a name you choose in
|
||||
Kokkoja's file chooser. Replacing an existing file takes a second press
|
||||
of the button, so a slip does not overwrite anything.
|
||||
|
||||
The notebooks are ordinary directories, and a page is an ordinary SVG
|
||||
file. Open one in a browser or in Inkscape, attach it to an email, keep
|
||||
it with your other documents. Only Kokkoja can load it as a live page
|
||||
again, with each person's writing still theirs.
|
||||
|
||||
## Ending a session
|
||||
|
||||
Close the browser tab, or stop the program with Ctrl-C in its terminal.
|
||||
There is nothing to save: the page is already in your notebook, and in
|
||||
each guest's. Guests who are still connected can go on writing on the
|
||||
page among themselves, but no new page can be started without you or a
|
||||
guest you have given the right, and a guest who arrives after everyone
|
||||
has left finds an empty session. The next lesson is a new session with a
|
||||
new invitation; load the pages you need from the student's notebook.
|
||||
|
||||
## When something goes wrong
|
||||
|
||||
* **The browser says the notebooks are locked.** Open Kokkoja from the
|
||||
full address the program printed when it started, the one with the
|
||||
token.
|
||||
* **"Disconnected" in the toolbar.** Kokkoja reconnects by itself as
|
||||
soon as the relay can be reached, and sends what was written meanwhile.
|
||||
If it does not come back, check your connection; the page is safe in
|
||||
your notebook either way.
|
||||
* **A guest sees "Waiting for the host's page".** You have not started
|
||||
a page yet, or your connection is down. The first page is started when
|
||||
you start the session, so this usually means the latter.
|
||||
* **Pressure does not work.** The last line of the panel reports what
|
||||
the browser receives from your pen. If it says *mouse*, the browser is
|
||||
not seeing the pen as a pen; try the other browser.
|
||||
@@ -0,0 +1,234 @@
|
||||
# How Kokkoja works
|
||||
|
||||
This part is for the curious, for anyone who wants to run a Kokkoja
|
||||
server, and for anyone who wants to read or change the code. It
|
||||
describes the parts, what travels between them, what is protected and
|
||||
what is not, and how pages are stored. The code is the final word; it
|
||||
is short and commented.
|
||||
|
||||
## The parts
|
||||
|
||||
Kokkoja is three programs and a few static files.
|
||||
|
||||
* **The client** (`client/`) runs in the web browser and does everything
|
||||
the user sees: the pen, the page, the text, the encryption, the
|
||||
saving. It is plain JavaScript modules, run by the browser as written,
|
||||
with no build step and no third-party packages. Its types are written
|
||||
in JSDoc comments and checked by the TypeScript compiler during
|
||||
development.
|
||||
* **The local program** (`kokkoja.py`) runs on the user's computer. It
|
||||
serves the client to the browser at `http://localhost:8760/`, keeps the
|
||||
user's identity, and reads and writes the notebook directories, which
|
||||
a web page cannot do on its own. It uses nothing but Python's standard
|
||||
library.
|
||||
* **The relay** (`relay/kokkoja_relay.py`) runs on a server that every
|
||||
participant can reach. It passes messages between the members of a
|
||||
session and nothing more: it cannot read them and it stores nothing.
|
||||
It is Python with one library, `websockets`.
|
||||
* **The web site** is the `client/` directory served over HTTPS, with a
|
||||
`config.json` beside it, so that a guest can join without the local
|
||||
program. `deploy/` has the server setup.
|
||||
|
||||
Every participant's computer holds the whole page. Only changes travel:
|
||||
a stroke as it is drawn, a text block as it is typed, an erasure, a new
|
||||
page, an image once. There is no central copy.
|
||||
|
||||
## Why the relay exists
|
||||
|
||||
Two computers behind home routers generally cannot connect to each other
|
||||
directly, so something in between has to pass the messages. The relay is
|
||||
the smallest such thing: a WebSocket server with *rooms*. A client
|
||||
connects to `wss://<server>/relay/room/<room id>`; the relay gives the
|
||||
connection a number, tells the room that a peer has arrived, and from
|
||||
then on sends every message it receives from one member to all the
|
||||
others, stamped with the sender's number. When the last member leaves,
|
||||
the room is gone. It holds at most 32 members per room and 500 rooms,
|
||||
and refuses messages over 1 MB; clients split anything larger.
|
||||
|
||||
## Sessions and encryption
|
||||
|
||||
The invitation is
|
||||
|
||||
<relay address>#<secret>.<host id>
|
||||
|
||||
The *secret* is 16 random bytes, made by the host's browser. From it
|
||||
every client derives two values with HKDF-SHA-256: the *room id*, which
|
||||
names the room at the relay, and the *message key*, a 256-bit AES-GCM
|
||||
key with which every message is encrypted before it is sent and
|
||||
decrypted on arrival. The relay sees the room id, the members' network
|
||||
addresses, and the size and timing of encrypted messages. It cannot
|
||||
compute the key from the room id, so it cannot read anything. Neither
|
||||
can anyone watching the network between the participants and the relay,
|
||||
which is TLS-protected in any case.
|
||||
|
||||
A link invitation wraps the same text:
|
||||
|
||||
https://<site>/#join=<invitation>
|
||||
|
||||
Browsers never send the part of an address after `#` to the site, so the
|
||||
secret stays between the host and the guests.
|
||||
|
||||
## Identity and layers
|
||||
|
||||
Each participant has a signing key pair (ECDSA on P-256), made in the
|
||||
browser the first time and then kept, by the local program in its
|
||||
configuration directory or by the browser in its own storage. The
|
||||
*participant id* is the first 16 bytes of the SHA-256 hash of the public
|
||||
key. When a client joins a room it announces itself with a signed
|
||||
`hello` carrying its name and public key; the signature covers the room
|
||||
id and the peer number the relay stamped on the message, so nobody can
|
||||
replay it elsewhere or claim another peer's connection. Until a peer has
|
||||
sent a valid `hello`, everything else from it is ignored.
|
||||
|
||||
A page is a stack of layers, one per participant, keyed by participant
|
||||
id. A client accepts a change to a layer only from the participant whose
|
||||
id it is, so the rule "nobody can change anyone else's writing" is not a
|
||||
permission that could be misconfigured but a consequence of how the data
|
||||
is organised. Because the id is derived from the key, and the key is
|
||||
kept, a person is the same participant in every session from the same
|
||||
computer, and their writing on a page loaded weeks later is still theirs.
|
||||
|
||||
The host is whoever holds the key whose id is in the invitation. Pages,
|
||||
images and the list of guests allowed to start pages are accepted only
|
||||
from the host (or from an allowed guest, for pages and images).
|
||||
|
||||
## Pages
|
||||
|
||||
Page coordinates are in units of 0.1 mm. A blank page is an A4 sheet,
|
||||
2100 by 2970 units. Writing within 8 mm of an edge grows the sheet by
|
||||
8 mm on that side. An image is placed at its natural size, taken from
|
||||
the resolution recorded in the file, or 0.1 mm per pixel when there is
|
||||
none, centred across the sheet, 8 mm from the top or from the image
|
||||
above, scaled down if wider than the sheet inside its margins (or, if the
|
||||
person adding it chooses, placed at natural size with the sheet widened
|
||||
to hold it). A page holds at most 12 images and 24 MB of image data.
|
||||
An image file over 2400 pixels on a side or 1.5 MB, or not PNG or JPEG,
|
||||
is redrawn at most 2400 pixels and stored as JPEG before it is shared;
|
||||
smaller PNG and JPEG files are shared byte for byte.
|
||||
|
||||
A stroke is a list of points with pressure, and the line width is a
|
||||
function of pressure: nominal at a pressure of 0.4, about 2.6 times that
|
||||
at 0.75. Points are sent while the stroke is drawn, so the others see it
|
||||
grow. A text block is sent whole at every keystroke, with its line
|
||||
breaks already laid out by the writer's browser, so that everyone shows
|
||||
the same lines.
|
||||
|
||||
Each page has a number, counted from 1 in the session, and a random id.
|
||||
A client that joins late asks the longest-connected member for the
|
||||
layers of absent participants, and receives each present participant's
|
||||
layer from that participant. A client that reconnects after a dropped
|
||||
connection sends its whole layer again, so that nothing drawn meanwhile
|
||||
is lost.
|
||||
|
||||
## The page file
|
||||
|
||||
A notebook is a directory. The page being written is `Current.svg` in
|
||||
it, rewritten a moment after every change (at most every 400 ms while
|
||||
writing continues, at least every 2 s). A *kept* page is a copy of
|
||||
`Current.svg` named `YYYY_MM_DD_NN.svg`, the day being when the page was
|
||||
last changed and NN a serial number within that day. A copy is made when
|
||||
the user asks (**Keep page**), and by the program whenever `Current.svg`
|
||||
is about to be given to another page: when the session moves to a new
|
||||
page, when the notebook is changed, and when a session begins and finds
|
||||
a page of another session in the notebook. Nothing is copied if the
|
||||
page is blank or an identical copy already exists.
|
||||
|
||||
The file is an SVG document that any viewer displays: the images as
|
||||
embedded `<image>` elements, each layer as a group with the participant's
|
||||
name and id, each stroke as a path and each text block as text.
|
||||
Beside the shapes, the same elements carry Kokkoja's own data in
|
||||
attributes, the exact points and pressures, the text and its breaks, so
|
||||
that Kokkoja reads the page back without loss, with each layer still
|
||||
belonging to its participant. The session id and host id are recorded
|
||||
too, which is how a reloaded tab knows that `Current.svg` is its own page
|
||||
to continue from.
|
||||
|
||||
With the local program, the notebooks are in `Kokkoja` in the Documents
|
||||
directory (`--notebooks` chooses another place), the identity and the
|
||||
browser's token in the configuration directory (`~/.config/kokkoja` on
|
||||
Linux, `~/Library/Application Support/kokkoja` on macOS). In the
|
||||
browser, the notebooks are in the site's IndexedDB under the same names
|
||||
and rules, and the identity in its localStorage; a kept page and an
|
||||
exported picture are offered as downloads, since that is the only way a
|
||||
web page can hand a file to its user.
|
||||
|
||||
## The local program and its token
|
||||
|
||||
The local program listens on 127.0.0.1 only. Any web page open in the
|
||||
browser could nevertheless send requests to `localhost:8760`, so every
|
||||
request that touches files must carry a secret token, which the program
|
||||
makes once, keeps in its configuration directory, and passes to the
|
||||
browser in the address it opens (`#token=...`, which the browser keeps
|
||||
in its local storage and never sends to any server). Requests without
|
||||
the token are refused. The program also checks that requests come from
|
||||
its own origin. Outside the notebooks it reads only image files and
|
||||
writes only PNG files, each where the file chooser was pointed.
|
||||
|
||||
## The web site, and whom you trust
|
||||
|
||||
A client served by a web site is a web application like any other: the
|
||||
site operator's code runs in your browser. The end-to-end encryption
|
||||
protects the session from the relay and the network, but a dishonest
|
||||
site could serve a client that gives the key away. Participants who run
|
||||
the local program load the client from their own disk and are not
|
||||
exposed to that; they trust the code they downloaded once. The site
|
||||
sends a Content Security Policy that lets the client load nothing from
|
||||
anywhere else, which protects against the ordinary web attacks but not
|
||||
against the operator. For a lesson this is the same trust one places in
|
||||
any video call service, and considerably less than in one that sees the
|
||||
content; it is stated here so that nobody is surprised.
|
||||
|
||||
## Running a server
|
||||
|
||||
A server is a machine with a public name, TLS and nginx (or any reverse
|
||||
proxy that passes WebSockets). `deploy/README.md` describes the one
|
||||
Kokkoja comes with: the relay as a systemd service bound to localhost,
|
||||
nginx terminating TLS, serving the client's files at `https://<name>/`
|
||||
and forwarding `wss://<name>/relay/` to the relay, and a `config.json`
|
||||
telling the client which relay to use and that notebooks are kept in the
|
||||
browser. A host uses the server with
|
||||
|
||||
python3 kokkoja.py --relay wss://<name>/relay
|
||||
|
||||
and invitations become links to `https://<name>/`. One server serves
|
||||
any number of hosts and sessions at once; the relay's work per session
|
||||
is passing a few kilobytes per second. Since the relay cannot read what
|
||||
it passes, a server run by someone else is a matter of availability, not
|
||||
of privacy; the web site is the part that requires trust, as described
|
||||
above. A teacher who wants no dependence on anyone's server needs a
|
||||
machine of their own that is reachable from the Internet, which a
|
||||
computer behind a home router is not without arrangement.
|
||||
|
||||
## What is not protected yet
|
||||
|
||||
* Only `hello` is signed. A participant working together with a
|
||||
dishonest relay could forge another participant's strokes. Signing
|
||||
every message, or page descriptors with the host's key, would close
|
||||
this.
|
||||
* A participant can announce a page with a higher number than the
|
||||
host's, and the others move to it.
|
||||
* The relay sees who talks to whom, when, and how much.
|
||||
|
||||
## Pen input
|
||||
|
||||
The client uses the browser's pointer events, which deliver every sample
|
||||
a pen produces, with pressure, in Firefox and in Chromium-based browsers
|
||||
on Linux and macOS. The panel's last line shows what arrives. When a
|
||||
pen has been used, touches are ignored, so a hand resting on a touch
|
||||
screen does not draw; this rule has not yet been tested with a device
|
||||
that has both a pen and a touch sensor. Kokkoja opens none of the
|
||||
browser's system dialogs (the colour palette and, with the local
|
||||
program, the file chooser are its own), because one such dialog crashed
|
||||
Firefox on Linux.
|
||||
|
||||
## Development
|
||||
|
||||
The code is Python and JavaScript only. The tests run with
|
||||
|
||||
.venv/bin/python -m unittest discover tests -v
|
||||
|
||||
where `tests/test_end_to_end.py` drives a headless Firefox through
|
||||
geckodriver with several participants, with and without the local
|
||||
program, and `npm run check` type-checks the client (the TypeScript
|
||||
compiler is the only development dependency). The relay's `--host` and
|
||||
`--port` and the local program's options are listed by `--help`.
|
||||
@@ -0,0 +1,106 @@
|
||||
# Kokkoja: an introduction
|
||||
|
||||
Kokkoja lets people at different computers write together on a shared
|
||||
*page*. Everyone in a session sees the same page. Anyone can write on it
|
||||
by hand or type on it, and what one person writes appears on the others'
|
||||
screens as it is written. A page can begin blank, or it can begin with an
|
||||
image, such as a scanned sheet or a photograph, which everyone then writes
|
||||
on. Every page is saved automatically to a *notebook* on each person's
|
||||
own computer, so that after the session each participant keeps a complete
|
||||
copy of everything that was written.
|
||||
|
||||
Kokkoja runs in a web browser, beside whatever video call you are already
|
||||
using. It replaces the screen sharing and annotation of a video call
|
||||
application with something made for writing together, and it leaves the
|
||||
voice and video to the call.
|
||||
|
||||
## What Kokkoja does
|
||||
|
||||
**A shared page, not a shared screen.** In a video call one person's
|
||||
screen is transmitted as a picture, and annotations float on top of that
|
||||
picture. In Kokkoja every participant's computer holds the page itself:
|
||||
the image, if there is one, and every mark made on it. Only the changes
|
||||
travel over the network. The marks belong to the page, are saved with it,
|
||||
and can be shown or hidden later.
|
||||
|
||||
**Handwriting and typing.** Kokkoja is made for a pen on a drawing
|
||||
tablet, with pressure giving the line its thickness, but a mouse or a
|
||||
finger will do. Typed text is placed anywhere on the page and wraps at a
|
||||
width you choose. Both come in any colour.
|
||||
|
||||
**Everyone writes on their own layer.** Each participant's writing is a
|
||||
layer of its own. You can erase your own writing and nothing else; nobody
|
||||
else can erase yours. Any layer can be hidden on your own screen without
|
||||
affecting anyone else's view.
|
||||
|
||||
**A page that grows.** A page is an A4 sheet. When writing approaches an
|
||||
edge, the sheet grows in that direction, so you are never short of room.
|
||||
An image is placed at its natural size; one wider than the sheet is
|
||||
scaled down to fit, and the sheet grows downward to hold further images.
|
||||
|
||||
**Saved without a save command.** The page you are on is written to your
|
||||
notebook continuously, as it changes. Nothing is lost if a connection
|
||||
drops or a computer crashes. When the session moves to another page, the
|
||||
page being left is kept as a dated file. You can also keep a dated copy
|
||||
of a page at any moment, and export a picture of it as a PNG file.
|
||||
|
||||
**Notebooks are yours.** A notebook is a directory on your own computer
|
||||
and a page is a single file in it, an ordinary SVG file that any
|
||||
image viewer or vector drawing program can open. Nothing is stored on any
|
||||
server, and there are no accounts: you choose the name the others will
|
||||
see, and that is all.
|
||||
|
||||
**Private.** Everything that travels between participants is encrypted
|
||||
on their own computers. The relay server that passes the messages cannot
|
||||
read them. The invitation to a session carries the key, so only those
|
||||
who were invited can read the page.
|
||||
|
||||
**Nothing to install for guests.** A guest joins by opening a link in a
|
||||
web browser. Someone who wants a notebook of real files on their own
|
||||
computer installs the small Kokkoja program instead; that is also how a
|
||||
host runs Kokkoja.
|
||||
|
||||
**Free software.** Kokkoja is free and open-source. Anyone can use it
|
||||
without paying, and nobody depends on a company's service. It runs in
|
||||
Firefox and in browsers built on the Chromium engine, such as Brave and
|
||||
Chrome, on Linux and macOS.
|
||||
|
||||
## Who uses it
|
||||
|
||||
Kokkoja is a general tool for working on a page together at a distance:
|
||||
going through a document with a colleague, sketching a diagram while
|
||||
talking it over, correcting a draft, teaching anything that is done on
|
||||
paper. Wherever two people would otherwise lean over one sheet, Kokkoja
|
||||
puts the sheet between their computers.
|
||||
|
||||
Its ideal use, and the one it was made for first, is teaching a foreign
|
||||
language over a video call. The teacher puts a scanned exercise on the
|
||||
page. The student fills it in by hand or by typing, during the lesson,
|
||||
while the teacher watches, corrects mistakes as they are made, and adds
|
||||
notes in the margin. Each correction is in the teacher's own hand and
|
||||
colour, on the teacher's own layer, where the student cannot change it.
|
||||
When the lesson is over, the student's notebook holds every exercise of
|
||||
the lesson, with the student's answers and the teacher's corrections
|
||||
together on one page, and the teacher's notebook holds the same pages as
|
||||
a record of what the student was given. The teacher's notebooks of
|
||||
exercises become a library: loading a page from one of them puts a fresh
|
||||
copy in front of a new student, and the original is never changed.
|
||||
|
||||
## Hosts and guests
|
||||
|
||||
A session has one *host*, the person who starts it, and any number of
|
||||
*guests*. In a lesson the teacher is the host and the students are the
|
||||
guests. The host sends the guests an invitation, by email, in a chat
|
||||
message, or by any other means. The host *drives*: only the host puts
|
||||
images on the page, starts new pages or loads pages from a notebook, and
|
||||
everyone follows. The host can give any guest the same abilities, for a
|
||||
discussion among colleagues.
|
||||
|
||||
The rest of this documentation describes Kokkoja from the guest's point of
|
||||
view first, since the host sees the same screen and has a few more
|
||||
buttons, then from the host's, and finally explains how it works
|
||||
underneath, for those who want to run their own server or read the code.
|
||||
|
||||
* [A guest's Kokkoja](guest.md)
|
||||
* [A host's Kokkoja](host.md)
|
||||
* [How Kokkoja works](implementation.md)
|
||||
Reference in new issue
Block a user