diff --git a/lib/frenzy/opml/importer.ex b/lib/frenzy/opml/importer.ex new file mode 100644 index 0000000..bb33194 --- /dev/null +++ b/lib/frenzy/opml/importer.ex @@ -0,0 +1,66 @@ +defmodule Frenzy.OPML.Importer do + import Record + + @typedoc """ + The data created from an OPML import. + A list of groups. Each group has a name (String) or :default (if the OPML specified no group) and a list of feed URLs. + """ + @type import_data() :: %{optional(String.t() | :default) => [String.t()]} + + defrecord :xmlElement, extract(:xmlElement, from_lib: "xmerl/include/xmerl.hrl") + defrecord :xmlAttribute, extract(:xmlAttribute, from_lib: "xmerl/include/xmerl.hrl") + + @spec parse_opml(String.t()) :: import_data() + def parse_opml(text) do + {doc, _} = + text + |> String.trim() + |> :binary.bin_to_list() + |> :xmerl_scan.string() + + outline_elements = :xmerl_xpath.string('/opml/body/outline', doc) + + outline_elements + |> Enum.flat_map(&get_feeds/1) + |> Enum.reduce(%{}, fn {group, feed_url}, acc -> + Map.update(acc, group, [], fn feeds -> [feed_url | feeds] end) + end) + end + + defp get_feeds(outline_el) do + attributes = xmlElement(outline_el, :attributes) + + # if the contains a xmlUrl attribute, it is a top level feed (uses the :default group) + # otherwise, it is a group of feeds + + if Enum.any?(attributes, fn attr -> xmlAttribute(attr, :name) == :xmlUrl end) do + [{:default, get_feed_from_outline(outline_el)}] + else + get_feeds_from_group(outline_el) + end + end + + defp get_feed_from_outline(feed_el) do + [attr] = :xmerl_xpath.string('//@xmlUrl', feed_el) + + xmlAttribute(attr, :value) + |> List.to_string() + end + + defp get_feeds_from_group(group_el) do + [title_attr] = :xmerl_xpath.string('/outline/@title', group_el) + + group_title = + xmlAttribute(title_attr, :value) + |> List.to_string() + + :xmerl_xpath.string('/outline/outline/@xmlUrl', group_el) + |> Enum.map(fn attr -> + feed_url = + xmlAttribute(attr, :value) + |> List.to_string() + + {group_title, feed_url} + end) + end +end diff --git a/lib/frenzy_web/controllers/account_controller.ex b/lib/frenzy_web/controllers/account_controller.ex index 4910b34..34e1d93 100644 --- a/lib/frenzy_web/controllers/account_controller.ex +++ b/lib/frenzy_web/controllers/account_controller.ex @@ -1,6 +1,6 @@ defmodule FrenzyWeb.AccountController do use FrenzyWeb, :controller - alias Frenzy.{Repo, User, FervorClient} + alias Frenzy.{Repo, User, FervorClient, Filter} alias FrenzyWeb.Router.Helpers, as: Routes alias FrenzyWeb.Endpoint @@ -102,4 +102,48 @@ defmodule FrenzyWeb.AccountController do redirect(conn, to: Routes.account_path(Endpoint, :show)) end + + def import(conn, %{"file" => %Plug.Upload{} = file}) do + user = conn.assigns[:user] + + {:ok, content} = File.read(file.path) + + parsed = Frenzy.OPML.Importer.parse_opml(content) + + total_imported_feeds = + Enum.map(parsed, fn {group_id, feeds} -> + group_title = + case group_id do + :default -> "Default" + _ when is_binary(group_id) -> group_id + end + + group_changeset = Ecto.build_assoc(user, :groups, %{title: group_title}) + + {:ok, group} = Repo.insert(group_changeset) + + Enum.each(feeds, fn feed_url -> + feed_changeset = + Ecto.build_assoc(group, :feeds, %{ + feed_url: feed_url, + filter: %Filter{ + mode: "reject", + score: 0 + } + }) + + {:ok, _feed} = Repo.insert(feed_changeset) + end) + + Enum.count(feeds) + end) + |> Enum.sum() + + conn + |> put_flash( + :info, + "Imported #{Enum.count(parsed)} groups and #{total_imported_feeds} feeds." + ) + |> redirect(to: Routes.group_path(Endpoint, :index)) + end end diff --git a/lib/frenzy_web/router.ex b/lib/frenzy_web/router.ex index bd71ace..e5dc4d8 100644 --- a/lib/frenzy_web/router.ex +++ b/lib/frenzy_web/router.ex @@ -43,6 +43,7 @@ defmodule FrenzyWeb.Router do get "/account/change_fever_password", AccountController, :change_fever_password post "/account/change_fever_password", AccountController, :do_change_fever_password post "/account/remove_client", AccountController, :remove_client + post "/account/import", AccountController, :import get "/", GroupController, :index resources "/groups", GroupController, except: [:edit, :update] diff --git a/lib/frenzy_web/templates/account/show.html.eex b/lib/frenzy_web/templates/account/show.html.eex index 30babda..ecca72e 100644 --- a/lib/frenzy_web/templates/account/show.html.eex +++ b/lib/frenzy_web/templates/account/show.html.eex @@ -5,32 +5,42 @@ Change Fever Password -

Approved Clients

+
+

Import/Export Data

+ <%= form_for @conn, Routes.account_path(@conn, :import), [method: :post, multipart: true], fn f -> %> + <%= file_input f, :file %> + <%= submit "Import OPML", class: "btn btn-primary" %> + <% end %> +
- - - - - - - - - <%= for {approved, fervor} <- @clients do %> +
+

Approved Clients

+ +
ClientRevoke Access
+ - - + + - <% end %> - -
- <%= if fervor.website do %> - <%= fervor.client_name %> - <% else %> - <%= fervor.client_name %> - <% end %> - - <%= form_tag Routes.account_path(@conn, :remove_client), method: :post do %> - - <%= submit "Revoke", class: "btn btn-danger" %> - <% end %> - ClientRevoke Access
\ No newline at end of file + + + <%= for {approved, fervor} <- @clients do %> + + + <%= if fervor.website do %> + <%= fervor.client_name %> + <% else %> + <%= fervor.client_name %> + <% end %> + + + <%= form_tag Routes.account_path(@conn, :remove_client), method: :post do %> + + <%= submit "Revoke", class: "btn btn-danger" %> + <% end %> + + + <% end %> + + +