aboutsummaryrefslogtreecommitdiff
path: root/src/server.ml
blob: efd0f0d5623099373aa716cf4bb9a2ea5e1d0489 (plain)
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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
(*
 *    Copyright (C) 2020 -- 2022  Alban Gruin
 *
 *    ucs is free software: you can redistribute it and/or modify
 *    it under the terms of the GNU Affero General Public License as published
 *    by the Free Software Foundation, either version 3 of the License, or
 *    (at your option) any later version.
 *
 *    ucs is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with ucs.  If not, see <http://www.gnu.org/licenses/>.
 *)

open CalendarLib

open Lwt.Infix

open Cohttp
open Cohttp_lwt_unix

let format_date = Printer.Date.sprint "%Y-%m-%d" and
    a_month = Date.Period.make 0 1 0 and
    a_year = Date.Period.make 1 0 0

type fetch_kind =
  | Group
  | Module

let kind_to_res_type = function
  | Group -> "103"
  | Module -> "100"

let kind_to_url = function
  | Group -> "g"
  | Module -> "m"

let skip_if predicate fn value =
  if predicate then
    value
  else
    fn value

let get_tz () =
  Lwt_unix.file_exists "/etc/timezone" >>= fun exists ->
  if exists then
    Lwt_io.(open_file ~mode:Input "/etc/timezone") >>= fun file ->
    Lwt.finalize
      (fun () ->
         Lwt_io.read file >|=
         String.trim)
      (fun () -> Lwt_io.close file)
  else
    Lwt.return ""

let log uri =
  let datetime =
    Printer.Calendar.sprint "%Y-%m-%d %H:%M:%S" @@ Calendar.now () in
  Lwt_io.printlf "[%s] %s" datetime (Uri.path_and_query uri)

let respond ?(mime="text/html; charset=utf-8") ?(status=`OK) body =
  let headers = Header.init_with "Content-Type" mime in
  Server.respond_string ~status ~body ~headers ()

let bad_request = Server.respond_string ~status:`Bad_request ~body:"Bad request\n"

let fetch celcat_url group kind =
  let current_date = Date.today () in
  let lower_date = format_date @@ Date.rem current_date a_month and
      upper_date = format_date @@ Date.add current_date a_year in
  let res_type = kind_to_res_type kind in
  let parameters = Uri.encoded_of_query
                     ["start", [lower_date]; "end", [upper_date];
                      "resType", [res_type]; "calView", ["month"];
                      "federationIds[]", [group]; "colourScheme", ["3"]] in
  let body = Cohttp_lwt.Body.of_string parameters and
      headers = Header.init_with "Content-Type" "application/x-www-form-urlencoded; charset=UTF-8" in
  Client.post ~body ~headers celcat_url
  >>= fun (_resp, body) ->
  Cohttp_lwt.Body.to_string body

let fetch_and_respond ?(groups=[]) tzname celcat_url file kind =
  let id = String.(sub file 0 (length file - 4)) in
  fetch celcat_url id kind >>= fun body ->
  Course.decode body
  |> skip_if (kind = Group || groups = []) (Ics.filter_groups groups)
  |> Ics.to_string tzname
  |> respond ~mime:"text/calendar; charset=utf-8"

let get_link ?(args=[]) base_url kind id =
  let args =
    if args = [] then ""
    else "?" ^ Uri.encoded_of_query args in
  base_url ^ "/ics/" ^ kind_to_url kind ^ "/" ^ id ^ ".ics" ^ args

let respond_link ?(groups=[]) base_url kind id =
  let page_args, args = match groups with
    | [] -> None, []
    | groups -> Some (id, groups), ["groups", groups] in
  get_link ~args base_url kind id
  |> Pages.link ?args:page_args
  |> respond

let get_module_and_groups_from_request body found not_found =
  Cohttp_lwt.Body.to_string body >>= fun body ->
  let data = Uri.query_of_encoded body in
  let groups, module_id, valid = List.fold_right (fun (k, v) (groups, module_id, valid) ->
      match k, v, module_id, valid with
      | _, _, _, false -> groups, module_id, false
      | "groups", ([] | [""]), _, _ -> groups, module_id, true
      | "groups", [group], _, _ -> group :: groups, module_id, true
      | "groups", g, _, _ -> g @ groups, module_id, true
      | "module", [module_id], None, _ -> groups, Some module_id, true
      | _, _, _, _ -> groups, None, false) data ([], None, true) in
  match module_id, valid with
  | Some module_id, true ->
    found module_id groups
  | _, _ -> not_found ()

let get_module_link base_url body =
  get_module_and_groups_from_request body
    (fun module_id groups ->
       respond_link ~groups base_url Module module_id)
    bad_request

let list_events_with_empty_location celcat_url body =
  get_module_and_groups_from_request body
    (fun module_id groups ->
       fetch celcat_url module_id Module >>= fun body ->
       Course.decode body
       |> skip_if (groups = []) (Ics.filter_groups groups)
       |> Ics.filter_empty_locations
       |> Pages.empty_events
       |> respond)
    bad_request

let check_ics = Astring.String.is_suffix ~affix:".ics"

let serve base_url celcat_url mode stop =
  get_tz () >>= fun tzname ->
  let callback _conn req body =
    let meth = Request.meth req and
        uri = Request.uri req in
    let path = Uri.path uri
               |> Astring.String.cuts ~empty:false ~sep:"/"
               |> List.map Uri.pct_decode and
        query = Uri.query uri in
    log uri >>= fun () ->
    match meth, path, query with
    | `GET, ([] | ["index.html"]), [] ->
       respond Pages.main
    | `GET, ["lnk"], ["group", [group]] ->
       respond_link base_url Group group
    | `POST, ["lnk"], [] ->
       get_module_link base_url body
    | `GET, ["ics"; "g"; file], [] when check_ics file ->
       fetch_and_respond tzname celcat_url file Group
    | `GET, ["ics"; "m"; file], ([] as groups | ["groups", groups]) when check_ics file ->
       fetch_and_respond ~groups tzname celcat_url file Module
    | `GET, ["filter"], ["module", [module_id]] ->
       fetch celcat_url module_id Module >>= fun body ->
       Course.decode body
       |> Ics.get_all_groups
       |> Pages.select module_id
       |> respond
    | `POST, ["empty"], [] ->
       list_events_with_empty_location celcat_url body
    | (`GET | `POST), ["lnk"], _ | `GET, ["filter"], _ | `POST, ["empty"], _ ->
       bad_request ()
    | `GET, ["ics"; "g" | "m"; file], _ when check_ics file ->
       bad_request ()
    | `GET, _, _ ->
       Server.respond_string ~status:`Not_found ~body:"Not found\n" ()
    | _ ->
       Server.respond_string
         ~status:`Method_not_allowed ~body:"Method not allowed\n" () in
  Server.create ~stop ~mode (Server.make ~callback ())