geochan/src/babillard.ml

414 lines
13 KiB
OCaml
Raw Normal View History

2021-12-29 21:07:17 +01:00
open Db
2022-02-02 19:16:53 +01:00
include Bindings
2021-12-29 21:07:17 +01:00
2022-01-29 09:23:57 +01:00
type thread_data =
{ subject : string
2022-01-29 09:23:57 +01:00
; lng : float
; lat : float
}
type reply =
{ id : string
; parent_id : string
; date : int
; nick : string
; comment : string
2022-01-25 14:07:28 +01:00
; image : (string * string * string) option
; tags : string list
; replies : string list
; citations : string list
}
type post =
2022-01-29 09:23:57 +01:00
| Op of thread_data * reply
| Reply of reply
2021-12-29 21:07:17 +01:00
module Q = struct
let create_post_user_table =
Caqti_request.exec Caqti_type.unit
"CREATE TABLE IF NOT EXISTS post_user (post_id TEXT, nick TEXT, PRIMARY \
KEY(post_id), FOREIGN KEY(nick) REFERENCES user(nick));"
(* post_id -> OP's post_id *)
let create_post_parent_table =
Caqti_request.exec Caqti_type.unit
"CREATE TABLE IF NOT EXISTS post_parent (post_id TEXT, parent_id TEXT, \
FOREIGN KEY(post_id) REFERENCES post_user(post_id),\n\
\ FOREIGN KEY(parent_id) REFERENCES post_user(post_id));"
let create_thread_table =
Caqti_request.exec Caqti_type.unit
"CREATE TABLE IF NOT EXISTS threads (thread_id TEXT, post_id TEXT,\n\
\ FOREIGN KEY(thread_id) REFERENCES post_user(post_id),\n\
\ FOREIGN KEY(post_id) REFERENCES post_user(post_id));"
let create_post_replies_table =
Caqti_request.exec Caqti_type.unit
"CREATE TABLE IF NOT EXISTS post_replies (post_id TEXT, reply_id TEXT, \
FOREIGN KEY(post_id) REFERENCES post_user(post_id),\n\
\ FOREIGN KEY(reply_id) REFERENCES post_user(post_id));"
let create_post_citations_table =
Caqti_request.exec Caqti_type.unit
"CREATE TABLE IF NOT EXISTS post_citations (post_id TEXT, cited_id TEXT, \
FOREIGN KEY(post_id) REFERENCES post_user(post_id),\n\
\ FOREIGN KEY(cited_id) REFERENCES post_user(post_id));"
let create_post_date_table =
Caqti_request.exec Caqti_type.unit
"CREATE TABLE IF NOT EXISTS post_date (post_id TEXT, date INT, FOREIGN \
KEY(post_id) REFERENCES post_user(post_id));"
let create_post_comment_table =
Caqti_request.exec Caqti_type.unit
"CREATE TABLE IF NOT EXISTS post_comment (post_id TEXT, comment TEXT, \
FOREIGN KEY(post_id) REFERENCES post_user(post_id));"
let create_post_image_table =
Caqti_request.exec Caqti_type.unit
"CREATE TABLE IF NOT EXISTS post_image (post_id TEXT, image_name TEXT, \
2022-01-25 14:07:28 +01:00
image_content TEXT, image_alt TEXT, FOREIGN KEY(post_id) REFERENCES \
post_user(post_id));"
2021-12-29 21:07:17 +01:00
let create_post_gps_table =
Caqti_request.exec Caqti_type.unit
"CREATE TABLE IF NOT EXISTS post_gps (post_id TEXT, lat FLOAT, lng FLOAT ,\n\
\ FOREIGN KEY(post_id) REFERENCES post_user(post_id));"
let create_post_subject_table =
Caqti_request.exec Caqti_type.unit
"CREATE TABLE IF NOT EXISTS post_subject (post_id TEXT, subject TEXT, \
FOREIGN KEY(post_id) REFERENCES post_user(post_id));"
let create_post_tags_table =
Caqti_request.exec Caqti_type.unit
"CREATE TABLE IF NOT EXISTS post_tags (post_id TEXT, tag TEXT, FOREIGN \
KEY(post_id) REFERENCES post_user(post_id));"
let upload_post_id =
Caqti_request.exec
Caqti_type.(tup2 string string)
"INSERT INTO post_user VALUES (?,?);"
let upload_post_gps =
Caqti_request.exec
Caqti_type.(tup3 string float float)
"INSERT INTO post_gps VALUES (?,?,?);"
let upload_post_image =
Caqti_request.exec
2022-01-25 14:07:28 +01:00
Caqti_type.(tup4 string string string string)
"INSERT INTO post_image VALUES (?,?,?,?);"
2021-12-29 21:07:17 +01:00
let upload_post_reply =
Caqti_request.exec
Caqti_type.(tup2 string string)
"INSERT INTO post_replies VALUES (?,?);"
let upload_post_comment =
Caqti_request.exec
Caqti_type.(tup2 string string)
"INSERT INTO post_comment VALUES (?,?);"
let upload_post_subject =
Caqti_request.exec
Caqti_type.(tup2 string string)
"INSERT INTO post_subject VALUES (?,?);"
let upload_post_tag =
Caqti_request.exec
Caqti_type.(tup2 string string)
"INSERT INTO post_tags VALUES (?,?);"
let upload_post_date =
Caqti_request.exec
Caqti_type.(tup2 string int)
"INSERT INTO post_date VALUES (?,?);"
let upload_to_thread =
Caqti_request.exec
Caqti_type.(tup2 string string)
"INSERT INTO threads VALUES (?,?);"
let upload_post_parent =
Caqti_request.exec
Caqti_type.(tup2 string string)
"INSERT INTO post_parent VALUES (?,?);"
let get_post_nick =
Caqti_request.find Caqti_type.string Caqti_type.string
"SELECT nick FROM post_user WHERE post_id=?;"
let get_post_comment =
Caqti_request.find Caqti_type.string Caqti_type.string
"SELECT comment FROM post_comment WHERE post_id=?;"
let get_post_image_content =
Caqti_request.find_opt Caqti_type.string Caqti_type.string
"SELECT image_content FROM post_image WHERE post_id=?;"
2022-01-25 14:07:28 +01:00
let get_post_image_info =
Caqti_request.find_opt Caqti_type.string
Caqti_type.(tup2 string string)
"SELECT image_name,image_alt FROM post_image WHERE post_id=?;"
2021-12-29 21:07:17 +01:00
let get_post_tags =
Caqti_request.collect Caqti_type.string Caqti_type.string
"SELECT tag FROM post_tags WHERE post_id=?;"
let get_post_date =
Caqti_request.find Caqti_type.string Caqti_type.int
"SELECT date FROM post_date WHERE post_id=?;"
let get_post_citations =
Caqti_request.collect Caqti_type.string Caqti_type.string
"SELECT post_id FROM post_citations WHERE reply_id=?;"
let get_post_replies =
Caqti_request.collect Caqti_type.string Caqti_type.string
"SELECT reply_id FROM post_replies WHERE post_id=?;"
let get_thread_posts =
Caqti_request.collect Caqti_type.string Caqti_type.string
"SELECT post_id FROM threads WHERE thread_id=?;"
2022-01-14 14:05:09 +01:00
let count_thread_posts =
Caqti_request.find Caqti_type.string Caqti_type.int
"SELECT COUNT(post_id) FROM threads WHERE thread_id=?;"
2022-02-18 01:37:25 +01:00
let get_thread =
Caqti_request.find Caqti_type.string Caqti_type.string
"SELECT thread_id FROM threads WHERE thread_id=? LIMIT 1;"
2021-12-29 21:07:17 +01:00
let get_post_subject =
Caqti_request.find_opt Caqti_type.string Caqti_type.string
"SELECT subject FROM post_subject WHERE post_id=?;"
let get_post_gps =
Caqti_request.find_opt Caqti_type.string
Caqti_type.(tup2 float float)
"SELECT lat, lng FROM post_gps WHERE post_id=?;"
let get_threads =
Caqti_request.collect Caqti_type.unit Caqti_type.string
"SELECT thread_id FROM threads;"
2021-12-29 21:07:17 +01:00
end
let () =
let tables =
[ Q.create_post_user_table
; Q.create_post_parent_table
; Q.create_thread_table
; Q.create_post_replies_table
; Q.create_post_citations_table
; Q.create_post_date_table
; Q.create_post_comment_table
; Q.create_post_image_table
; Q.create_post_gps_table
; Q.create_post_subject_table
; Q.create_post_tags_table
]
in
if
List.exists Result.is_error
(List.map (fun query -> Db.exec query ()) tables)
2022-02-18 02:40:04 +01:00
then Dream.error (fun log -> log "can't create table")
2021-12-29 21:07:17 +01:00
2022-01-25 14:07:28 +01:00
let parse_image image =
match image with
| None -> Ok None
2022-02-18 03:22:24 +01:00
| Some (name, content, alt) ->
let name =
match name with
| Some name -> Dream.html_escape name
| None ->
2022-01-25 14:07:28 +01:00
(* make up random name if no name was given *)
2022-02-18 03:22:24 +01:00
Uuidm.to_string (Uuidm.v4_gen random_state ())
2022-01-25 14:07:28 +01:00
in
2022-02-18 18:31:55 +01:00
if not (is_valid_image content) then Error "invalid image"
else if String.length alt > 1000 then Error "Image description too long"
else Ok (Some (name, content, alt))
2022-01-25 14:07:28 +01:00
(*TODO switch to markdown !*)
(* insert html into the comment, and keep tracks of citations :
-wraps lines starting with ">" with a <span class="quote">
-make raw posts uuid into links
(*TODO fix bad link if post is in other thread*)
-keeps tracks of every post cited in this comment
- add <br> at each line *)
2021-12-29 21:07:17 +01:00
let parse_comment comment =
let handle_word w =
let trim_w = String.trim w in
(* '>' is '&gt;' after html_escape *)
if String.starts_with ~prefix:{|&gt;&gt;|} trim_w then
let sub_w = String.sub trim_w 8 (String.length trim_w - 8) in
match Uuidm.of_string sub_w with
| None -> (w, None)
| Some _ ->
let new_w = Format.sprintf {|<a href="#%s">%s</a>|} sub_w w in
(new_w, Some sub_w)
2022-02-18 02:40:04 +01:00
else (w, None)
in
let handle_line l =
let trim_w = String.trim l in
(*insert quote*)
let line =
match
String.starts_with ~prefix:{|&gt;|} trim_w
&& not (String.starts_with ~prefix:{|&gt;&gt;|} trim_w)
with
| false -> l
| true -> {|<span class="quote">|} ^ l ^ {|</span>|}
in
let words = String.split_on_char ' ' line in
let words, cited_posts =
List.fold_left
(fun (acc_words, acc_cited_posts) w ->
match handle_word w with
| w, Some cited_id -> (w :: acc_words, cited_id :: acc_cited_posts)
| w, None -> (w :: acc_words, acc_cited_posts) )
([], []) words
in
let words = List.rev words in
let line =
Format.asprintf "%a"
(Format.pp_print_list
~pp_sep:(fun fmt () -> Format.fprintf fmt " ")
Format.pp_print_string )
words
in
(line, cited_posts)
in
2022-01-14 21:04:52 +01:00
let comment = String.trim comment in
let lines = String.split_on_char '\n' comment in
let lines, cited_posts =
2021-12-29 21:07:17 +01:00
List.fold_left
(fun (acc_lines, acc_cited_posts) l ->
let line, cited_posts = handle_line l in
(line :: acc_lines, cited_posts @ acc_cited_posts) )
([], []) lines
2021-12-29 21:07:17 +01:00
in
let lines = List.rev lines in
(*insert <br>*)
let comment =
Format.asprintf "%a"
(Format.pp_print_list
2022-01-14 14:05:09 +01:00
~pp_sep:(fun fmt () -> Format.fprintf fmt "@.<br>")
Format.pp_print_string )
lines
in
(* remove duplicate cited_id *)
2022-01-27 10:45:34 +01:00
let cited_posts = List.sort_uniq String.compare cited_posts in
2021-12-29 21:07:17 +01:00
(comment, cited_posts)
let upload_post post =
2022-01-29 09:23:57 +01:00
let thread_data, reply =
match post with
2022-01-29 09:23:57 +01:00
| Op (thread_data, reply) -> (Some thread_data, reply)
| Reply reply -> (None, reply)
2021-12-29 21:07:17 +01:00
in
let { id; parent_id; date; nick; comment; image; tags; citations; _ } =
reply
2022-01-29 09:23:57 +01:00
in
let^ () = Db.exec Q.upload_post_id (id, nick) in
let^ () = Db.exec Q.upload_post_comment (id, comment) in
let^ () = Db.exec Q.upload_post_date (id, date) in
let^ () = Db.exec Q.upload_to_thread (parent_id, id) in
let^ () =
match image with
| None -> Ok ()
2022-01-25 14:07:28 +01:00
| Some (image_name, image_content, alt) ->
Db.exec Q.upload_post_image (id, image_name, image_content, alt)
2021-12-29 21:07:17 +01:00
in
(* what is parent and why do i need it again? TODO TODO *)
let^ () = Db.exec Q.upload_post_parent (id, parent_id) in
let^ () =
match
2021-12-29 21:07:17 +01:00
List.find_opt Result.is_error
(List.map (fun tag -> Db.exec Q.upload_post_tag (id, tag)) tags)
with
| Some (Error e) -> Error e
2021-12-29 21:07:17 +01:00
| Some _ -> assert false
| None -> Ok ()
2021-12-29 21:07:17 +01:00
in
let^ () =
match
List.find_opt Result.is_error
(List.map
(fun cited_id -> Db.exec Q.upload_post_reply (cited_id, id))
citations )
with
| Some (Error e) -> Error e
| Some _ -> assert false
| None -> Ok ()
in
2022-01-29 09:23:57 +01:00
match thread_data with
| None -> Ok id
| Some { subject; lng; lat } ->
let^ () = Db.exec Q.upload_post_gps (id, lat, lng) in
let^ () = Db.exec Q.upload_post_subject (id, subject) in
Ok id
2021-12-29 21:07:17 +01:00
2022-01-29 09:23:57 +01:00
let build_reply ~comment ?image ~tags ?parent_id nick =
2022-01-14 21:04:52 +01:00
let comment = Dream.html_escape comment in
let tags = Dream.html_escape tags in
2022-01-29 09:23:57 +01:00
let id = Uuidm.to_string (Uuidm.v4_gen random_state ()) in
(* parent_id is None if this reply is supposed to be a new thread *)
let parent_id = Option.value parent_id ~default:id in
2022-02-18 02:40:04 +01:00
if Option.is_none (Uuidm.of_string parent_id) then Error "invalid thread id"
else if String.length comment > 10000 then Error "invalid comment"
else
2022-01-25 14:07:28 +01:00
match parse_image image with
| Error e -> Error e
| Ok image ->
2022-02-18 02:40:04 +01:00
if String.length tags > 1000 then Error "invalid tags"
else
2022-01-25 14:07:28 +01:00
(* TODO latlng validation? *)
let tag_list = Str.split (Str.regexp " +") tags in
let date = int_of_float (Unix.time ()) in
let comment, citations = parse_comment comment in
2022-01-25 14:07:28 +01:00
let reply =
2022-01-29 09:23:57 +01:00
{ id
; parent_id
; date
; nick
; comment
; image
; tags = tag_list
; replies = []
; citations
}
in
2022-01-29 09:23:57 +01:00
Ok reply
2022-01-25 14:07:28 +01:00
let build_op ~comment ?image ~tags ~subject ~lat ~lng nick =
2022-01-25 14:07:28 +01:00
let subject = Dream.html_escape subject in
2022-01-29 09:23:57 +01:00
(* TODO latlng validation? *)
let is_valid_latlng = true in
2022-02-18 02:40:04 +01:00
if not is_valid_latlng then Error "Invalid coordinate"
else if String.length subject > 600 then Error "Invalid subject"
2022-01-25 14:07:28 +01:00
else
let thread_data = { subject; lng; lat } in
2022-02-02 19:16:53 +01:00
let* reply =
2022-01-29 09:23:57 +01:00
match image with
| Some image -> build_reply ~comment ~image ~tags nick
| None -> build_reply ~comment ~tags nick
in
let op = Op (thread_data, reply) in
Ok op
let make_reply ~comment ?image ~tags ~parent_id nick =
2022-02-02 19:16:53 +01:00
let* reply = build_reply ~comment ?image ~tags ~parent_id nick in
2022-01-29 09:23:57 +01:00
let post = Reply reply in
upload_post post
let make_op ~comment ?image ~tags ~subject ~lat ~lng nick =
let* op = build_op ~comment ?image ~tags ~subject ~lat ~lng nick in
2022-01-29 09:23:57 +01:00
upload_post op
2022-01-29 09:23:57 +01:00
let get_post_image_content post_id =
2022-02-02 19:16:53 +01:00
let^? content = Db.find_opt Q.get_post_image_content post_id in
2022-01-12 15:38:30 +01:00
Ok content