Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #6 from nicklayb/2.0.0
2.0.0
- Loading branch information
Showing
18 changed files
with
290 additions
and
176 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,6 @@ | |
|
||
@import "clock"; | ||
@import "weather"; | ||
@import "rss"; | ||
@import "news"; | ||
@import "calendar"; | ||
@import "suntime"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
defmodule Snowhite.Modules.News.Adapter do | ||
@moduledoc """ | ||
Adapter for a given news feed. By default when you use the News module, it uses | ||
the RSS adapter because it assumes that the news feed you provided is a RSS feed. | ||
To use another adapter, simply implements the following callback. You can also | ||
look at the provided adapters to see one matches your needs. | ||
""" | ||
@callback fetch(url :: String.t(), options :: map()) :: [News.Item.t()] | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
defmodule Snowhite.Modules.News.Adapters.Json do | ||
@moduledoc """ | ||
The JSON adapter can be used to fetch JSON endpoints. It must have a `item_mapper` | ||
to know how each fields are mapped since every JSON endpoints behaves differently. | ||
It also supports couples of HTTP options to customize the underlying `HTTPoison.request/5` | ||
call. | ||
## Options | ||
- `item_mapper` **required**: A MF (`{module, funciton}`) that received two arguments, | ||
the json payload and the options. This function must return a list of `Item.t()`. | ||
- `body` (default `GET`): The method to use for the HTTP call. | ||
- `headers` (default `[]`): Headers for the HTTP call. | ||
- `body` (default `""`): Body content, in case of a non-GET call. | ||
- `http_poison_options` (default `[]`): Other option for `HTTPoison`. | ||
""" | ||
@behaviour Snowhite.Modules.News.Adapter | ||
|
||
def fetch(url, %{item_mapper: {module, function}} = options) do | ||
method = Map.get(options, :method, "GET") | ||
body = Map.get(options, :body, "") | ||
headers = Map.get(options, :headers, []) | ||
http_poison_options = Map.get(options, :http_poison_options, []) | ||
|
||
with {:ok, %{body: body}} <- | ||
HTTPoison.request(method, url, body, headers, http_poison_options), | ||
{:ok, json} <- Jason.decode(body) do | ||
apply(module, function, [json, options]) | ||
else | ||
_error -> | ||
[] | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
defmodule Snowhite.Modules.News.Adapters.Rss do | ||
@moduledoc """ | ||
Implements RSS feed lookup. The Feeds must have the following properties | ||
- `id` a unique identifier of the feed | ||
- `title` the title, most likely the headline of the article | ||
- `rss2:link` the url to read the news. This will be used to generate the QR code. | ||
- `date` date at which the article has been published. | ||
The RSS adapter doesn't support any options by now. | ||
""" | ||
@behaviour Snowhite.Modules.News.Adapter | ||
|
||
alias Snowhite.Helpers.List, as: ListHelper | ||
alias Snowhite.Modules.News.Item | ||
|
||
def fetch(url, _) do | ||
with {:ok, %{entries: entries}} <- Rss.Poller.poll(url) do | ||
ListHelper.filter_map(entries, &valid?/1, &to_rss_item/1) | ||
else | ||
_ -> [] | ||
end | ||
end | ||
|
||
defp to_rss_item(%{id: id} = entry) do | ||
%Item{ | ||
id: id, | ||
original_url: Map.get(entry, :"rss2:link"), | ||
date: Map.get(entry, :updated), | ||
title: String.trim(Map.get(entry, :title, "")) | ||
} | ||
end | ||
|
||
@required_fields [:id, :title, :"rss2:link", :updated] | ||
defp valid?(item) do | ||
Enum.all?(@required_fields, fn field -> | ||
not (item | ||
|> Map.get(field) | ||
|> is_nil()) | ||
end) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
defmodule Snowhite.Modules.News.Feed do | ||
@moduledoc """ | ||
News Feed structure | ||
""" | ||
defstruct [:name, :url, :adapter, :options] | ||
|
||
require Logger | ||
|
||
alias __MODULE__ | ||
|
||
@default_adapter Snowhite.Modules.News.Adapters.Rss | ||
|
||
@type init_arguments :: | ||
{String.t(), String.t()} | ||
| %{name: String.t(), url: String.t(), adapter: module(), options: map()} | ||
|
||
@type t :: %Feed{name: String.t(), url: String.t(), adapter: module(), options: map()} | ||
|
||
@doc "Initialize a feed structure" | ||
@spec new(init_arguments()) :: t() | ||
def new({name, url}) do | ||
new(%{name: name, url: url, adapter: @default_adapter, options: %{}}) | ||
end | ||
|
||
def new(%{name: name, url: url, adapter: adapter, options: options}) do | ||
%Feed{name: name, url: url, adapter: adapter, options: options} | ||
end | ||
|
||
def call_adapter(%Feed{name: name, url: url, adapter: adapter, options: options}) do | ||
Logger.info("[#{inspect(adapter)}] [#{name}] Polling #{url}") | ||
news = apply(adapter, :fetch, [url, options]) | ||
Logger.info("[#{inspect(adapter)}] [#{name}] got #{length(news)} news") | ||
news | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
defmodule Snowhite.Modules.News.Item do | ||
@moduledoc """ | ||
News feed item | ||
""" | ||
defstruct [:id, :title, :original_url, :short_url, :date, :qr_code] | ||
|
||
alias __MODULE__ | ||
|
||
@type t :: %Item{ | ||
title: String.t(), | ||
original_url: String.t(), | ||
short_url: String.t(), | ||
date: String.t(), | ||
qr_code: EQRCode.Matrix.t() | ||
} | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
defmodule Snowhite.Modules.News.Server do | ||
use GenServer | ||
|
||
alias Snowhite.Modules.News.Item | ||
alias Snowhite.Modules.News.Feed | ||
alias Snowhite.UrlShortener | ||
import Snowhite.Helpers.Timing | ||
require Logger | ||
|
||
@auto_sync_timer ~d(15m) | ||
|
||
@spec start_link(any) :: GenServer.on_start() | ||
def start_link(args) do | ||
GenServer.start_link(__MODULE__, args, name: __MODULE__) | ||
end | ||
|
||
@doc "Updates the news feed" | ||
@spec update() :: :ok | ||
def update do | ||
GenServer.cast(__MODULE__, :update) | ||
end | ||
|
||
@doc "Gets server's news feeds" | ||
@spec news() :: [{String.t(), [Item.t()]}] | ||
def news do | ||
GenServer.call(__MODULE__, :news) | ||
end | ||
|
||
@impl GenServer | ||
def init(options) do | ||
{feeds, options} = Keyword.pop!(options, :feeds) | ||
Logger.info("[#{__MODULE__}] Started with #{length(feeds)} feeds") | ||
send(self(), :auto_sync) | ||
{:ok, %{options: options, feeds: init_feeds(feeds), news: []}} | ||
end | ||
|
||
@impl GenServer | ||
def handle_cast(:update, state) do | ||
send(self(), :notify) | ||
{:noreply, update(state)} | ||
end | ||
|
||
@impl GenServer | ||
def handle_call(:news, _, %{news: news} = state) do | ||
{:reply, news, state} | ||
end | ||
|
||
@impl GenServer | ||
def handle_info(:auto_sync, %{options: options} = state) do | ||
state = update(state) | ||
update_later(options) | ||
send(self(), :notify) | ||
{:noreply, state} | ||
end | ||
|
||
def handle_info(:notify, state) do | ||
Phoenix.PubSub.broadcast!(Snowhite.PubSub, "snowhite:modules:news", :updated) | ||
{:noreply, state} | ||
end | ||
|
||
defp update(%{feeds: feeds} = state) do | ||
news = Enum.map(feeds, &map_item(&1, state)) | ||
|
||
%{state | news: news} | ||
end | ||
|
||
defp map_item(%Feed{name: name} = feed, state) do | ||
news = | ||
feed | ||
|> Feed.call_adapter() | ||
|> Enum.map(fn item -> | ||
item | ||
|> shorten_url(state) | ||
|> put_qr_code(state) | ||
end) | ||
|
||
{name, news} | ||
end | ||
|
||
defp put_qr_code(%Item{short_url: short_url} = item, %{options: options}) do | ||
if Keyword.get(options, :qr_codes, true) do | ||
%Item{item | qr_code: EQRCode.encode(short_url)} | ||
else | ||
item | ||
end | ||
end | ||
|
||
defp shorten_url(%Item{original_url: url} = item, %{options: options}) do | ||
short = if Keyword.get(options, :short_link, true), do: shorten_url(url), else: url | ||
|
||
%Item{item | short_url: short} | ||
end | ||
|
||
defp shorten_url(link) when is_bitstring(link) do | ||
case UrlShortener.shorten(link) do | ||
nil -> link | ||
short -> short | ||
end | ||
end | ||
|
||
defp update_later(options) do | ||
Process.send_after(self(), :auto_sync, Keyword.get(options, :refresh, @auto_sync_timer)) | ||
end | ||
|
||
defp init_feeds(feeds) do | ||
Enum.map(feeds, &Feed.new/1) | ||
end | ||
end |
Oops, something went wrong.