Skip to content

Commit 01d54a6

Browse files
authored
Merge pull request #109 from jagerman/docs-day-2022-04
API docs generation using docsify
2 parents d21fde4 + 6d5abf7 commit 01d54a6

File tree

12 files changed

+283
-79
lines changed

12 files changed

+283
-79
lines changed

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

Lines changed: 96 additions & 12 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
@@ -95,6 +110,8 @@ def endpoint_sort_key(rule):
95110

96111
for rule in sorted(app.url_map.iter_rules(), key=endpoint_sort_key):
97112
ep = rule.endpoint
113+
if ep == 'static':
114+
continue
98115
methods = [m for m in rule.methods if m not in ('OPTIONS', 'HEAD')]
99116
if not methods:
100117
app.logger.warning(f"Endpoint {ep} has no useful method, skipping!")
@@ -109,6 +126,11 @@ def endpoint_sort_key(rule):
109126
handler = app.view_functions[ep]
110127

111128
doc = handler.__doc__
129+
if ep.startswith('legacy'):
130+
# We deliberately omit legacy endpoint documentation
131+
if doc is not None:
132+
app.logger.warning(f"Legacy endpoint {ep} has docstring but it will be omitted")
133+
continue
112134
if doc is None:
113135
app.logger.warning(f"Endpoint {ep} has no docstring!")
114136
doc = '*(undocumented)*'
@@ -174,7 +196,9 @@ def endpoint_sort_key(rule):
174196
argdoc = argdoc.replace('\n', '\n ')
175197

176198
if ':`' in argdoc:
177-
app.logger.warning(f"{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+
)
178202

179203
s.append(f" — {argdoc}\n\n")
180204
else:
@@ -189,30 +213,90 @@ def endpoint_sort_key(rule):
189213

190214
more = read_snippet(f'{ep}.md', depth=3)
191215
if more:
216+
s.append("\n\n")
192217
s.append(more)
193218

194219
s.append("\n\n\n")
195220

196221

197-
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
198223

199224
if section_list[-1]:
200225
# We have some uncategorized entries, so load the .md for it
201226
other = read_snippet('uncategorized.md')
202-
if other:
203-
section_list[-1].insert(0, other)
204-
else:
227+
if not other:
205228
app.logger.warning(
206229
"Found uncategorized sections, but uncategorized.md not found; inserting stub"
207230
)
208-
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+
)
209294

210-
for s in section_list:
211-
for x in s:
295+
for x in section_list[i]:
212296
print(x, end='', file=out)
213297
print("\n\n", file=out)
214298

215-
if out_file:
299+
if out is not None and out != sys.stdout:
216300
out.close()
217301

218302
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)

0 commit comments

Comments
 (0)