Skip to content

Commit 6d5abf7

Browse files
committed
Convert generation to use docsify
1 parent b81b841 commit 6d5abf7

File tree

9 files changed

+237
-58
lines changed

9 files changed

+237
-58
lines changed

docs/make-api-docs.py renamed to docs/generate-api-docs.py

Lines changed: 90 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import os
77
import argparse
88
import sys
9+
import shutil
910

1011
parser = argparse.ArgumentParser()
1112
parser.add_argument(
@@ -18,11 +19,18 @@
1819
"top-level headings start with a single `#`, sub-headings start with `##`, etc. For example, "
1920
"3 would start with `###`, sub-headings would be `####`, etc.",
2021
)
22+
parser.add_argument(
23+
"-m",
24+
"--multi-file",
25+
action="store_true",
26+
help="Write to one markdown file per section, rather than one single large file",
27+
)
2128
parser.add_argument(
2229
"-o",
2330
"--output",
2431
type=str,
25-
help="Specify output file; specifing a - or omitting outputs to stdout",
32+
help="Specify output file (or directory, when using -m); specifing a - or omitting outputs to "
33+
"stdout, but is not accepted in multi-file (-m) mode.",
2634
)
2735
parser.add_argument(
2836
"-W",
@@ -34,7 +42,11 @@
3442
args = parser.parse_args()
3543

3644
out_file = False
37-
if args.output and args.output != '-':
45+
if args.multi_file and (not args.output or args.output == '-'):
46+
print("-o must specify a directory when using -m")
47+
sys.exit(1)
48+
49+
if not args.multi_file and args.output and args.output != '-':
3850
if not args.overwrite and os.path.exists(args.output):
3951
print(f"{args.output} already exists; remove it first or use --overwrite/-W to overwrite")
4052
sys.exit(1)
@@ -71,20 +83,23 @@ def read_snippet(markdown, depth=args.markdown_level):
7183
return None
7284

7385

74-
section_list, sections = [], {}
86+
section_list, section_names, section_snips, sections = [], [], [], {}
7587
for name, bp in app.blueprints.items():
7688
s = []
7789
section_list.append(s)
90+
section_names.append(name)
7891
sections[name] = s
7992
snip = read_snippet(f'{name}.md')
8093
if snip:
8194
s.append(snip)
8295
else:
8396
s.append(f"{h1} {name.title()}\n\n")
8497
app.logger.warning(f"{name}.md not found: adding stub '{name.title()}' section")
98+
section_snips.append(snip)
8599

86100
# Last section is for anything with a not found category:
87101
section_list.append([])
102+
section_names.append('uncategorized')
88103

89104

90105
# Sort endpoints within a section by the number of URL parts, first, then alphabetically because we
@@ -114,7 +129,7 @@ def endpoint_sort_key(rule):
114129
if ep.startswith('legacy'):
115130
# We deliberately omit legacy endpoint documentation
116131
if doc is not None:
117-
app.logger.warning(f"Legacy endpoint {ep} has docstring but it will be omitted");
132+
app.logger.warning(f"Legacy endpoint {ep} has docstring but it will be omitted")
118133
continue
119134
if doc is None:
120135
app.logger.warning(f"Endpoint {ep} has no docstring!")
@@ -181,7 +196,9 @@ def endpoint_sort_key(rule):
181196
argdoc = argdoc.replace('\n', '\n ')
182197

183198
if ':`' in argdoc:
184-
app.logger.warning(f"{method} {url} ({arg}) still contains some rst crap we need to handle")
199+
app.logger.warning(
200+
f"{method} {url} ({arg}) still contains some rst crap we need to handle"
201+
)
185202

186203
s.append(f" — {argdoc}\n\n")
187204
else:
@@ -196,30 +213,90 @@ def endpoint_sort_key(rule):
196213

197214
more = read_snippet(f'{ep}.md', depth=3)
198215
if more:
216+
s.append("\n\n")
199217
s.append(more)
200218

201219
s.append("\n\n\n")
202220

203221

204-
out = open(args.output, 'w') if out_file else sys.stdout
222+
out = open(args.output, 'w') if out_file else sys.stdout if not args.multi_file else None
205223

206224
if section_list[-1]:
207225
# We have some uncategorized entries, so load the .md for it
208226
other = read_snippet('uncategorized.md')
209-
if other:
210-
section_list[-1].insert(0, other)
211-
else:
227+
if not other:
212228
app.logger.warning(
213229
"Found uncategorized sections, but uncategorized.md not found; inserting stub"
214230
)
215-
section_list[-1].insert(0, "# Uncategorized Endpoints\n\n")
231+
other = "# Uncategorized Endpoints\n\n"
232+
233+
section_list[-1].insert(0, other)
234+
section_snips.append(other)
235+
236+
else:
237+
section_list.pop()
238+
section_names.pop()
239+
240+
if args.multi_file:
241+
if not os.path.exists(args.output):
242+
os.makedirs(args.output)
243+
if not args.overwrite and os.path.exists(args.output + '/index.md'):
244+
print(f"{args.output}/index.md already exists; remove it first or use --overwrite/-W")
245+
sys.exit(1)
246+
247+
api_readme_f = open(args.output + '/index.md', 'w')
248+
249+
section_order = range(0, len(section_list))
250+
if args.multi_file:
251+
# In multi-file mode we take the order for the index file section from the order it is listed in
252+
# sidebar.md:
253+
sidebar = read_snippet('sidebar.md')
254+
shutil.copy(snippets + '/sidebar.md', args.output + '/sidebar.md')
255+
256+
def pos(i):
257+
x = sidebar.find(section_names[i])
258+
return x if x >= 0 else len(sidebar)
259+
260+
section_order = sorted(section_order, key=pos)
261+
262+
for i in section_order:
263+
if args.multi_file:
264+
filename = args.output + '/' + section_names[i] + '.md'
265+
if not args.overwrite and os.path.exists(filename):
266+
print(f"{filename} already exists; remove it first or use --overwrite/-W to overwrite")
267+
sys.exit(1)
268+
if out is not None:
269+
out.close()
270+
out = open(filename, 'w')
271+
272+
if section_names[i] + '.md' not in sidebar:
273+
app.logger.warning(
274+
f"{section_names[i]}.md not found in snippets/sidebar.md: "
275+
"section will be missing from the sidebar!"
276+
)
277+
278+
snip = section_snips[i]
279+
if snip.startswith(f'{h1} '):
280+
preamble = snip.find(f'{h2}')
281+
if preamble == -1:
282+
preamble = snip
283+
else:
284+
preamble = snip[:preamble]
285+
print(
286+
re.sub(fr'^{h1} (.*)', fr'{h1} [\1]({section_names[i]}.md)', preamble),
287+
file=api_readme_f,
288+
)
289+
else:
290+
app.logger.warning(
291+
f"{section_names[i]} section didn't start with expected '# Title', "
292+
f"cannot embed section link in {args.output}/index.md"
293+
)
216294

217-
for s in section_list:
218-
for x in s:
295+
for x in section_list[i]:
219296
print(x, end='', file=out)
220297
print("\n\n", file=out)
221298

222-
if out_file:
299+
if out is not None and out != sys.stdout:
223300
out.close()
224301

225302
app.logger.info("API doc created successfully!")

docs/make-docs.sh

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
if [ "$(basename $(pwd))" != "docs" ] || ! [ -e "make-docs.sh" ]; then
6+
echo "Error: you must run this from the docs directory" >&2
7+
exit 1
8+
fi
9+
10+
rm -rf api
11+
12+
docsify init --local api
13+
14+
rm -f api/README.md
15+
16+
if [ -n "$NPM_PACKAGES" ]; then
17+
npm_dir="$NPM_PACKAGES/lib/node_modules"
18+
elif [ -n "$NODE_PATH" ]; then
19+
npm_dir="$NODE_PATH"
20+
elif [ -d "$HOME/node_modules" ]; then
21+
npm_dir="$HOME/node_modules"
22+
elif [ -d "/usr/local/lib/node_modules" ]; then
23+
npm_dir="/usr/local/lib/node_modules"
24+
else
25+
echo "Can't determine your node_modules path; set NPM_PACKAGES or NODE_PATH appropriately" >&2
26+
exit 1
27+
fi
28+
29+
cp $npm_dir/docsify/node_modules/prismjs/components/prism-{json,python,http}.min.js api/vendor
30+
cp $npm_dir/docsify-katex/dist/docsify-katex.js api/vendor
31+
cp $npm_dir/docsify-katex/node_modules/katex/dist/katex.min.css api/vendor
32+
33+
PYTHONPATH=.. ./generate-api-docs.py -m -o api
34+
35+
perl -ni.bak -e '
36+
BEGIN { $first = 0; }
37+
if (m{^\s*<script>\s*$} .. m{^\s*</script>\s*$}) {
38+
if (not $first) {
39+
$first = false;
40+
print qq{
41+
<script>
42+
window.\$docsify = {
43+
name: "Session PySOGS API",
44+
repo: "https://github.com/oxen-io/session-pysogs",
45+
loadSidebar: "sidebar.md",
46+
subMaxLevel: 2,
47+
homepage: "index.md",
48+
}
49+
</script>\n};
50+
}
51+
} else {
52+
s{<title>.*</title>}{<title>Session PySOGS API</title>};
53+
s{(name="description" content=)"[^"]*"}{$1"Session PySOGS API documentation"};
54+
if (m{^\s*</body>}) {
55+
print qq{
56+
<script src="vendor/prism-json.min.js"></script>
57+
<script src="vendor/prism-python.min.js"></script>
58+
<script src="vendor/prism-http.min.js"></script>
59+
<script src="vendor/docsify-katex.js"></script>
60+
<link rel="stylesheet" href="vendor/katex.min.css" />
61+
};
62+
}
63+
print;
64+
}' api/index.html
65+

docs/mkdocs.yml

Lines changed: 0 additions & 3 deletions
This file was deleted.

docs/snippets/dm.md

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,49 @@
1-
# DMs
1+
# Direct Messages
22

3-
Direct messages between blinded users
3+
These endpoints are used for sending and retrieving direct messages between blinded users. Such
4+
messages are only used for introductions; once both parties know each other's Session ID messages
5+
should be done via regular Session one-to-one messaging.
46

57
## Encryption details
68

79
SOGS itself does not have the ability to decrypt the message contents and thus cannot enforce
810
any particular content; the following, however, is strongly recommended for Session client
911
interoperability:
1012

11-
Alice has master Session Ed25519 keypair `a`, `A` and blinded keypair `ka`, `kA`.
13+
Alice has master Session Ed25519 keypair $a$, $A$ and blinded keypair $ka$, $kA$.
1214

13-
Bob has master Session Ed25519 keypair `b`, `B` and blinded keypair `kb`, `kB`.
15+
Bob has master Session Ed25519 keypair $b$, $B$ and blinded keypair $kb$, $kB$.
1416

15-
Alice wants to send a message to Bob, knowing only `kB` (i.e. the blinded Session ID after
17+
Alice wants to send a message $M$ to Bob, knowing only $kB$ (i.e. the blinded Session ID after
1618
stripping the `0x15` prefix).
1719

1820
Alice constructs a message using Session protobuf encoding, then concatenates her *unblinded*
19-
pubkey, `A`, to this message:
21+
pubkey, $A$, to this message:
2022

21-
MSG || A
23+
$$
24+
M \parallel A
25+
$$
2226

2327
Alice then constructs an encryption key:
2428

25-
E = H(a * kB || kA || kB)
29+
$$
30+
E = H(a * kB \parallel kA \parallel kB)
31+
$$
2632

27-
where `H(.)` is 32-byte BLAKE2b, and the `*` denotes unclamped Ed25519 scalar*point multiplication
33+
where $H(\cdot)$ is 32-byte BLAKE2b, and the $*$ denotes unclamped Ed25519 scalar*point multiplication
2834
(e.g. libsodium's `crypto_scalarmult_ed25519_noclamp`).
2935

30-
The `MSG || A` plaintext value is then encrypted using XChaCha20-Poly1305 (e.g. using
36+
The $M \parallel A$ plaintext value is then encrypted using XChaCha20-Poly1305 (e.g. using
3137
libsodium's `crypto_aead_xchacha20poly1305_ietf_encrypt` function), using a secure-random
32-
24-byte nonce, no additional data, and encryption key `E`.
38+
24-byte nonce, no additional data, and encryption key $E$.
3339

3440
The final data message is then constructed as:
3541

36-
0x00 || CIPHERTEXT || NONCE
42+
$$
43+
\texttt{0x00} \parallel \textit{Ciphertext} \parallel \textit{Nonce}
44+
$$
3745

38-
where 0x00 is a version byte (allowing for future alternative encryption formats) and the rest
46+
where $0x00$ is a version byte (allowing for future alternative encryption formats) and the rest
3947
are bytes.
4048

4149
Finally this is base64-encoded when send to/retrieved from the SOGS.
@@ -48,23 +56,25 @@ Decryption proceeds by reversing the steps above:
4856

4957
2. Grab the version byte from the front and the 24-byte nonce from the back of the value.
5058

51-
a) if the version byte is not `0x00` abort because this message is from someone using a
59+
a) if the version byte is not $0x00$ abort because this message is from someone using a
5260
different encryption protocol.
5361

5462
3. Construct the encryption key by calculating:
5563

64+
$$
5665
E = H(b * kA || kA || kB)
66+
$$
5767

58-
where `kA` is the sender's de-prefixed blinded Session ID, `b` is the user's master Ed25519 key,
59-
and `kB` is the user's blinded Ed25519 for this SOGS server.
68+
where $kA$ is the sender's de-prefixed blinded Session ID, $b$ is the user's master Ed25519 key,
69+
and $kB$ is the user's blinded Ed25519 for this SOGS server.
6070

6171
4. Decrypt the remaining ciphertext using the nonce.
6272

63-
5. Unpack the plaintext value into MSG and the sender's `A` value (i.e. the last 32 bytes).
73+
5. Unpack the plaintext value into MSG and the sender's $A$ value (i.e. the last 32 bytes).
6474

65-
6. Derive the sender's actual Session ID by converting `A` from an ed25519 pubkey to an
75+
6. Derive the sender's actual Session ID by converting $A$ from an ed25519 pubkey to an
6676
curve25519 pubkey and prepending 0x05. (E.g. using libsodium's
67-
`crypto_sign_ed25519_pk_to_curve25519` on `A`, then adding the `05` prefix to the front).
77+
`crypto_sign_ed25519_pk_to_curve25519` on $A$, then adding the `05` prefix to the front).
6878

6979
This then leaves the receiving client with the true Session ID of the sender, and the message
7080
body (encoded according to typical Session message protobuf encoding).

docs/snippets/general.get_caps.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Example retrieving capabilities:
2+
3+
```http
4+
GET /capabilities HTTP/1.1
5+
Host: example.com
6+
```
7+
8+
```json
9+
{
10+
"capabilities": ["sogs", "batch"]
11+
}
12+
```
13+
14+
# Example with capability check
15+
16+
```http
17+
GET /capabilities?required=magic,batch HTTP/1.1
18+
```
19+
20+
```json
21+
{
22+
"capabilities": ["sogs", "batch"],
23+
"missing": ["magic"]
24+
}
25+
```

docs/snippets/sidebar.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
- [General](general.md)
2+
- [Rooms](rooms.md)
3+
- [Messages](messages.md)
4+
- [User management](users.md)
5+
- [Direct messages](dm.md)
6+
- [Onion requests](onion_request.md)
7+
- [Web viewer](views.md)
8+
- [Deprecated legacy SOGS](legacy.md)

docs/snippets/users.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# Users
1+
# User Management
22

3-
These endpoint relate to user-specific actions such as posting/retrieving private messages,
4-
controlling global moderators/admins, and global server bans.
3+
These endpoint relate to user-specific actions such as server bans and moderator permissions.

docs/snippets/views.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Publicly viewable URLs
2+
3+
These endpoint are not meant for Session client but rather for web browsers when attempting to open
4+
a SOGS URL.

0 commit comments

Comments
 (0)