Current section
44 Versions
Jump to
Current section
44 Versions
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, |