Публікація

Дизайн API для частково статичного клону Reddit

Докладний дизайн бази даних, структури статичних файлів в R2 та API для частково статичного клону Reddit

Дизайн API для частково статичного клону Reddit

Як виглядає API для частково статичного клону Reddit, якщо АПІ писати самому. Я вже визначився з тим, що основний контент буде в JSON Feed і буде лежати в статиці в Cloudflare R2, проте я схиляюся до думки, що підписки, коментарі, реакції доцільніше тримати в динаміці. В попередньому пості я писав про те що таку динаміку добре класти в Supabase, проте подумав що дешевше буде мати власноруч написані скріпти десь на хостінгу. Таким чином, якщо хостінг буде падати або працювати нестабільно користувачі все одно зможуть читати фіди, просто соціальний функціонал буде недоступний. А ми виграємо в тому що у нас розвʼязані руки в тому де хостити соціальний функціонал і кости на утримання сервера будуть значно нижчими.

Тож як повинен виглядати АПІ який буде розполагатися на власному хостінгу?

Хеші

1
2
USER_HASH = SHA1(email + SALT)
POST_HASH = SHA1(POST_ID + SALT)

Структура БД

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
updates
-------
record_type enum(POST|REPOST|COMMENT|REACT|SUBSCRIBE) INDEX
record_id INDEX
created_at INDEX

subscriptions
-------------
subscription_id INDEX
user_id USER_HASH INDEX
feed_url INDEX

posts
-----
post_id INDEX
user_id USER_HASH INDEX
feed_url
title
body
media_url

reposts
-------
repost_id INDEX
user_id USER_HASH INDEX
post_id INDEX
feed_url

comments
--------
comment_id INDEX
user_id USER_HASH INDEX
record_type enum(POST|COMMENT) INDEX
record_id INDEX
body
media_url

reactions
---------
reaction_id INDEX
user_id USER_HASH INDEX
type REACTION_TYPE
value INTEGER
record_type enum(POST|COMMENT) INDEX
record_id INDEX

counts
------
record_type enum(POST|COMMENT) INDEX
record_id INDEX
type REACTION_TYPE
value INTEGER
  • REACTION_TYPE = THUMBS_UP|THUMBS_DOWN|LAUGH|HOORAY|CONFUSED|HEART|ROCKET|EYES|VIEW|OPENING|COMMENT|REPOST

Статика в R2:

Нехай в статиці буде мінімальний набір даних для функціонування сайту на читання. Все інше буде лежати в динаміці.

1
2
3
4
/u/{USER_HASH}/updates.json
/u/{USER_HASH}/{YEAR}/{MONTH}/{DAY}/updates.json
/u/{USER_HASH}/subscriptions.json
/p/{POST_HASH}/content.json

Авторизація

Авторизацію доцільно тримати в Cloudflare функції, щоб користувач міг увійти, щоб отримати :userhash та власний список підписок. Це мінімум щоб почати читати сайт. Якщо користувач не залогінен – використовувати список підписок за замовчуванням – це список “спільні стрічки”.

Редірект на сторінку авторизації Google:

1
2
3
> POST /login/google
< Status: 301
< Location: {URL_TO_OAUTH_PAGE}

JWT-токен авторизованного користувача:

1
2
> GET /login/google?code={OAUTH_CODE}
< Body: {"token": "{AUTH_JWT_TOKEN}"}

Підписки

Створити одну або більше підписок:

1
2
3
4
> POST /subscriptions
> Authorization: Bearer {AUTH_JWT_TOKEN}
> Body: ["{FEED_URL}", "{FEED_URL}"]
< Status: 200

Видалити підписку:

1
2
3
> DELETE /subscriptions?feed_url={FEED_URL}
> Authorization: Bearer {AUTH_JWT_TOKEN}
< Status: 200
  • {FEED_URL} – посилання на підписку /u/{USER_HASH}, /t/{TOPIC}, /m/{MIRROR}

Пости

Створити пост:

1
2
3
4
5
> POST /posts
> Authorization: Bearer {AUTH_JWT_TOKEN}
> Body: {"title: "{TITLE}", "body": "{BODY}"}
< Status: 200
< Body: {"post_id": "{POST_ID}"}

Редагувати пост:

1
2
3
4
5
> PATCH /posts?post_id={POST_ID}
> Authorization: Bearer {AUTH_JWT_TOKEN}
> Body: {"title: "{TITLE}", "body": "{BODY}"}
< Status: 200
< Body: {"post_id": "{POST_ID}"}

Видалити пост:

1
2
3
> DELETE /posts?post_id={POST_ID}
> Authorization: Bearer {AUTH_JWT_TOKEN}
< Status: 200

Коментарі

Створити коментар:

1
2
3
4
5
> POST /comments
> Authorization: Bearer {AUTH_JWT_TOKEN}
> Body: {"post_id": "{POST_ID}", "record_id": "{RECORD_ID}", "BODY": "{BODY}"}
< Status: 200
< Body: {"comment_id": "{COMMENT_ID}"}

Редагувати коментар:

1
2
3
4
5
> PATCH /comments?comment_id={COMMENT_ID}
> Authorization: Bearer {AUTH_JWT_TOKEN}
> Body: {"post_id": "{POST_ID}", "record_id": "{RECORD_ID}", "BODY": "{BODY}"}
< Status: 200
< Body: {"comment_id": "{COMMENT_ID}"}

Видалити коментар:

1
2
3
> DELETE /comments?comment_id={COMMENT_ID}
> Authorization: Bearer {AUTH_JWT_TOKEN}
< Status: 200

Список коментарів:

1
2
3
4
> GET /comments?record_type={RECORD_TYPE}&record_id[]={RECORD_ID}&record_id[]={RECORD_ID}
> Authorization: Bearer {AUTH_JWT_TOKEN}
< Status: 200
< Body: [{"record_type": "{RECORD_TYPE}", "record_id": "{RECORD_ID}", "comment_id": "{COMMENT_ID}", "body": "{BODY}"}]
  • {RECORD_TYPE}post, comment
  • {RECORD_ID} – ID поста або коментаря

Реакції

Створити реакцію:

1
2
3
4
5
> POST /reactions
> Authorization: Bearer {AUTH_JWT_TOKEN}
> Body: {"record_id": "{RECORD_ID}", "record_type": "{RECORD_TYPE}", "type": "{REACTION_TYPE}"}
< Status: 200
< Body: {"reaction_id": "{REACTION_ID}"}

Змінити реакцію:

1
2
3
4
> PATCH /reactions
> Body: {"record_id": "{RECORD_ID}", "record_type": "{RECORD_TYPE}", "type": "{REACTION_TYPE}"}
< Status: 200
< Body: [{"record_type": "{RECORD_TYPE}", "record_id": "{RECORD_ID}", "type": "{REACTION_TYPE}"}]

Видалити реакцію:

1
2
3
> DELETE /reactions?record_id={RECORD_ID}&record_type={RECORD_TYPE}
< Status: 200
< Body: [{"record_type": "{RECORD_TYPE}", "record_id": "{RECORD_ID}", "type": "{REACTION_TYPE}"}]

Список реакцій:

1
2
3
> GET /reactions?record_type={RECORD_TYPE}&record_id[]={RECORD_ID}&record_id[]={RECORD_ID}
< Status: 200
< Body: [{"record_type": "{RECORD_TYPE}", "record_id": "{RECORD_ID}", "user_id": "{USER_ID}", "type": "{REACTION_TYPE}", "value": 0}]
  • {RECORD_TYPE}post, comment
  • {RECORD_ID} – ID поста або коментаря
  • REACTION_TYPE = THUMBS_UP|THUMBS_DOWN|LAUGH|HOORAY|CONFUSED|HEART|ROCKET|EYES|VIEW|OPENING|COMMENT|REPOST

Підрахунки

Список підрахунків:

1
2
3
> GET /counts?record_type={RECORD_TYPE}&record_id[]={RECORD_ID}&record_id[]={RECORD_ID}
< Status: 200
< Body: [{"record_type": "{RECORD_TYPE}", "record_id": "{RECORD_ID}", "user_id": "{USER_ID}", "type": "{REACTION_TYPE}", "value": 0}]
  • {RECORD_TYPE}post, comment
  • {RECORD_ID} – ID поста або коментаря

Ще в контексті підрахунків буде такий параметр посту як engagement = SUM(reactions)

Публікація захищена ліцензією CC BY 4.0 .