Current section

44 Versions

Jump to

Compare versions

4 files changed
+7 additions
-242 deletions
  @@ -4,12 +4,11 @@
4 4 <<"A library for the reverse-engineered LumiNUS API (https://luminus.nus.edu.sg)">>}.
5 5 {<<"elixir">>,<<"~> 1.6">>}.
6 6 {<<"files">>,
7 - [<<"lib">>,<<"lib/mix">>,<<"lib/mix/tasks">>,<<"lib/mix/tasks/fluminus.ex">>,
8 - <<"lib/fluminus.ex">>,<<"lib/fluminus">>,<<"lib/fluminus/constants.ex">>,
9 - <<"lib/fluminus/http_client.ex">>,<<"lib/fluminus/api">>,
10 - <<"lib/fluminus/api/module.ex">>,<<"lib/fluminus/api/file.ex">>,
11 - <<"lib/fluminus/api.ex">>,<<"lib/fluminus/application.ex">>,
12 - <<"lib/fluminus/cli.ex">>,<<"lib/fluminus/authorization.ex">>,
7 + [<<"lib">>,<<"lib/fluminus.ex">>,<<"lib/fluminus">>,
8 + <<"lib/fluminus/constants.ex">>,<<"lib/fluminus/http_client.ex">>,
9 + <<"lib/fluminus/api">>,<<"lib/fluminus/api/module.ex">>,
10 + <<"lib/fluminus/api/file.ex">>,<<"lib/fluminus/api.ex">>,
11 + <<"lib/fluminus/application.ex">>,<<"lib/fluminus/authorization.ex">>,
13 12 <<".formatter.exs">>,<<"mix.exs">>,<<"README.md">>,<<"LICENSE">>]}.
14 13 {<<"licenses">>,[<<"MIT">>]}.
15 14 {<<"links">>,[{<<"GitHub">>,<<"https://github.com/indocomsoft/fluminus">>}]}.
  @@ -45,4 +44,4 @@
45 44 {<<"optional">>,false},
46 45 {<<"repository">>,<<"hexpm">>},
47 46 {<<"requirement">>,<<"~> 1.1">>}]]}.
48 - {<<"version">>,<<"0.2.2">>}.
47 + {<<"version">>,<<"0.2.3">>}.
  @@ -1,205 +0,0 @@
1 - defmodule Fluminus.CLI do
2 - @moduledoc """
3 - Provides functions related to Fluminus' CLI.
4 - """
5 -
6 - @config_file "config.json"
7 -
8 - alias Fluminus.API.File
9 - alias Fluminus.Authorization
10 -
11 - def run(args) do
12 - HTTPoison.start()
13 - {username, password} = load_credentials()
14 -
15 - case Authorization.jwt(username, password) do
16 - {:ok, auth} ->
17 - save_credentials(username, password)
18 - run(args, auth)
19 -
20 - {:error, :invalid_credentials} ->
21 - IO.puts("Invalid credentials!")
22 - clear_credentials()
23 - run(args)
24 -
25 - {:error, error} ->
26 - IO.puts("Error: #{inspect(error)}")
27 - end
28 - end
29 -
30 - @spec run([String.t()], Authorization.t()) :: :ok
31 - defp run(args, auth = %Authorization{}) do
32 - {parsed, _, _} = OptionParser.parse(args, strict: [announcements: :boolean, files: :boolean, download_to: :string])
33 -
34 - IO.puts("Hi #{Fluminus.API.name(auth)}")
35 - modules = Fluminus.API.modules(auth, true)
36 - IO.puts("You are taking:")
37 - modules |> Enum.filter(&(not &1.teaching?)) |> Enum.each(&IO.puts("- #{&1.code} #{&1.name}"))
38 - IO.puts("And teaching:")
39 - modules |> Enum.filter(& &1.teaching?) |> Enum.each(&IO.puts("- #{&1.code} #{&1.name}"))
40 - IO.puts("")
41 -
42 - Enum.each(parsed, fn
43 - {:announcements, true} -> list_announcements(auth, modules)
44 - {:files, true} -> list_files(auth, modules)
45 - {:download_to, path} -> download_to(auth, modules, path)
46 - end)
47 - end
48 -
49 - defp download_to(auth, modules, path) do
50 - IO.puts("Download to #{path}")
51 -
52 - if Elixir.File.exists?(path) do
53 - for mod <- modules do
54 - IO.puts("## #{mod.code}\n")
55 -
56 - mod
57 - |> File.from_module(auth)
58 - |> download_file(auth, path)
59 -
60 - IO.puts("\n")
61 - end
62 - else
63 - IO.puts("Download destination does not exist!")
64 - end
65 - end
66 -
67 - defp download_file(file, auth, path) do
68 - destination = Path.join(path, file.name)
69 -
70 - if file.directory? do
71 - Elixir.File.mkdir_p!(destination)
72 -
73 - file.children
74 - |> Enum.map(&File.load_children(&1, auth))
75 - |> Enum.each(&download_file(&1, auth, destination))
76 - else
77 - case File.download(file, auth, path) do
78 - :ok -> IO.puts("Downloaded to #{destination}")
79 - {:error, :exists} -> :ok
80 - {:error, reason} -> IO.puts("Unable to download to #{destination}, reason: #{reason}")
81 - end
82 - end
83 - end
84 -
85 - defp list_files(auth, modules) do
86 - IO.puts("\n# Files:\n")
87 -
88 - for mod <- modules do
89 - IO.puts("## #{mod.code} #{mod.name}")
90 -
91 - mod |> File.from_module(auth) |> list_file(auth)
92 - IO.puts("")
93 - end
94 - end
95 -
96 - defp list_announcements(auth, modules) do
97 - IO.puts("\n# Announcements:\n")
98 -
99 - for mod <- modules do
100 - IO.puts("## #{mod.code} #{mod.name}")
101 -
102 - for {title, description} <- Fluminus.API.Module.announcements(mod, auth) do
103 - IO.puts("=== #{title} ===")
104 - IO.puts(description)
105 - end
106 -
107 - IO.puts("")
108 - end
109 - end
110 -
111 - @spec load_credentials :: {String.t(), String.t()}
112 - defp load_credentials do
113 - with {:ok, data} <- Elixir.File.read(@config_file),
114 - {:ok, decoded} <- Jason.decode(data) do
115 - {decoded["username"], decoded["password"]}
116 - else
117 - _ ->
118 - username = IO.gets("username: ") |> String.trim()
119 - password = password_get("password: ") |> String.trim()
120 - {username, password}
121 - end
122 - end
123 -
124 - @spec clear_credentials() :: :ok | nil
125 - defp clear_credentials do
126 - case Elixir.File.rm(@config_file) do
127 - :ok ->
128 - IO.puts("Cleared stored credentials")
129 - :ok
130 -
131 - {:error, _} ->
132 - nil
133 - end
134 - end
135 -
136 - @spec save_credentials(String.t(), String.t()) :: :ok | :error
137 - defp save_credentials(username, password) when is_binary(username) and is_binary(password) do
138 - data = %{username: username, password: password}
139 -
140 - with {:exists, false} <- {:exists, Elixir.File.exists?(@config_file)},
141 - true <- confirm?("Do you want to store your credential? (WARNING: they are stored in plain text) [y/n]"),
142 - {:ok, encoded} <- Jason.encode(data),
143 - :ok <- Elixir.File.write("config.json", encoded) do
144 - :ok
145 - else
146 - {:exists, true} ->
147 - :ok
148 -
149 - {:error, reason} ->
150 - IO.puts("Unable to save credentials: #{reason}")
151 - :error
152 - end
153 - end
154 -
155 - @spec confirm?(String.t()) :: bool()
156 - defp confirm?(prompt) when is_binary(prompt) do
157 - answer = prompt |> IO.gets() |> String.trim() |> String.downcase()
158 -
159 - case answer do
160 - "y" -> true
161 - "n" -> false
162 - _ -> confirm?(prompt)
163 - end
164 - end
165 -
166 - @spec list_file(File.t(), Authorization.t()) :: :ok
167 - defp list_file(file, auth), do: list_file(file, auth, "")
168 -
169 - defp list_file(file, auth, prefix) when is_binary(prefix) do
170 - if file.directory? do
171 - file.children
172 - |> Enum.map(&File.load_children(&1, auth))
173 - |> Enum.each(&list_file(&1, auth, "#{prefix}/#{file.name}"))
174 - else
175 - IO.puts("#{prefix}/#{file.name}")
176 - end
177 - end
178 -
179 - # From Mix.Hex.Utils
180 - # Password prompt that hides input by every 1ms
181 - # clearing the line with stderr
182 - @spec password_get(String.t()) :: String.t()
183 - defp password_get(prompt) do
184 - pid = spawn_link(fn -> loop(prompt) end)
185 - ref = make_ref()
186 - value = IO.gets(prompt)
187 -
188 - send(pid, {:done, self(), ref})
189 - receive do: ({:done, ^pid, ^ref} -> :ok)
190 -
191 - value
192 - end
193 -
194 - defp loop(prompt) do
195 - receive do
196 - {:done, parent, ref} ->
197 - send(parent, {:done, self(), ref})
198 - IO.write(:standard_error, "\e[2K\r")
199 - after
200 - 1 ->
201 - IO.write(:standard_error, "\e[2K\r#{prompt}")
202 - loop(prompt)
203 - end
204 - end
205 - end
  @@ -1,29 +0,0 @@
1 - defmodule Mix.Tasks.Fluminus do
2 - @help """
3 - mix fluminus [OPTIONS]
4 -
5 - --announcements Show announcements
6 - --files Show files
7 - --download-to=PATH Download files to PATH
8 - """
9 -
10 - @moduledoc """
11 - Runs the Fluminus CLI.
12 -
13 - ```
14 - #{@help}
15 - ```
16 - """
17 -
18 - @shortdoc "Runs the Fluminus CLI."
19 -
20 - use Mix.Task
21 -
22 - def run(args) do
23 - if "--help" in args or "-h" in args do
24 - IO.puts(@help)
25 - else
26 - Fluminus.CLI.run(args)
27 - end
28 - end
29 - end
  @@ -4,7 +4,7 @@ defmodule Fluminus.MixProject do
4 4 def project do
5 5 [
6 6 app: :fluminus,
7 - version: "0.2.2",
7 + version: "0.2.3",
8 8 elixir: "~> 1.6",
9 9 elixirc_paths: elixirc_paths(Mix.env()),
10 10 start_permanent: Mix.env() == :prod,