Compare commits
No commits in common. "main" and "master" have entirely different histories.
|
@ -1,56 +0,0 @@
|
|||
// const esbuild = require("esbuild");
|
||||
import * as esbuild from "esbuild";
|
||||
import {sassPlugin} from "esbuild-sass-plugin";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const watch = args.includes("--watch");
|
||||
const deploy = args.includes("--deploy");
|
||||
|
||||
const loader = {
|
||||
".svg": "file",
|
||||
".eot": "file",
|
||||
".ttf": "file",
|
||||
".woff": "file",
|
||||
".otf": "file",
|
||||
};
|
||||
|
||||
const plugins = [
|
||||
sassPlugin(),
|
||||
];
|
||||
|
||||
let opts = {
|
||||
entryPoints: ["js/app.js"],
|
||||
bundle: true,
|
||||
target: "es2017",
|
||||
outdir: "../priv/static/assets",
|
||||
logLevel: "info",
|
||||
loader,
|
||||
plugins,
|
||||
};
|
||||
|
||||
if (watch) {
|
||||
opts = {
|
||||
...opts,
|
||||
watch,
|
||||
sourcemap: "inline",
|
||||
};
|
||||
}
|
||||
|
||||
if (deploy) {
|
||||
opts = {
|
||||
...opts,
|
||||
minify: true,
|
||||
};
|
||||
}
|
||||
|
||||
const promise = esbuild.build(opts);
|
||||
|
||||
if (watch) {
|
||||
promise.then(result => {
|
||||
process.stdin.on("close", () => {
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
process.stdin.resume();
|
||||
});
|
||||
}
|
|
@ -83,20 +83,15 @@ label.sidebar-toggle > .oi {
|
|||
.sidebar .nav-item a {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.sidebar .nav-item a:hover {
|
||||
text-decoration: none;
|
||||
color: var(--bs-link-color);
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.sidebar .nav-link.active {
|
||||
color: var(--bs-link-color);
|
||||
}
|
||||
|
||||
.sidebar .nav-item details summary a {
|
||||
padding: 0;
|
||||
color: var(--blue);
|
||||
}
|
||||
|
||||
.sidebar .nav-item details summary a:hover {
|
||||
|
@ -146,14 +141,6 @@ label.sidebar-toggle > .oi {
|
|||
display: inline-block;
|
||||
}
|
||||
|
||||
.item-content img {
|
||||
.post-content img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.item-content > .raw-content {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.item-table tr > .date {
|
||||
min-width: 200px;
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,16 +1,30 @@
|
|||
{
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.15.7",
|
||||
"esbuild-sass-plugin": "^2.3.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "5.2.1",
|
||||
"jquery": "^3.6.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"phoenix": "file:../deps/phoenix",
|
||||
"phoenix_html": "file:../deps/phoenix_html",
|
||||
"phoenix_live_view": "file:../deps/phoenix_live_view",
|
||||
"@popperjs/core": "^2.11.6"
|
||||
}
|
||||
"scripts": {
|
||||
"deploy": "webpack --mode production",
|
||||
"watch": "webpack --mode development --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"bootstrap": "^4.5.0",
|
||||
"jquery": "^3.5.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"phoenix": "file:../deps/phoenix",
|
||||
"phoenix_html": "file:../deps/phoenix_html",
|
||||
"phoenix_live_view": "file:../deps/phoenix_live_view",
|
||||
"popper.js": "^1.16.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.0.0",
|
||||
"@babel/preset-env": "^7.0.0",
|
||||
"babel-loader": "^8.0.0",
|
||||
"copy-webpack-plugin": "^5.1.1",
|
||||
"css-loader": "^3.4.2",
|
||||
"mini-css-extract-plugin": "^0.9.0",
|
||||
"node-sass": "^4.13.1",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
||||
"sass-loader": "^8.0.2",
|
||||
"terser-webpack-plugin": "^2.3.2",
|
||||
"url-loader": "^4.1.0",
|
||||
"webpack": "4.41.5",
|
||||
"webpack-cli": "^3.3.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
|
||||
const TerserPlugin = require('terser-webpack-plugin');
|
||||
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
|
||||
const CopyWebpackPlugin = require('copy-webpack-plugin');
|
||||
|
||||
module.exports = (env, options) => {
|
||||
const devMode = options.mode !== 'production';
|
||||
|
||||
return {
|
||||
optimization: {
|
||||
minimizer: [
|
||||
new TerserPlugin({ cache: true, parallel: true, sourceMap: devMode }),
|
||||
new OptimizeCSSAssetsPlugin({})
|
||||
]
|
||||
},
|
||||
entry: {
|
||||
'app': glob.sync('./vendor/**/*.js').concat(['./js/app.js'])
|
||||
},
|
||||
output: {
|
||||
filename: '[name].js',
|
||||
path: path.resolve(__dirname, '../priv/static/js'),
|
||||
publicPath: '/js/'
|
||||
},
|
||||
devtool: devMode ? 'source-map' : undefined,
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.js$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.[s]?css$/,
|
||||
use: [
|
||||
MiniCssExtractPlugin.loader,
|
||||
'css-loader',
|
||||
'sass-loader',
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(eot|otf|svg|ttf|woff)/,
|
||||
use: {
|
||||
loader: 'url-loader',
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
plugins: [
|
||||
new MiniCssExtractPlugin({ filename: '../css/app.css' }),
|
||||
new CopyWebpackPlugin([{ from: 'static/', to: '../' }])
|
||||
]
|
||||
}
|
||||
};
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
# is restricted to this project.
|
||||
|
||||
# General application configuration
|
||||
import Config
|
||||
use Mix.Config
|
||||
|
||||
config :frenzy,
|
||||
ecto_repos: [Frenzy.Repo]
|
||||
|
@ -28,24 +28,6 @@ config :logger, :console,
|
|||
# Use Jason for JSON parsing in Phoenix
|
||||
config :phoenix, :json_library, Jason
|
||||
|
||||
config :logger, truncate: :infinity
|
||||
|
||||
config :frenzy, env: config_env()
|
||||
config :frenzy, sentry_enabled: false
|
||||
config :frenzy, external_readability: false
|
||||
config :frenzy, oidc_enabled: false
|
||||
|
||||
config :ueberauth, Ueberauth,
|
||||
providers: [
|
||||
oidc:
|
||||
{Ueberauth.Strategy.OIDC,
|
||||
[
|
||||
default: [
|
||||
provider: :default_oidc
|
||||
]
|
||||
]}
|
||||
]
|
||||
|
||||
# Import environment specific config. This must remain at the bottom
|
||||
# of this file so it overrides the configuration defined above.
|
||||
import_config "#{config_env()}.exs"
|
||||
import_config "#{Mix.env()}.exs"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import Config
|
||||
use Mix.Config
|
||||
|
||||
# For development, we disable any cache and enable
|
||||
# debugging and code reloading.
|
||||
|
@ -12,7 +12,13 @@ config :frenzy, FrenzyWeb.Endpoint,
|
|||
code_reloader: true,
|
||||
check_origin: false,
|
||||
watchers: [
|
||||
node: ["build.js", "--watch", cd: Path.expand("../assets", __DIR__)]
|
||||
node: [
|
||||
"node_modules/webpack/bin/webpack.js",
|
||||
"--mode",
|
||||
"development",
|
||||
"--watch-stdin",
|
||||
cd: Path.expand("../assets", __DIR__)
|
||||
]
|
||||
]
|
||||
|
||||
# ## SSL Support
|
||||
|
@ -68,6 +74,4 @@ config :frenzy, Frenzy.Repo,
|
|||
hostname: "localhost",
|
||||
pool_size: 10
|
||||
|
||||
config :tesla, Tesla.Middleware.Logger, debug: false
|
||||
|
||||
import_config "dev.secret.exs"
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import Config
|
||||
use Mix.Config
|
||||
|
||||
# For production, don't forget to configure the url host
|
||||
# to something meaningful, Phoenix uses this information
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import Config
|
||||
use Mix.Config
|
||||
|
||||
# We don't run a server during test. If one is required,
|
||||
# you can enable the server option below.
|
||||
|
|
|
@ -6,12 +6,4 @@ defmodule Frenzy do
|
|||
Contexts are also responsible for managing your data, regardless
|
||||
if it comes from the database, an external API or others.
|
||||
"""
|
||||
|
||||
def sentry_enabled? do
|
||||
Application.get_env(:frenzy, :sentry_enabled)
|
||||
end
|
||||
|
||||
def oidc_enabled? do
|
||||
Application.get_env(:frenzy, :oidc_enabled)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -16,18 +16,9 @@ defmodule Frenzy.Application do
|
|||
FrenzyWeb.Endpoint,
|
||||
# Starts a worker by calling: Frenzy.Worker.start_link(arg)
|
||||
# {Frenzy.Worker, arg},
|
||||
{Frenzy.UpdateFeeds, name: Frenzy.UpdateFeeds},
|
||||
{Frenzy.BuiltinExtractor, name: Frenzy.BuiltinExtractor}
|
||||
{Frenzy.UpdateFeeds, name: Frenzy.UpdateFeeds}
|
||||
]
|
||||
|
||||
children =
|
||||
if Frenzy.oidc_enabled?() do
|
||||
children ++
|
||||
[{OpenIDConnect.Worker, Application.get_env(:ueberauth, Ueberauth.Strategy.OIDC)}]
|
||||
else
|
||||
children
|
||||
end
|
||||
|
||||
# See https://hexdocs.pm/elixir/Supervisor.html
|
||||
# for other strategies and supported options
|
||||
opts = [strategy: :one_for_one, name: Frenzy.Supervisor]
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
defmodule Frenzy.BuiltinExtractor do
|
||||
use GenServer
|
||||
alias Frenzy.Network
|
||||
require Logger
|
||||
|
||||
@external_url Application.compile_env(:frenzy, :external_readability_url)
|
||||
|
||||
def start_link(state) do
|
||||
GenServer.start_link(__MODULE__, :ok, state)
|
||||
end
|
||||
|
||||
@spec article(String.t(), String.t()) :: Floki.html_tree()
|
||||
def article(url, html) do
|
||||
GenServer.call(__MODULE__, {:article, url, html})
|
||||
end
|
||||
|
||||
def init(_state) do
|
||||
use_external = Application.get_env(:frenzy, :external_readability)
|
||||
|
||||
use_external =
|
||||
if use_external do
|
||||
uri = URI.parse(@external_url)
|
||||
uri = %URI{uri | path: "/status"}
|
||||
uri = URI.to_string(uri)
|
||||
|
||||
case Network.http_get(uri) do
|
||||
{:ok, %Tesla.Env{status: 200}} ->
|
||||
true
|
||||
|
||||
_ ->
|
||||
Logger.warning("Could not reach external readability for healthcheck, disabling")
|
||||
false
|
||||
end
|
||||
else
|
||||
false
|
||||
end
|
||||
|
||||
{:ok, use_external}
|
||||
end
|
||||
|
||||
def handle_call({:article, url, html}, _from, state) do
|
||||
# the genserver state is a boolean telling us whether to use the external readability
|
||||
if state do
|
||||
uri = URI.parse(@external_url)
|
||||
uri = %URI{uri | path: "/readability", query: URI.encode_query(url: url)}
|
||||
uri = URI.to_string(uri)
|
||||
|
||||
Logger.debug("Sending external readability request: #{uri}")
|
||||
|
||||
case Network.http_post(uri, html, headers: [{"content-type", "text/html"}]) do
|
||||
{:ok, %Tesla.Env{status: 200, body: body}} ->
|
||||
{:ok, doc} = Floki.parse_document(body)
|
||||
{:reply, doc, state}
|
||||
|
||||
{:ok, %Tesla.Env{status: status}} ->
|
||||
Logger.error("External readability failed, got HTTP #{status}")
|
||||
|
||||
if Frenzy.sentry_enabled?() do
|
||||
Sentry.capture_message("External readability failed, got HTTP #{status}")
|
||||
end
|
||||
|
||||
{:reply, Readability.article(html), state}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("External readability failed: #{inspect(reason)}")
|
||||
|
||||
if Frenzy.sentry_enabled?() do
|
||||
Sentry.capture_message("External readability failed: #{inspect(reason)}")
|
||||
end
|
||||
|
||||
{:reply, Readability.article(html), state}
|
||||
end
|
||||
else
|
||||
{:reply, Readability.article(html), state}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -11,22 +11,21 @@ defmodule Frenzy.Feed do
|
|||
title: feed.title,
|
||||
url: feed.feed_url,
|
||||
site_url: feed.site_url,
|
||||
last_updated_on_time:
|
||||
if(is_nil(feed.last_updated), do: 0, else: Timex.to_unix(feed.last_updated)),
|
||||
last_updated_on_time: Timex.to_unix(feed.last_updated),
|
||||
is_spark: false
|
||||
}
|
||||
end
|
||||
|
||||
def to_fervor(feed) do
|
||||
%{
|
||||
id: feed.id |> Integer.to_string(),
|
||||
id: feed.id,
|
||||
title: feed.title,
|
||||
url: feed.site_url,
|
||||
feed_url: feed.feed_url,
|
||||
service_url:
|
||||
Application.get_env(:frenzy, :base_url) <> Routes.feed_path(Endpoint, :show, feed.id),
|
||||
last_updated: DateTime.to_iso8601(feed.last_updated),
|
||||
group_ids: [feed.group_id |> Integer.to_string()]
|
||||
group_ids: [feed.group_id]
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -20,9 +20,9 @@ defmodule Frenzy.Group do
|
|||
|
||||
def to_fervor(group) do
|
||||
%{
|
||||
id: group.id |> Integer.to_string(),
|
||||
id: group.id,
|
||||
title: group.title,
|
||||
feed_ids: group.feeds |> Enum.map(&Integer.to_string(&1.id)),
|
||||
feed_ids: group.feeds |> Enum.map(fn f -> f.id end),
|
||||
service_url:
|
||||
Application.get_env(:frenzy, :base_url) <> Routes.group_path(Endpoint, :show, group.id)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
defmodule Frenzy.HTTP do
|
||||
require Logger
|
||||
@redirect_codes [301, 302]
|
||||
|
||||
def get(url, opts \\ []) do
|
||||
case HTTPoison.get(url, opts) do
|
||||
{:ok, %HTTPoison.Response{status_code: 200} = response} ->
|
||||
{:ok, response}
|
||||
|
||||
{:ok, %HTTPoison.Response{status_code: status_code, headers: headers}}
|
||||
when status_code in @redirect_codes ->
|
||||
headers
|
||||
|> Enum.find(fn {name, _value} -> name == "Location" end)
|
||||
|> case do
|
||||
{"Location", new_url} ->
|
||||
new_url =
|
||||
case URI.parse(new_url) do
|
||||
%URI{host: nil, path: path} ->
|
||||
# relative path
|
||||
%URI{URI.parse(url) | path: path} |> URI.to_string()
|
||||
|
||||
uri ->
|
||||
uri
|
||||
end
|
||||
|
||||
Logger.debug("Got 301 redirect from #{url} to #{new_url}")
|
||||
get(new_url, opts)
|
||||
|
||||
_ ->
|
||||
{:error, "Missing Location header for redirect"}
|
||||
end
|
||||
|
||||
{:ok, %HTTPoison.Response{status_code: 403}} ->
|
||||
{:error, "403 Forbidden"}
|
||||
|
||||
{:ok, %HTTPoison.Response{status_code: 404}} ->
|
||||
{:error, "404 Not Found"}
|
||||
|
||||
{:ok, %HTTPoison.Response{status_code: status_code}} ->
|
||||
{:error, "HTTP #{status_code}"}
|
||||
|
||||
{:error, %HTTPoison.Error{reason: reason}} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +1,6 @@
|
|||
defmodule Frenzy.Item do
|
||||
use Ecto.Schema
|
||||
import Ecto.Changeset
|
||||
import Ecto.Query
|
||||
alias FrenzyWeb.Router.Helpers, as: Routes
|
||||
alias FrenzyWeb.Endpoint
|
||||
|
||||
|
@ -26,8 +25,8 @@ defmodule Frenzy.Item do
|
|||
|
||||
def to_fervor(item) do
|
||||
res = %{
|
||||
id: item.id |> Integer.to_string(),
|
||||
feed_id: item.feed_id |> Integer.to_string(),
|
||||
id: item.id,
|
||||
feed_id: item.feed_id,
|
||||
title: item.title,
|
||||
author: item.creator,
|
||||
content: item.content,
|
||||
|
@ -38,15 +37,14 @@ defmodule Frenzy.Item do
|
|||
}
|
||||
|
||||
if item.date do
|
||||
Map.put(res, :published, DateTime.to_iso8601(item.date))
|
||||
Map.put(res, :created_at, DateTime.to_iso8601(item.date))
|
||||
else
|
||||
res
|
||||
end
|
||||
end
|
||||
|
||||
schema "items" do
|
||||
field :content, :string, default: ""
|
||||
field :content_type, :string
|
||||
field :content, :string
|
||||
field :date, :utc_datetime
|
||||
field :creator, :string
|
||||
field :guid, :string
|
||||
|
@ -65,7 +63,6 @@ defmodule Frenzy.Item do
|
|||
__meta__: Ecto.Schema.Metadata.t(),
|
||||
id: integer() | nil,
|
||||
content: String.t(),
|
||||
content_type: String.t() | nil,
|
||||
date: DateTime.t(),
|
||||
creator: String.t(),
|
||||
guid: String.t(),
|
||||
|
@ -82,23 +79,7 @@ defmodule Frenzy.Item do
|
|||
@doc false
|
||||
def changeset(item, attrs) do
|
||||
item
|
||||
|> cast(attrs, [
|
||||
:guid,
|
||||
:title,
|
||||
:url,
|
||||
:creator,
|
||||
:date,
|
||||
:content,
|
||||
:read,
|
||||
:read_date,
|
||||
:tombstone,
|
||||
:feed_id
|
||||
])
|
||||
|> validate_required([:guid, :url, :date, :feed_id])
|
||||
|> unique_constraint([:feed_id, :guid], name: :items_feed_guid_index)
|
||||
end
|
||||
|
||||
def exists?(feed_id, guid) do
|
||||
Frenzy.Repo.exists?(from i in __MODULE__, where: i.feed_id == ^feed_id and i.guid == ^guid)
|
||||
|> cast(attrs, [:guid, :title, :url, :creator, :date, :content, :read, :read_date, :tombstone])
|
||||
|> validate_required([:guid, :url, :date, :content, :feed])
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,101 +0,0 @@
|
|||
defmodule Frenzy.Network do
|
||||
require Logger
|
||||
|
||||
defmodule HTTP do
|
||||
use Tesla
|
||||
|
||||
adapter(Tesla.Adapter.Mint)
|
||||
|
||||
plug Tesla.Middleware.Logger, log_level: &log_level/1
|
||||
plug Tesla.Middleware.FollowRedirects
|
||||
plug Tesla.Middleware.Compression
|
||||
|
||||
# can't use JSON middleware currently, because feed_parser expects to parse the raw body data itself
|
||||
# plug Tesla.Middleware.JSON
|
||||
plug Tesla.Middleware.Timeout, timeout: 10_000
|
||||
|
||||
def log_level(env) do
|
||||
case env.status do
|
||||
code when code >= 400 -> :warn
|
||||
_ -> :debug
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@spec http_get(String.t()) :: Tesla.Env.result()
|
||||
def http_get(url) do
|
||||
HTTP.get(url)
|
||||
end
|
||||
|
||||
@spec http_post(String.t(), Tesla.Env.body(), [Tesla.option()]) :: Tesla.Env.result()
|
||||
def http_post(url, body, options \\ []) do
|
||||
HTTP.post(url, body, options)
|
||||
end
|
||||
|
||||
# @http_redirect_codes [301, 302]
|
||||
|
||||
# @spec http_get(String.t()) :: {:ok, HTTPoison.Response.t()} | {:error, term()}
|
||||
# def http_get(url) do
|
||||
# case HTTPoison.get(url) do
|
||||
# {:ok, %HTTPoison.Response{status_code: 200} = response} ->
|
||||
# {:ok, response}
|
||||
|
||||
# {:ok, %HTTPoison.Response{status_code: status_code, headers: headers}}
|
||||
# when status_code in @http_redirect_codes ->
|
||||
# headers
|
||||
# |> Enum.find(fn {name, _value} -> String.downcase(name) == "location" end)
|
||||
# |> case do
|
||||
# {_, new_url} ->
|
||||
# new_url =
|
||||
# case URI.parse(new_url) do
|
||||
# %URI{host: nil, path: path} ->
|
||||
# # relative path
|
||||
# %URI{URI.parse(url) | path: path} |> URI.to_string()
|
||||
|
||||
# uri ->
|
||||
# uri
|
||||
# end
|
||||
|
||||
# Logger.debug("Got 301 redirect from #{url} to #{new_url}")
|
||||
# http_get(new_url)
|
||||
|
||||
# _ ->
|
||||
# {:error, "Missing Location header for redirect"}
|
||||
# end
|
||||
|
||||
# {:ok, %HTTPoison.Response{status_code: 403}} ->
|
||||
# {:error, "403 Forbidden"}
|
||||
|
||||
# {:ok, %HTTPoison.Response{status_code: 404}} ->
|
||||
# {:error, "404 Not Found"}
|
||||
|
||||
# {:ok, %HTTPoison.Response{status_code: status_code}} ->
|
||||
# {:error, "HTTP #{status_code}"}
|
||||
|
||||
# {:error, error} ->
|
||||
# {:error, error}
|
||||
# end
|
||||
# end
|
||||
|
||||
@gemini_success_codes 20..29
|
||||
@gemini_redirect_codes 30..39
|
||||
|
||||
@spec gemini_request(String.t() | URI.t()) :: {:ok, Gemini.Response.t()} | {:error, term()}
|
||||
|
||||
def gemini_request(uri) do
|
||||
case Gemini.request(uri) do
|
||||
{:ok, %Gemini.Response{status: code} = response} when code in @gemini_success_codes ->
|
||||
{:ok, response}
|
||||
|
||||
{:ok, %Gemini.Response{status: code, meta: new_url}}
|
||||
when code in @gemini_redirect_codes ->
|
||||
gemini_request(URI.merge(uri, new_url))
|
||||
|
||||
{:ok, %Gemini.Response{status: code}} ->
|
||||
{:error, "Unhandled Gemini status code: #{code}"}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -23,7 +23,7 @@ defmodule Frenzy.OPML.Importer do
|
|||
outline_elements
|
||||
|> Enum.flat_map(&get_feeds/1)
|
||||
|> Enum.reduce(%{}, fn {group, feed_url}, acc ->
|
||||
Map.update(acc, group, [feed_url], fn feeds -> [feed_url | feeds] end)
|
||||
Map.update(acc, group, [], fn feeds -> [feed_url | feeds] end)
|
||||
end)
|
||||
end
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ defmodule Frenzy.Pipeline.ConditionalStage do
|
|||
|
||||
@impl Stage
|
||||
def apply(opts, item_params) do
|
||||
Logger.warning("Received invalid conditional opts: #{inspect(opts)}")
|
||||
Logger.warn("Received invalid conditional opts: #{inspect(opts)}")
|
||||
{:ok, item_params}
|
||||
end
|
||||
|
||||
|
@ -30,7 +30,7 @@ defmodule Frenzy.Pipeline.ConditionalStage do
|
|||
end
|
||||
|
||||
defp test_condition(condition, _item_params) do
|
||||
Logger.warning("Received invalid condition: #{inspect(condition)}")
|
||||
Logger.warn("Received invalid condition: #{inspect(condition)}")
|
||||
false
|
||||
end
|
||||
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
defmodule Frenzy.Pipeline.Extractor.ArsTechnica do
|
||||
@moduledoc """
|
||||
Extractor for https://arstechnica.com
|
||||
Handles multi-page articles
|
||||
"""
|
||||
|
||||
require Logger
|
||||
alias Frenzy.Network
|
||||
alias Frenzy.Pipeline.Extractor
|
||||
@behaviour Extractor
|
||||
|
||||
@impl Extractor
|
||||
def extract(html_tree) do
|
||||
case get_pages_from_tree(html_tree) do
|
||||
{:error, _} = err -> err
|
||||
content -> {:ok, content}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_pages_from_tree(tree) do
|
||||
with [article | _] <- Floki.find(tree, ~s([itemtype="http://schema.org/NewsArticle"])),
|
||||
[content | _] <- Floki.find(article, ~s([itemprop=articleBody])) do
|
||||
content = clean_content(content)
|
||||
|
||||
next_page_url =
|
||||
with [next | _] <- Floki.find(article, ".page-numbers a:last-of-type"),
|
||||
"Next" <> _ <- Floki.text(next),
|
||||
[href] <- Floki.attribute(next, "href") do
|
||||
href
|
||||
else
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
if next_page_url != nil do
|
||||
with body when not is_nil(body) <- fetch_page(next_page_url),
|
||||
{:ok, doc} <- Floki.parse_document(body),
|
||||
next_pages when is_list(next_pages) <- get_pages_from_tree(doc) do
|
||||
[content] ++ next_pages
|
||||
else
|
||||
_ ->
|
||||
[
|
||||
content,
|
||||
{"p", [], [{"em", [], ["Article truncated, unable to scrape subsequent pages"]}]}
|
||||
]
|
||||
end
|
||||
else
|
||||
[content]
|
||||
end
|
||||
else
|
||||
_ -> {:error, "no matching elements"}
|
||||
end
|
||||
end
|
||||
|
||||
defp clean_content(tree) do
|
||||
Floki.filter_out(tree, ".social-left, .story-sidebar, .ad_wrapper, figcaption .enlarge-link")
|
||||
end
|
||||
|
||||
defp fetch_page(url) do
|
||||
Logger.debug("Getting Ars Technica page from #{url}")
|
||||
|
||||
case Network.http_get(url) do
|
||||
{:ok, %Tesla.Env{status: code, body: body}} when code in 200..299 ->
|
||||
body
|
||||
|
||||
{:ok, %Tesla.Env{status: code}} ->
|
||||
Logger.warning("Unexpected HTTP code #{code} getting Ars Technica page #{url}")
|
||||
nil
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Couldn't get Ars Technica page #{url}: #{inspect(reason)}")
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,19 +0,0 @@
|
|||
defmodule Frenzy.Pipeline.Extractor.Birchtree do
|
||||
@moduledoc """
|
||||
Extractor for https://birchtree.me
|
||||
"""
|
||||
|
||||
alias Frenzy.Pipeline.Extractor
|
||||
@behaviour Extractor
|
||||
|
||||
@impl Extractor
|
||||
def extract(html_tree) do
|
||||
case Floki.find(html_tree, "section.post-content") do
|
||||
[content_elem | _] ->
|
||||
{:ok, content_elem}
|
||||
|
||||
_ ->
|
||||
{:error, "no matching elements"}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -30,8 +30,8 @@ defmodule Frenzy.Pipeline.Extractor.DaringFireball do
|
|||
|
||||
defp get_link_element(html_tree) do
|
||||
case Floki.find(html_tree, "dl.linkedlist dd") do
|
||||
[{_, _, dd_children} | _] ->
|
||||
dd_children
|
||||
[dd_elem | _] ->
|
||||
dd_elem
|
||||
|
||||
_ ->
|
||||
nil
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
defmodule Frenzy.Pipeline.Extractor.ElectionLawBlog do
|
||||
@moduledoc """
|
||||
Extractor for https://electionlawblog.org
|
||||
"""
|
||||
|
||||
alias Frenzy.Pipeline.Extractor
|
||||
@behaviour Extractor
|
||||
|
||||
@impl Extractor
|
||||
def extract(html_tree) do
|
||||
case Floki.find(html_tree, "div.entry-content") do
|
||||
[content_elem | _] ->
|
||||
filtered = Floki.filter_out(content_elem, ".addtoany_share_save_container")
|
||||
{:ok, filtered}
|
||||
|
||||
_ ->
|
||||
{:error, "no matching elements"}
|
||||
end
|
||||
end
|
||||
end
|
|
@ -15,7 +15,7 @@ defmodule Frenzy.Pipeline.Extractor.MacStories do
|
|||
# some images have full size links, strip those out
|
||||
|> Floki.filter_out("a.view-full-size")
|
||||
# rewrite non-standard images captions to <figure>/<figcaption>
|
||||
|> Floki.find_and_update("div, p", &rewrite_element/1)
|
||||
|> Floki.map(&rewrite_element/1)
|
||||
|
||||
{:ok, content_elem}
|
||||
|
||||
|
|
|
@ -19,6 +19,5 @@ defmodule Frenzy.Pipeline.Extractor.OmMalik do
|
|||
_ ->
|
||||
{:error, "no matching elements"}
|
||||
end
|
||||
|> Extractor.Util.strip_wp_lazy_loading()
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
defmodule Frenzy.Pipeline.Extractor.Slate do
|
||||
@moduledoc """
|
||||
Extractor for https://slate.com
|
||||
"""
|
||||
|
||||
alias Frenzy.Pipeline.Extractor
|
||||
@behaviour Extractor
|
||||
|
||||
@impl Extractor
|
||||
def extract(html_tree) do
|
||||
case get_article_content(html_tree) do
|
||||
nil ->
|
||||
{:error, "no matching elements"}
|
||||
|
||||
elem ->
|
||||
{:ok, elem}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_article_content(html_tree) do
|
||||
case Floki.find(html_tree, ".article__content") do
|
||||
[el] ->
|
||||
article_content =
|
||||
Floki.filter_out(
|
||||
el,
|
||||
".slate-ad, .in-article-recirc, .social-share, .newsletter-signup, .recirc-line, .product"
|
||||
)
|
||||
|
||||
image = Floki.find(html_tree, ".article__top-image img")
|
||||
|
||||
case image do
|
||||
[] ->
|
||||
article_content
|
||||
|
||||
[image | _] ->
|
||||
[image, article_content]
|
||||
end
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,28 +0,0 @@
|
|||
defmodule Frenzy.Pipeline.Extractor.Util do
|
||||
@doc """
|
||||
WordPress Jetpack uses a 1x1 pixel transparent gif in a srcset to keep browsers from loading images
|
||||
by overriding the src attribute. We want to strip those so the images actually load.
|
||||
"""
|
||||
@spec strip_wp_lazy_loading(Floki.html_tree()) :: Floki.html_tree()
|
||||
def strip_wp_lazy_loading(tree) do
|
||||
Floki.find_and_update(tree, "img.jetpack-lazy-image", fn
|
||||
{"img", attrs} = el ->
|
||||
class = Enum.find(attrs, fn {k, _} -> k == "class" end)
|
||||
|
||||
if !is_nil(class) && String.contains?(elem(class, 1), "jetpack-lazy-image") do
|
||||
{
|
||||
"img",
|
||||
Enum.filter(attrs, fn
|
||||
{"srcset", _} -> false
|
||||
_ -> true
|
||||
end)
|
||||
}
|
||||
else
|
||||
el
|
||||
end
|
||||
|
||||
el ->
|
||||
el
|
||||
end)
|
||||
end
|
||||
end
|
|
@ -1,112 +0,0 @@
|
|||
defmodule Frenzy.Pipeline.Extractor.TheVerge do
|
||||
@moduledoc """
|
||||
Extractor for https://theverge.com
|
||||
Handles their bizarro new layout that's a pile of unsemantic classes
|
||||
"""
|
||||
|
||||
require Logger
|
||||
alias Frenzy.Pipeline.Extractor
|
||||
@behaviour Extractor
|
||||
|
||||
@impl Extractor
|
||||
def extract(html_tree) do
|
||||
image = extract_header_image(html_tree)
|
||||
|
||||
content =
|
||||
html_tree
|
||||
|> Floki.find("main article > div:not(.duet--article--lede)")
|
||||
|> Floki.filter_out(
|
||||
".hidden, .duet--layout--rail, .duet--article--article-pullquote, .duet--article--comments-join-the-conversation, .duet--recirculation--related-list, .duet--article--comments-button, .duet--article--share-buttons"
|
||||
)
|
||||
|> Readability.Helper.remove_attrs("style")
|
||||
|> Floki.traverse_and_update(&rewrite/1)
|
||||
|
||||
{:ok, image ++ content}
|
||||
end
|
||||
|
||||
@spec extract_header_image(Floki.html_tree()) :: Floki.html_tree()
|
||||
defp extract_header_image(html_tree) do
|
||||
case Floki.find(html_tree, "article#content > .duet--article--lede figure") do
|
||||
[figure | _] ->
|
||||
img =
|
||||
case Floki.find(figure, "img") do
|
||||
[img | _] ->
|
||||
[img]
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
caption =
|
||||
case Floki.find(figure, ".duet--media--caption") do
|
||||
[{_tag, _attrs, children} | _] ->
|
||||
[{"figcaption", [], [Floki.text(children)]}]
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
|
||||
if img do
|
||||
[{"figure", [], img ++ caption}]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
_ ->
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
defp rewrite({_tag, _attrs, children} = el) do
|
||||
cond do
|
||||
is_empty_gif(el) ->
|
||||
nil
|
||||
|
||||
is_gallery(el) ->
|
||||
images =
|
||||
Floki.find(children, ":not(noscript) > img")
|
||||
|> Enum.map(fn el ->
|
||||
[src] = Floki.attribute(el, "src")
|
||||
src
|
||||
end)
|
||||
|
||||
{
|
||||
"div",
|
||||
[
|
||||
{"style",
|
||||
"display: flex; flex-direction: row; overflow-x: auto; scroll-snap-type: x mandatory;;"}
|
||||
],
|
||||
Enum.map(images, fn src ->
|
||||
{
|
||||
"img",
|
||||
[
|
||||
{"src", src},
|
||||
{"loading", "lazy"},
|
||||
{"style", "max-width: 85%; max-height: 30vh; scroll-snap-align: start;"}
|
||||
],
|
||||
[]
|
||||
}
|
||||
end)
|
||||
}
|
||||
|
||||
true ->
|
||||
el
|
||||
end
|
||||
end
|
||||
|
||||
defp rewrite(other), do: other
|
||||
|
||||
defp is_gallery(el) do
|
||||
case Floki.attribute(el, "class") do
|
||||
[classes] -> String.contains?(classes, "duet--article--gallery")
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
|
||||
defp is_empty_gif(el) do
|
||||
case Floki.attribute(el, "src") do
|
||||
[src] -> String.starts_with?(src, "data:image/gif;")
|
||||
_ -> false
|
||||
end
|
||||
end
|
||||
end
|
|
@ -18,36 +18,10 @@ defmodule Frenzy.Pipeline.Extractor.WhateverScalzi do
|
|||
end
|
||||
|
||||
defp get_article_content(html_tree) do
|
||||
# there's no element that contains only the post content
|
||||
# .postarea contains the headline, post content, social media buttons, and comments
|
||||
case Floki.find(html_tree, ".postarea") do
|
||||
[{_tag, _attrs, postarea_children}] ->
|
||||
Enum.split_while(postarea_children, fn
|
||||
{"h1", _, _} -> true
|
||||
_ -> false
|
||||
end)
|
||||
|> case do
|
||||
{_before_headline, [_headline | rest]} ->
|
||||
{article_content, _rest} =
|
||||
Enum.split_while(rest, fn
|
||||
{"div", attrs, _} = el ->
|
||||
class = Floki.attribute(el, "class") |> List.first()
|
||||
|
||||
if {"id", "comments"} in attrs do
|
||||
false
|
||||
else
|
||||
is_nil(class) || !String.contains?(class, "sharedaddy")
|
||||
end
|
||||
|
||||
_ ->
|
||||
true
|
||||
end)
|
||||
|
||||
Extractor.Util.strip_wp_lazy_loading(article_content)
|
||||
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
case Floki.find(html_tree, "article.post > div.entry-content") do
|
||||
[content_elem | _] ->
|
||||
# remove social media buttons that are included in the .entry-content element
|
||||
Floki.filter_out(content_elem, "div#jp-post-flair")
|
||||
|
||||
_ ->
|
||||
nil
|
||||
|
|
|
@ -45,24 +45,18 @@ defmodule Frenzy.Pipeline.FilterEngine do
|
|||
|
||||
def validate_rules(_rules), do: {:error, "rules must be a list"}
|
||||
|
||||
@rule_modes ~W[contains_string contains_string_case_sensitive matches_regex]
|
||||
@rule_properties ~W[url title author content]
|
||||
|
||||
def validate_rule(rule) do
|
||||
cond do
|
||||
not is_map(rule) ->
|
||||
{:error, "rule must be a map"}
|
||||
|
||||
not (Map.has_key?(rule, "mode") and is_binary(rule["mode"]) and
|
||||
rule["mode"] in @rule_modes) ->
|
||||
rule_modes_text = Enum.map_join(@rule_modes, ", ", &"'#{&1}'")
|
||||
{:error, "mode property must be a string, one of #{rule_modes_text}"}
|
||||
rule["mode"] in ["contains_string", "matches_regex"]) ->
|
||||
{:error, "mode property must be a string, either 'contains_string' or 'matches_regex'"}
|
||||
|
||||
not (Map.has_key?(rule, "property") and is_binary(rule["property"]) and
|
||||
rule["property"] in @rule_properties) ->
|
||||
rule_props_text = Enum.map_join(@rule_properties, ", ", &"'#{&1}'")
|
||||
|
||||
{:error, "property property must be a string, one of #{rule_props_text}"}
|
||||
rule["property"] in ["url", "title", "author"]) ->
|
||||
{:error, "property property must be a string, either 'url', 'title', or 'author'"}
|
||||
|
||||
not (Map.has_key?(rule, "param") and is_binary(rule["param"])) ->
|
||||
{:error, "param property must be a string"}
|
||||
|
@ -111,31 +105,16 @@ defmodule Frenzy.Pipeline.FilterEngine do
|
|||
end
|
||||
|
||||
defp matches(value, "contains_string", param) do
|
||||
String.contains?(String.downcase(value), String.downcase(param))
|
||||
end
|
||||
|
||||
defp matches(value, "contains_string_case_sensitive", param) do
|
||||
String.contains?(value, param)
|
||||
end
|
||||
|
||||
defp matches(value, "matches_regex", param) do
|
||||
{:ok, regex} = Regex.compile(param, "i")
|
||||
{:ok, regex} = Regex.compile(param)
|
||||
String.match?(value, regex)
|
||||
end
|
||||
|
||||
defp get_property(item_params, "url"), do: item_params.url
|
||||
defp get_property(item_params, "title"), do: item_params.title
|
||||
defp get_property(item_params, "author"), do: item_params.author
|
||||
|
||||
defp get_property(%{content: content, content_type: type}, "content")
|
||||
when type in ["text/plain", "text/gemini"],
|
||||
do: content
|
||||
|
||||
defp get_property(%{content: content, content_type: "text/html"}, "content") do
|
||||
content
|
||||
|> Floki.parse_fragment()
|
||||
|> Floki.text()
|
||||
end
|
||||
|
||||
defp get_property(_item_params, _property), do: {:error, "invalid property"}
|
||||
end
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
defmodule Frenzy.Pipeline.GeminiScrapeStage do
|
||||
require Logger
|
||||
alias Frenzy.Network
|
||||
alias Frenzy.Pipeline.Stage
|
||||
@behaviour Stage
|
||||
|
||||
@impl Stage
|
||||
def apply(opts, %{url: url} = item_params) do
|
||||
case get_content(url, opts) do
|
||||
{:error, reason} ->
|
||||
Logger.warning("Unable to get Gemini content for #{url}: #{reason}")
|
||||
{:ok, item_params}
|
||||
|
||||
{content, content_type} ->
|
||||
{:ok, %{item_params | content: content, content_type: content_type}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Stage
|
||||
def validate_opts(opts) do
|
||||
{:ok, opts}
|
||||
end
|
||||
|
||||
@impl Stage
|
||||
def default_opts(), do: %{}
|
||||
|
||||
@spec get_content(String.t(), map()) :: {String.t(), String.t()} | {:error, term()}
|
||||
def get_content(url, _opts) do
|
||||
case Network.gemini_request(url) do
|
||||
{:error, reason} ->
|
||||
{:error, reason}
|
||||
|
||||
{:ok, %Gemini.Response{body: body, meta: meta}} ->
|
||||
{body, parse_content_type(meta)}
|
||||
end
|
||||
end
|
||||
|
||||
defp parse_content_type(meta) do
|
||||
meta
|
||||
|> String.split(";")
|
||||
|> hd()
|
||||
|> String.trim()
|
||||
end
|
||||
end
|
|
@ -1,105 +0,0 @@
|
|||
defmodule Frenzy.Pipeline.RenderGeminiStage do
|
||||
require Logger
|
||||
alias Frenzy.Pipeline.Stage
|
||||
@behaviour Stage
|
||||
|
||||
@impl Stage
|
||||
def apply(_opts, %{content: content, content_type: "text/gemini"} = item_params) do
|
||||
html = render_gemini(content)
|
||||
{:ok, %{item_params | content_type: "text/html", content: html}}
|
||||
end
|
||||
|
||||
def apply(_opts, %{content_type: content_type} = item_params) do
|
||||
Logger.debug("Not rendering Gemini text for item, incorect content type: #{content_type}")
|
||||
{:ok, item_params}
|
||||
end
|
||||
|
||||
@impl Stage
|
||||
def validate_opts(opts) do
|
||||
{:ok, opts}
|
||||
end
|
||||
|
||||
@impl Stage
|
||||
def default_opts(), do: %{}
|
||||
|
||||
def render_gemini(gemini_source) do
|
||||
gemini_source
|
||||
|> Gemini.parse()
|
||||
|> render_lines()
|
||||
|> Floki.raw_html()
|
||||
end
|
||||
|
||||
@spec render_lines([Gemini.line()], [String.t()]) :: [String.t()]
|
||||
|
||||
defp render_lines(lines, acc \\ [])
|
||||
|
||||
defp render_lines([], acc) do
|
||||
Enum.reverse(acc)
|
||||
end
|
||||
|
||||
defp render_lines([{:text, text} | rest], acc) do
|
||||
render_lines(rest, [{"p", [], [text]} | acc])
|
||||
end
|
||||
|
||||
defp render_lines([{:link, uri, text} | rest], acc) do
|
||||
uri_str = URI.to_string(uri)
|
||||
text = if is_nil(text), do: uri_str, else: text
|
||||
a = {"a", [{"href", uri_str}], [text]}
|
||||
p = {"p", [], [a]}
|
||||
render_lines(rest, [p | acc])
|
||||
end
|
||||
|
||||
defp render_lines([{:preformatted_start, _alt} | rest], acc) do
|
||||
{preformatted_lines, [:preformatted_end | rest]} =
|
||||
Enum.split_while(rest, fn
|
||||
{:preformatted, _} -> true
|
||||
_ -> false
|
||||
end)
|
||||
|
||||
pre_text =
|
||||
preformatted_lines
|
||||
|> Enum.map(fn {:preformatted, text} -> text end)
|
||||
|> Enum.join("\n")
|
||||
|
||||
pre = {"pre", [], pre_text}
|
||||
render_lines(rest, [pre | acc])
|
||||
end
|
||||
|
||||
defp render_lines([{:heading, text, level} | rest], acc) do
|
||||
tag = "h#{level}"
|
||||
heading = {tag, [], [text]}
|
||||
render_lines(rest, [heading | acc])
|
||||
end
|
||||
|
||||
defp render_lines([{:list_item, _text} | _rest] = lines, acc) do
|
||||
{list_items, rest} =
|
||||
Enum.split_while(lines, fn
|
||||
{:list_item, _} -> true
|
||||
_ -> false
|
||||
end)
|
||||
|
||||
lis =
|
||||
Enum.map(list_items, fn {:list_item, text} ->
|
||||
{"li", [], [text]}
|
||||
end)
|
||||
|
||||
ul = {"ul", [], lis}
|
||||
render_lines(rest, [ul | acc])
|
||||
end
|
||||
|
||||
defp render_lines([{:quoted, _text} | _rest] = lines, acc) do
|
||||
{quoted_lines, rest} =
|
||||
Enum.split_while(lines, fn
|
||||
{:quoted, _} -> true
|
||||
_ -> false
|
||||
end)
|
||||
|
||||
ps =
|
||||
Enum.map(quoted_lines, fn {:quoted, text} ->
|
||||
{"p", [], [text]}
|
||||
end)
|
||||
|
||||
blockquote = {"blockquote", [], ps}
|
||||
render_lines(rest, [blockquote | acc])
|
||||
end
|
||||
end
|
|
@ -1,7 +1,6 @@
|
|||
defmodule Frenzy.Pipeline.ScrapeStage do
|
||||
require Logger
|
||||
alias Frenzy.Network
|
||||
alias Frenzy.BuiltinExtractor
|
||||
alias Frenzy.HTTP
|
||||
alias Frenzy.Pipeline.Stage
|
||||
@behaviour Stage
|
||||
|
||||
|
@ -12,7 +11,7 @@ defmodule Frenzy.Pipeline.ScrapeStage do
|
|||
{:ok, %{item_params | content: content}}
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.warning("Unable to get article content for #{url}: #{reason}")
|
||||
Logger.warn("Unable to get article content for #{url}: #{reason}")
|
||||
{:ok, item_params}
|
||||
end
|
||||
end
|
||||
|
@ -69,14 +68,11 @@ defmodule Frenzy.Pipeline.ScrapeStage do
|
|||
Logger.debug("Getting article from #{url}")
|
||||
|
||||
url
|
||||
|> Network.http_get()
|
||||
|> HTTP.get()
|
||||
|> case do
|
||||
{:ok, %Tesla.Env{status: code} = response} when code in 200..299 ->
|
||||
{:ok, response} ->
|
||||
handle_response(url, response, opts)
|
||||
|
||||
{:ok, %Tesla.Env{status: code}} ->
|
||||
{:error, "Unexpected HTTP code #{code}"}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, "Couldn't scrape article: #{reason}"}
|
||||
end
|
||||
|
@ -84,47 +80,16 @@ defmodule Frenzy.Pipeline.ScrapeStage do
|
|||
|
||||
defp get_article_content(_url, _opts), do: {:error, "URL must be a non-empty string"}
|
||||
|
||||
@spec handle_response(String.t(), Tesla.Env.t(), map()) ::
|
||||
@spec handle_response(String.t(), HTTPoison.Response.t(), map()) ::
|
||||
{:ok, String.t()} | {:error, String.t()}
|
||||
defp handle_response(url, %Tesla.Env{body: body}, opts) do
|
||||
defp handle_response(url, %HTTPoison.Response{body: body}, opts) do
|
||||
case opts["extractor"] do
|
||||
"builtin" ->
|
||||
{:ok, BuiltinExtractor.article(url, body)}
|
||||
{:ok, Readability.article(body)}
|
||||
|
||||
module_name ->
|
||||
{:ok, html_tree} = Floki.parse_document(body)
|
||||
|
||||
try do
|
||||
apply(String.to_existing_atom("Elixir." <> module_name), :extract, [html_tree])
|
||||
|> case do
|
||||
{:ok, content} ->
|
||||
# non-builtin extractors go through readable_html to cleanup any bad/untrusted html
|
||||
# this is what Floki.readable_html without turning back into a string
|
||||
content =
|
||||
Readability.Helper.remove_attrs(content, Readability.regexes(:protect_attrs))
|
||||
|
||||
{:ok, content}
|
||||
|
||||
err ->
|
||||
err
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
Logger.error(
|
||||
"Encountered error extracting article content from '#{url}' with #{module_name}, falling back to default"
|
||||
)
|
||||
|
||||
Logger.error(Exception.format(:error, e, __STACKTRACE__))
|
||||
|
||||
if Frenzy.sentry_enabled?() do
|
||||
Sentry.capture_exception(e,
|
||||
stacktrace: __STACKTRACE__,
|
||||
extra: %{extractor: module_name, item_url: url}
|
||||
)
|
||||
end
|
||||
|
||||
{:ok, BuiltinExtractor.article(url, body)}
|
||||
end
|
||||
html_tree = Floki.parse(body)
|
||||
apply(String.to_existing_atom("Elixir." <> module_name), :extract, [html_tree])
|
||||
end
|
||||
|> case do
|
||||
{:ok, html} ->
|
||||
|
@ -134,15 +99,15 @@ defmodule Frenzy.Pipeline.ScrapeStage do
|
|||
value -> value
|
||||
end
|
||||
|
||||
html =
|
||||
html
|
||||
|> Floki.filter_out("script")
|
||||
|> Floki.find_and_update(
|
||||
"img",
|
||||
rewrite_image_urls(convert_to_data_uris, URI.parse(url))
|
||||
)
|
||||
html = Floki.map(html, rewrite_image_urls(convert_to_data_uris, URI.parse(url)))
|
||||
|
||||
{:ok, Floki.raw_html(html)}
|
||||
case opts["extractor"] do
|
||||
"builtin" ->
|
||||
{:ok, Readability.readable_html(html)}
|
||||
|
||||
_ ->
|
||||
{:ok, Floki.raw_html(html)}
|
||||
end
|
||||
|
||||
res ->
|
||||
res
|
||||
|
@ -164,16 +129,6 @@ defmodule Frenzy.Pipeline.ScrapeStage do
|
|||
attr
|
||||
end)
|
||||
|
||||
has_src = Enum.find(new_attrs, fn {name, _} -> name == "src" end)
|
||||
|
||||
# remove srcsets because our transformation only applies to the src attribute, so that should always be used
|
||||
new_attrs =
|
||||
if has_src do
|
||||
Enum.reject(new_attrs, fn {name, _} -> name == "srcset" end)
|
||||
else
|
||||
new_attrs
|
||||
end
|
||||
|
||||
{"img", new_attrs}
|
||||
|
||||
elem ->
|
||||
|
@ -184,22 +139,18 @@ defmodule Frenzy.Pipeline.ScrapeStage do
|
|||
@content_type_allowlist ["image/jpeg", "image/png", "image/heic", "image/heif", "image/tiff"]
|
||||
|
||||
# convert images to data URIs so that they're stored by clients as part of the body
|
||||
defp image_to_data_uri("data:" <> _ = src, _site_uri, _convert) do
|
||||
src
|
||||
end
|
||||
|
||||
defp image_to_data_uri(src, site_uri, true) do
|
||||
absolute_url = URI.merge(site_uri, src) |> to_string()
|
||||
|
||||
case Network.http_get(absolute_url) do
|
||||
{:ok, %Tesla.Env{body: body, headers: headers}} ->
|
||||
Enum.find(headers, fn {header, _value} -> String.downcase(header) == "content-type" end)
|
||||
|> case do
|
||||
{_, content_type} when content_type in @content_type_allowlist ->
|
||||
"data:#{content_type};base64,#{Base.encode64(body)}"
|
||||
case HTTP.get(absolute_url) do
|
||||
{:ok, %HTTPoison.Response{body: body, headers: headers}} ->
|
||||
{"Content-Type", content_type} =
|
||||
Enum.find(headers, fn {header, _value} -> header == "Content-Type" end)
|
||||
|
||||
_ ->
|
||||
src
|
||||
if content_type in @content_type_allowlist do
|
||||
"data:#{content_type};base64,#{Base.encode64(body)}"
|
||||
else
|
||||
src
|
||||
end
|
||||
|
||||
_ ->
|
||||
|
@ -207,5 +158,5 @@ defmodule Frenzy.Pipeline.ScrapeStage do
|
|||
end
|
||||
end
|
||||
|
||||
defp image_to_data_uri(src, site_uri, false), do: to_string(URI.merge(site_uri, src))
|
||||
defp image_to_data_uri(src, _site_uri, false), do: src
|
||||
end
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
defmodule Frenzy.Task.CreateItem do
|
||||
require Logger
|
||||
use Task
|
||||
alias Frenzy.{Repo, Item}
|
||||
alias Frenzy.Repo
|
||||
|
||||
@spec start_link(Frenzy.Feed.t(), FeedParser.Item.t()) :: {:ok, pid()}
|
||||
def start_link(feed, entry) do
|
||||
Task.start_link(__MODULE__, :run, [feed, entry])
|
||||
end
|
||||
|
||||
@spec run(Frenzy.Feed.t(), FeedParser.Item.t()) :: :ok
|
||||
def run(feed, entry) do
|
||||
Logger.metadata(item_task_id: generate_task_id())
|
||||
|
||||
{guid, url} = real_guid_and_url(entry)
|
||||
url = get_real_url(entry)
|
||||
|
||||
Logger.debug("Creating item for #{url}")
|
||||
|
||||
|
@ -29,14 +27,12 @@ defmodule Frenzy.Task.CreateItem do
|
|||
end
|
||||
|
||||
item_params = %{
|
||||
guid: guid,
|
||||
guid: entry.guid,
|
||||
title: entry.title,
|
||||
url: url,
|
||||
date: date,
|
||||
creator: entry.creator,
|
||||
content: entry.content,
|
||||
# we assume text/html in the feed itself, other stages may alter this
|
||||
content_type: "text/html"
|
||||
creator: "",
|
||||
content: entry.content
|
||||
}
|
||||
|
||||
feed = Repo.preload(feed, :pipeline)
|
||||
|
@ -46,13 +42,6 @@ defmodule Frenzy.Task.CreateItem do
|
|||
feed.pipeline.stages
|
||||
|> Enum.reduce({:ok, item_params}, fn
|
||||
stage, {:ok, item_params} ->
|
||||
if Frenzy.sentry_enabled?() do
|
||||
Sentry.Context.add_breadcrumb(%{
|
||||
category: "pipeline",
|
||||
message: stage["module_name"]
|
||||
})
|
||||
end
|
||||
|
||||
apply(String.to_existing_atom("Elixir." <> stage["module_name"]), :apply, [
|
||||
stage["options"],
|
||||
item_params
|
||||
|
@ -68,73 +57,38 @@ defmodule Frenzy.Task.CreateItem do
|
|||
{:ok, item_params}
|
||||
end
|
||||
|
||||
changeset =
|
||||
case result do
|
||||
{:error, error} ->
|
||||
Logger.error(error)
|
||||
case result do
|
||||
{:err, error} ->
|
||||
Logger.error(error)
|
||||
|
||||
if Frenzy.sentry_enabled?() do
|
||||
Sentry.capture_message(
|
||||
"Error evaluating pipeline: #{inspect(error)}",
|
||||
extra: %{
|
||||
feed_id: feed.id,
|
||||
pipeline_id: feed.pipeline.id,
|
||||
pipeline_name: feed.pipeline.name
|
||||
}
|
||||
)
|
||||
end
|
||||
{:ok, item_params} ->
|
||||
changeset = Ecto.build_assoc(feed, :items, item_params)
|
||||
|
||||
:error
|
||||
|
||||
{:ok, item_params} ->
|
||||
item_params = Map.put(item_params, :feed_id, feed.id)
|
||||
Item.changeset(%Item{}, item_params)
|
||||
|
||||
:tombstone ->
|
||||
Item.changeset(%Item{}, %{
|
||||
guid: item_params.guid,
|
||||
tombstone: true
|
||||
})
|
||||
end
|
||||
|
||||
case changeset do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
changeset ->
|
||||
case Repo.insert(changeset) do
|
||||
{:ok, item} ->
|
||||
item
|
||||
|
||||
{:error, changeset} ->
|
||||
with [feed_id: {_, list}] <- changeset.errors,
|
||||
true <- {:constraint_name, "items_feed_guid_index"} in list do
|
||||
Logger.warning("Did not insert duplicate item for #{item_params.guid}")
|
||||
else
|
||||
_ ->
|
||||
Logger.error("Error inserting item #{item_params.guid}")
|
||||
Logger.error(changeset.errors)
|
||||
Logger.error("Error inserting item #{entry.guid}")
|
||||
Logger.error(changeset)
|
||||
end
|
||||
|
||||
if Frenzy.sentry_enabled?() do
|
||||
Sentry.capture_message("Error inserting item: #{inspect(changeset.errors)}",
|
||||
extra: %{
|
||||
item_guid: item_params.guid,
|
||||
feed_id: feed.id,
|
||||
errors: changeset.errors
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
:tombstone ->
|
||||
changeset =
|
||||
Ecto.build_assoc(feed, :items, %{
|
||||
guid: item_params.guid,
|
||||
tombstone: true
|
||||
})
|
||||
|
||||
case Repo.insert(changeset) do
|
||||
{:ok, item} ->
|
||||
item
|
||||
|
||||
{:error, changeset} ->
|
||||
Logger.error("Error inserting tombstone for #{entry.guid}")
|
||||
Logger.error(changeset)
|
||||
end
|
||||
end
|
||||
|
||||
:ok
|
||||
end
|
||||
|
||||
def real_guid_and_url(entry) do
|
||||
url = get_real_url(entry)
|
||||
# fallback to url if guid isn't present
|
||||
{entry.guid || url, url}
|
||||
end
|
||||
|
||||
defp get_real_url(entry) do
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
defmodule Frenzy.Task.FetchFavicon do
|
||||
require Logger
|
||||
use Task
|
||||
alias Frenzy.{Network, Repo, Feed}
|
||||
alias Frenzy.{HTTP, Repo, Feed}
|
||||
|
||||
def start_link(feed) do
|
||||
Task.start_link(__MODULE__, :run, [feed])
|
||||
|
@ -13,42 +13,42 @@ defmodule Frenzy.Task.FetchFavicon do
|
|||
site_url =
|
||||
case feed.site_url do
|
||||
url when is_binary(url) ->
|
||||
URI.parse(url)
|
||||
url
|
||||
|
||||
_ ->
|
||||
%URI{URI.parse(feed.feed_url) | path: nil, query: nil, fragment: nil}
|
||||
%URI{URI.parse(feed.feed_url) | path: nil, query: nil, fragment: nil} |> URI.to_string()
|
||||
end
|
||||
|
||||
if site_url.scheme in ["http", "https"] do
|
||||
Logger.debug("Fetching favicon for #{site_url}")
|
||||
Logger.debug("Fetching favicon for #{site_url}")
|
||||
|
||||
favicon_url =
|
||||
fetch_favicon_url_from_webpage(site_url) || URI.merge(site_url, "/favicon.ico")
|
||||
favicon_url = fetch_favicon_url_from_webpage(site_url) || URI.merge(site_url, "/favicon.ico")
|
||||
|
||||
with true <- is_binary(favicon_url),
|
||||
%Feed{favicon_url: old_url} when old_url != favicon_url <- feed,
|
||||
{:ok, favicon_data} <- fetch_favicon_data(favicon_url) do
|
||||
changeset =
|
||||
Feed.changeset(feed, %{
|
||||
favicon: favicon_data,
|
||||
favicon_url: to_string(favicon_url)
|
||||
})
|
||||
with %Feed{favicon_url: old_url} when old_url != favicon_url <- feed,
|
||||
{:ok, favicon_data} <- fetch_favicon_data(favicon_url) do
|
||||
changeset =
|
||||
Feed.changeset(feed, %{
|
||||
favicon: favicon_data,
|
||||
favicon_url: to_string(favicon_url)
|
||||
})
|
||||
|
||||
{:ok, _feed} = Repo.update(changeset)
|
||||
else
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
{:ok, _feed} = Repo.update(changeset)
|
||||
else
|
||||
_ ->
|
||||
:ok
|
||||
end
|
||||
end
|
||||
|
||||
@spec fetch_favicon_url_from_webpage(url :: String.t()) :: String.t()
|
||||
|
||||
defp fetch_favicon_url_from_webpage(url) when is_binary(url) do
|
||||
case Network.http_get(url) do
|
||||
{:ok, %Tesla.Env{body: body, status: code}} when code in 200..299 ->
|
||||
case HTTP.get(url) do
|
||||
{:ok, %HTTPoison.Response{body: body, status_code: code}} when code in 200..299 ->
|
||||
extract_favicon_url(url, body)
|
||||
|
||||
{:ok, %HTTPoison.Response{status_code: code}} ->
|
||||
Logger.debug("Unhandled HTTP code #{code} for '#{url}'")
|
||||
nil
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.debug("Error fetching webpage for favicon: #{inspect(reason)}")
|
||||
nil
|
||||
|
@ -59,7 +59,7 @@ defmodule Frenzy.Task.FetchFavicon do
|
|||
|
||||
@spec extract_favicon_url(page_url :: String.t(), body :: term()) :: String.t()
|
||||
defp extract_favicon_url(page_url, body) do
|
||||
{:ok, html_tree} = Floki.parse_document(body)
|
||||
html_tree = Floki.parse(body)
|
||||
|
||||
case Floki.find(html_tree, "link[rel=icon]") do
|
||||
[] ->
|
||||
|
@ -108,10 +108,14 @@ defmodule Frenzy.Task.FetchFavicon do
|
|||
defp fetch_favicon_data(favicon_url) do
|
||||
Logger.debug("Fetching favicon from: '#{favicon_url}'")
|
||||
|
||||
case Network.http_get(favicon_url) do
|
||||
{:ok, %Tesla.Env{body: body, status: code}} when code in 200..299 ->
|
||||
case HTTP.get(favicon_url) do
|
||||
{:ok, %HTTPoison.Response{body: body, status_code: code}} when code in 200..299 ->
|
||||
{:ok, "data:image/png;base64,#{Base.encode64(body)}"}
|
||||
|
||||
{:ok, %HTTPoison.Response{status_code: code}} ->
|
||||
Logger.debug("Unhandled HTTP code #{code} for '#{favicon_url}'")
|
||||
:error
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.debug("Error fetching favicon: #{inspect(reason)}")
|
||||
:error
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
defmodule Frenzy.UpdateFeeds do
|
||||
use GenServer
|
||||
alias Frenzy.{Network, Repo, Feed, Item}
|
||||
alias Frenzy.{HTTP, Repo, Feed, Item}
|
||||
alias Frenzy.Task.{CreateItem, FetchFavicon}
|
||||
import Ecto.Query
|
||||
require Logger
|
||||
|
@ -14,7 +14,8 @@ defmodule Frenzy.UpdateFeeds do
|
|||
end
|
||||
|
||||
def init(state) do
|
||||
Process.send_after(self(), :update_feeds, 5 * 1000)
|
||||
update_feeds()
|
||||
schedule_update()
|
||||
{:ok, state}
|
||||
end
|
||||
|
||||
|
@ -23,20 +24,12 @@ defmodule Frenzy.UpdateFeeds do
|
|||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info({:update_feed, feed_id, retry_count}, state) do
|
||||
update_feed(Repo.get(Feed, feed_id), retry_count)
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
def handle_info(:update_feeds, state) do
|
||||
update_feeds()
|
||||
schedule_update()
|
||||
{:noreply, state}
|
||||
end
|
||||
|
||||
# workaround for unhanled {:ssl_closed, {:sslsocket, {:gen_tcp, ...}}} message when Gemini module
|
||||
def handle_info({:ssl_closed, _}, state), do: {:noreply, state}
|
||||
|
||||
defp schedule_update() do
|
||||
# 30 minutes
|
||||
Process.send_after(self(), :update_feeds, 30 * 60 * 1000)
|
||||
|
@ -57,37 +50,20 @@ defmodule Frenzy.UpdateFeeds do
|
|||
|
||||
Logger.info("Updating #{count} feeds")
|
||||
|
||||
do_update_feeds(feeds)
|
||||
Enum.each(feeds, &update_feed/1)
|
||||
|
||||
prune_old_items()
|
||||
end
|
||||
|
||||
def force_update_feeds() do
|
||||
feeds = Repo.all(Feed)
|
||||
|
||||
Logger.info("Force updating #{Enum.count(feeds)} feeds")
|
||||
|
||||
do_update_feeds(feeds)
|
||||
end
|
||||
|
||||
defp do_update_feeds(feeds) do
|
||||
Enum.each(feeds, fn feed ->
|
||||
try do
|
||||
update_feed(feed)
|
||||
rescue
|
||||
error ->
|
||||
Logger.warning(
|
||||
Logger.warn(
|
||||
"Encountered error updating feed #{feed.id} #{feed.feed_url}: #{inspect(error)}"
|
||||
)
|
||||
|
||||
if Frenzy.sentry_enabled?() do
|
||||
Sentry.capture_exception(error,
|
||||
stacktrace: __STACKTRACE__,
|
||||
extra: %{feed_id: feed.id, feed_url: feed.feed_url}
|
||||
)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
prune_old_items()
|
||||
end
|
||||
|
||||
defp prune_old_items() do
|
||||
|
@ -107,32 +83,19 @@ defmodule Frenzy.UpdateFeeds do
|
|||
Logger.info("Converted #{count} read items to tombstones")
|
||||
end
|
||||
|
||||
defp update_feed(feed, retry_count \\ 0) do
|
||||
defp update_feed(feed) do
|
||||
Logger.debug("Updating #{feed.feed_url}")
|
||||
|
||||
case URI.parse(feed.feed_url) do
|
||||
%URI{scheme: "gemini"} = uri ->
|
||||
update_feed_gemini(feed, uri, retry_count)
|
||||
|
||||
%URI{scheme: scheme} when scheme in ["http", "https"] ->
|
||||
update_feed_http(feed, retry_count)
|
||||
|
||||
%URI{scheme: scheme} ->
|
||||
Logger.warning("Unhandled scheme for feed: #{scheme}")
|
||||
end
|
||||
end
|
||||
|
||||
defp update_feed_http(feed, retry_count) do
|
||||
case Network.http_get(feed.feed_url) do
|
||||
case HTTP.get(feed.feed_url) do
|
||||
{:ok,
|
||||
%Tesla.Env{
|
||||
status: 200,
|
||||
%HTTPoison.Response{
|
||||
status_code: 200,
|
||||
body: body,
|
||||
headers: headers
|
||||
}} ->
|
||||
{_, content_type} =
|
||||
headers
|
||||
|> Enum.find(fn {k, _v} -> String.downcase(k) == "content-type" end)
|
||||
|> Enum.find(fn {k, _v} -> k == "Content-Type" end)
|
||||
|
||||
content_type =
|
||||
content_type
|
||||
|
@ -140,77 +103,36 @@ defmodule Frenzy.UpdateFeeds do
|
|||
|> Enum.map(&String.trim/1)
|
||||
|> Enum.find(fn s -> !String.contains?(s, "=") end)
|
||||
|
||||
do_update_feed(feed, content_type, body)
|
||||
case FeedParser.parse(body, content_type) do
|
||||
{:ok, rss} ->
|
||||
update_feed_from_rss(feed, rss)
|
||||
|
||||
{:ok, %Tesla.Env{status: status}} ->
|
||||
Logger.error("Couldn't load feed #{feed.feed_url}: HTTP #{status}")
|
||||
|
||||
if Frenzy.sentry_enabled?() do
|
||||
Sentry.capture_message("Got HTTP #{status} when loading feed '#{feed.feed_url}'",
|
||||
extra: %{feed_id: feed.id}
|
||||
)
|
||||
{:error, reason} ->
|
||||
Logger.error("Unable to parse feed at '#{feed.feed_url}': #{inspect(reason)}")
|
||||
end
|
||||
|
||||
{:error, reason} ->
|
||||
if retry_count < 5 do
|
||||
Process.send_after(
|
||||
self(),
|
||||
{:update_feed, feed.id, retry_count + 1},
|
||||
trunc(:math.pow(4, retry_count)) * 1000
|
||||
)
|
||||
else
|
||||
Logger.error("Couldn't load feed #{feed.feed_url}: #{inspect(reason)}")
|
||||
{:ok, %HTTPoison.Response{status_code: 404}} ->
|
||||
Logger.warn("RSS feed #{feed.feed_url} not found")
|
||||
|
||||
if Frenzy.sentry_enabled?() do
|
||||
Sentry.capture_message("Error loading HTTP feed: #{inspect(reason)}",
|
||||
extra: %{feed_id: feed.id, feed_url: feed.feed_url}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
{:ok, %HTTPoison.Response{status_code: status_code, headers: headers}}
|
||||
when status_code in [301, 302] ->
|
||||
{"Location", new_url} =
|
||||
Enum.find(headers, fn {name, _value} ->
|
||||
name == "Location"
|
||||
end)
|
||||
|
||||
defp update_feed_gemini(feed, feed_uri, retry_count) do
|
||||
case Network.gemini_request(feed_uri) do
|
||||
{:ok, %Gemini.Response{meta: content_type, body: body}} ->
|
||||
do_update_feed(feed, content_type, body)
|
||||
Logger.debug("Got 301 redirect from #{feed.feed_url} to #{new_url}, updating feed URL")
|
||||
changeset = Feed.changeset(feed, %{feed_url: new_url})
|
||||
{:ok, feed} = Repo.update(changeset)
|
||||
update_feed(feed)
|
||||
|
||||
{:error, reason} ->
|
||||
if retry_count < 5 do
|
||||
Process.send_after(
|
||||
self(),
|
||||
{:update_feed, feed.id, retry_count + 1},
|
||||
trunc(:math.pow(4, retry_count)) * 1000
|
||||
)
|
||||
else
|
||||
Logger.error("Couldn't load feed #{feed.feed_url}: #{inspect(reason)}")
|
||||
{:ok, %HTTPoison.Response{} = response} ->
|
||||
Logger.error(
|
||||
"Couldn't load RSS feed #{feed.feed_url}, got unexpected response: #{inspect(response)}"
|
||||
)
|
||||
|
||||
if Frenzy.sentry_enabled?() do
|
||||
Sentry.capture_message(
|
||||
"Error loading Gemini feed: #{inspect(reason)}",
|
||||
extra: %{feed_id: feed.id, feed_url: feed.feed_url}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp do_update_feed(feed, content_type, data) do
|
||||
case FeedParser.parse(data, content_type) do
|
||||
{:ok, rss} ->
|
||||
update_feed_from_rss(feed, rss)
|
||||
|
||||
{:error, :no_data} ->
|
||||
:ok
|
||||
|
||||
{:error, reason} ->
|
||||
Logger.error("Unable to parse feed at '#{feed.feed_url}': #{inspect(reason)}")
|
||||
|
||||
if Frenzy.sentry_enabled?() do
|
||||
Sentry.capture_message("Unable to parse feed: #{inspect(reason)}",
|
||||
extra: %{feed_id: feed.id, feed_url: feed.feed_url}
|
||||
)
|
||||
end
|
||||
{:error, %HTTPoison.Error{reason: reason}} ->
|
||||
Logger.error("Couldn't load RSS feed #{feed.feed_url}: #{inspect(reason)}")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -228,10 +150,11 @@ defmodule Frenzy.UpdateFeeds do
|
|||
FetchFavicon.run(feed)
|
||||
end
|
||||
|
||||
Enum.each(rss.items, fn entry ->
|
||||
{guid, _} = CreateItem.real_guid_and_url(entry)
|
||||
feed = Repo.preload(feed, [:items])
|
||||
|
||||
unless Item.exists?(feed.id, guid) do
|
||||
Enum.each(rss.items, fn entry ->
|
||||
# todo: use Repo.exists for this
|
||||
if !Enum.any?(feed.items, fn item -> item.guid == entry.guid end) do
|
||||
CreateItem.start_link(feed, entry)
|
||||
end
|
||||
end)
|
||||
|
|
|
@ -8,12 +8,10 @@ defmodule Frenzy.User do
|
|||
field :password_hash, :string
|
||||
field :fever_password, :string, virtual: true
|
||||
field :fever_auth_token, :string
|
||||
field :oidc_subject, :string
|
||||
|
||||
has_many :approved_clients, Frenzy.ApprovedClient, on_delete: :delete_all
|
||||
|
||||
has_many :groups, Frenzy.Group, on_delete: :delete_all
|
||||
has_many :feeds, through: [:groups, :feeds]
|
||||
|
||||
timestamps()
|
||||
end
|
||||
|
@ -26,7 +24,6 @@ defmodule Frenzy.User do
|
|||
password_hash: String.t(),
|
||||
fever_password: String.t() | nil,
|
||||
fever_auth_token: String.t(),
|
||||
oidc_subject: String.t() | nil,
|
||||
approved_clients: [Frenzy.ApprovedClient.t()] | Ecto.Association.NotLoaded.t(),
|
||||
groups: [Frenzy.Group.t()] | Ecto.Association.NotLoaded.t(),
|
||||
inserted_at: NaiveDateTime.t(),
|
||||
|
@ -63,11 +60,6 @@ defmodule Frenzy.User do
|
|||
|> put_fever_token()
|
||||
end
|
||||
|
||||
def set_oidc_subject_changeset(user, attrs) do
|
||||
user
|
||||
|> cast(attrs, [:oidc_subject])
|
||||
end
|
||||
|
||||
defp put_password_hash(
|
||||
%Ecto.Changeset{valid?: true, changes: %{password: password}} = changeset
|
||||
) do
|
||||
|
|
|
@ -15,8 +15,7 @@ defmodule FrenzyWeb.AccountController do
|
|||
|
||||
render(conn, "show.html", %{
|
||||
user: user,
|
||||
clients: clients,
|
||||
can_link_oidc: Frenzy.oidc_enabled?() && user.oidc_subject in [nil, ""]
|
||||
clients: clients
|
||||
})
|
||||
end
|
||||
|
||||
|
@ -148,10 +147,4 @@ defmodule FrenzyWeb.AccountController do
|
|||
opml = Frenzy.OPML.Exporter.export(user.groups)
|
||||
send_download(conn, {:binary, opml}, filename: "frenzy_export.opml")
|
||||
end
|
||||
|
||||
def link_oidc(conn, _params) do
|
||||
conn
|
||||
|> put_session(:continue_path, Routes.account_path(conn, :show))
|
||||
|> redirect(to: Routes.login_path(conn, :ueberauth_request, "oidc"))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
defmodule FrenzyWeb.Fervor.ItemsController do
|
||||
use FrenzyWeb, :controller
|
||||
alias Frenzy.{Repo, Item, Group, Feed}
|
||||
alias Frenzy.{Repo, Item}
|
||||
import Ecto.Query
|
||||
alias FrenzyWeb.Fervor.Paginator
|
||||
|
||||
plug :get_specific_item
|
||||
|
||||
def get_specific_item(%Plug.Conn{path_params: %{"id" => id}} = conn, _opts) when id != "sync" do
|
||||
user = conn.assigns[:user] |> Repo.preload(:feeds)
|
||||
def get_specific_item(%Plug.Conn{path_params: %{"id" => id}} = conn, _opts) do
|
||||
user = conn.assigns[:user] |> Repo.preload(groups: [:feeds])
|
||||
|
||||
feeds = Enum.flat_map(user.groups, fn g -> g.feeds end)
|
||||
|
||||
item = Repo.get(Item, id)
|
||||
|
||||
if Enum.any?(user.feeds, fn f -> f.id == item.feed_id end) do
|
||||
if Enum.any?(feeds, fn f -> f.id == item.feed_id end) do
|
||||
assign(conn, :item, item)
|
||||
else
|
||||
conn
|
||||
|
@ -56,18 +58,14 @@ defmodule FrenzyWeb.Fervor.ItemsController do
|
|||
json(conn, Item.to_fervor(item))
|
||||
end
|
||||
|
||||
defp mark_item(conn, changes) do
|
||||
def mark_item(conn, changes) do
|
||||
item = conn.assigns[:item] |> Repo.preload(:feed)
|
||||
|
||||
changeset = Item.changeset(item, changes)
|
||||
|
||||
if changeset.valid? do
|
||||
{:ok, item} = Repo.update(changeset)
|
||||
{:ok, item} = Repo.update(changeset)
|
||||
|
||||
json(conn, Item.to_fervor(item))
|
||||
else
|
||||
json(conn, Item.to_fervor(item))
|
||||
end
|
||||
json(conn, Item.to_fervor(item))
|
||||
end
|
||||
|
||||
def read_specific_item(conn, _params) do
|
||||
|
@ -78,7 +76,7 @@ defmodule FrenzyWeb.Fervor.ItemsController do
|
|||
mark_item(conn, %{read: false})
|
||||
end
|
||||
|
||||
defp mark_multiple_items(conn, %{"ids" => ids}, changes) do
|
||||
def mark_multiple_items(conn, %{"ids" => ids}, changes) do
|
||||
user = conn.assigns[:user] |> Repo.preload(groups: [:feeds])
|
||||
feeds = Enum.flat_map(user.groups, fn g -> g.feeds end)
|
||||
|
||||
|
@ -94,14 +92,14 @@ defmodule FrenzyWeb.Fervor.ItemsController do
|
|||
Repo.get(Item, id)
|
||||
end)
|
||||
|> Enum.filter(fn item ->
|
||||
Enum.any?(feeds, fn f -> f.id == item.feed_id end) && !item.tombstone
|
||||
Enum.any?(feeds, fn f -> f.id == item.feed_id end)
|
||||
end)
|
||||
|> Enum.map(fn item ->
|
||||
item = Repo.preload(item, :feed)
|
||||
changeset = Item.changeset(item, changes)
|
||||
|
||||
case Repo.update(changeset) do
|
||||
{:ok, item} -> to_string(item.id)
|
||||
{:ok, item} -> item.id
|
||||
_ -> nil
|
||||
end
|
||||
end)
|
||||
|
@ -110,7 +108,7 @@ defmodule FrenzyWeb.Fervor.ItemsController do
|
|||
json(conn, read_ids)
|
||||
end
|
||||
|
||||
defp mark_multiple_items(conn, _params, _changes) do
|
||||
def mark_multiple_items(conn, _params, _changes) do
|
||||
conn
|
||||
|> put_status(400)
|
||||
|> json(%{error: "No items provided."})
|
||||
|
@ -123,50 +121,4 @@ defmodule FrenzyWeb.Fervor.ItemsController do
|
|||
def unread_multiple(conn, params) do
|
||||
mark_multiple_items(conn, params, %{read: false})
|
||||
end
|
||||
|
||||
def sync(conn, params) do
|
||||
sync_timestamp = Timex.now()
|
||||
|
||||
feed_ids =
|
||||
Group
|
||||
|> where([g], g.user_id == ^conn.assigns.user.id)
|
||||
|> join(:inner, [g], f in Feed, on: f.group_id == g.id)
|
||||
|> select([g, f], f.id)
|
||||
|> Repo.all()
|
||||
|
||||
last_sync =
|
||||
with s when is_binary(s) <- Map.get(params, "last_sync"),
|
||||
{:ok, datetime} <- Timex.parse(s, "{ISO:Extended:Z}") do
|
||||
datetime
|
||||
else
|
||||
_ ->
|
||||
nil
|
||||
end
|
||||
|
||||
{deleted_ids, upserted} =
|
||||
case last_sync do
|
||||
nil ->
|
||||
items =
|
||||
Item
|
||||
|> where([i], not i.tombstone and i.feed_id in ^feed_ids)
|
||||
|> order_by([i], desc: i.inserted_at)
|
||||
|> limit(1000)
|
||||
|> Repo.all()
|
||||
|
||||
{[], items}
|
||||
|
||||
_ ->
|
||||
all_items =
|
||||
Repo.all(from i in Item, where: i.feed_id in ^feed_ids and i.updated_at >= ^last_sync)
|
||||
|
||||
{tombstones, rest} = Enum.split_with(all_items, & &1.tombstone)
|
||||
{Enum.map(tombstones, & &1.id), rest}
|
||||
end
|
||||
|
||||
json(conn, %{
|
||||
sync_timestamp: Timex.format!(sync_timestamp, "{ISO:Extended:Z}"),
|
||||
delete: Enum.map(deleted_ids, &to_string/1),
|
||||
upsert: Enum.map(upserted, &Item.to_fervor/1)
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
|
@ -110,7 +110,7 @@ defmodule FrenzyWeb.Fervor.OauthController do
|
|||
)
|
||||
|
||||
uri = %URI{parsed | query: query}
|
||||
redirect(conn, external: URI.to_string(uri))
|
||||
redirect(conn, to: uri)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -174,8 +174,7 @@ defmodule FrenzyWeb.Fervor.OauthController do
|
|||
|
||||
json(conn, %{
|
||||
access_token: access_token,
|
||||
token_type: "bearer",
|
||||
owner: to_string(approved_client.user_id)
|
||||
token_type: "bearer"
|
||||
})
|
||||
end
|
||||
end
|
||||
|
|
|
@ -196,12 +196,7 @@ defmodule FrenzyWeb.FeverController do
|
|||
|> Enum.map(fn f -> f.id end)
|
||||
|
||||
unread =
|
||||
Repo.all(
|
||||
from i in Item,
|
||||
where: i.feed_id in ^feed_ids and not i.read,
|
||||
limit: 10000,
|
||||
order_by: [desc: :id]
|
||||
)
|
||||
Repo.all(from i in Item, where: i.feed_id in ^feed_ids, where: [read: false])
|
||||
|> Enum.map(fn item -> item.id end)
|
||||
|> Enum.join(",")
|
||||
|
||||
|
@ -227,20 +222,19 @@ defmodule FrenzyWeb.FeverController do
|
|||
items =
|
||||
cond do
|
||||
Map.has_key?(params, "with_ids") ->
|
||||
item_ids =
|
||||
params["with_ids"]
|
||||
|> String.split(",")
|
||||
|> Enum.map(fn str ->
|
||||
{id, _} = str |> String.trim() |> Integer.parse()
|
||||
id
|
||||
end)
|
||||
params["with_ids"]
|
||||
|> String.split(",")
|
||||
|> Enum.map(fn id ->
|
||||
{id, _} = id |> String.trim() |> Integer.parse()
|
||||
item = Repo.get(Item, id)
|
||||
|
||||
Repo.all(
|
||||
from i in Item,
|
||||
where: i.id in ^item_ids,
|
||||
where: i.feed_id in ^feed_ids,
|
||||
where: not i.tombstone
|
||||
)
|
||||
if not is_nil(item) and item.feed_id in feed_ids do
|
||||
item
|
||||
else
|
||||
nil
|
||||
end
|
||||
end)
|
||||
|> Enum.reject(&is_nil/1)
|
||||
|
||||
Map.has_key?(params, "since_id") ->
|
||||
since = Repo.get(Item, params["since_id"])
|
||||
|
|
|
@ -10,7 +10,9 @@ defmodule FrenzyWeb.ItemController do
|
|||
|
||||
item = Repo.get(Item, id)
|
||||
|
||||
if Enum.any?(user.feeds, fn f -> f.id == item.feed_id end) do
|
||||
feeds = Enum.flat_map(user.groups, fn g -> g.feeds end)
|
||||
|
||||
if Enum.any?(feeds, fn f -> f.id == item.feed_id end) do
|
||||
conn
|
||||
|> assign(:item, item)
|
||||
else
|
||||
|
@ -32,7 +34,7 @@ defmodule FrenzyWeb.ItemController do
|
|||
})
|
||||
end
|
||||
|
||||
def read(conn, params) do
|
||||
def read(conn, _params) do
|
||||
item = conn.assigns[:item] |> Repo.preload(:feed)
|
||||
|
||||
changeset =
|
||||
|
@ -42,11 +44,10 @@ defmodule FrenzyWeb.ItemController do
|
|||
})
|
||||
|
||||
{:ok, item} = Repo.update(changeset)
|
||||
path = Map.get(params, "redirect") || Routes.item_path(Endpoint, :show, item.id)
|
||||
redirect(conn, to: path)
|
||||
redirect(conn, to: Routes.item_path(Endpoint, :show, item.id))
|
||||
end
|
||||
|
||||
def unread(conn, params) do
|
||||
def unread(conn, _params) do
|
||||
item = conn.assigns[:item] |> Repo.preload(:feed)
|
||||
|
||||
changeset =
|
||||
|
@ -55,8 +56,7 @@ defmodule FrenzyWeb.ItemController do
|
|||
read_date: nil
|
||||
})
|
||||
|
||||
{:ok, item} = Repo.update(changeset)
|
||||
path = Map.get(params, "redirect") || Routes.item_path(Endpoint, :show, item.id)
|
||||
redirect(conn, to: path)
|
||||
Repo.update(changeset)
|
||||
redirect(conn, to: Routes.item_path(Endpoint, :show, item.id))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,29 +3,27 @@ defmodule FrenzyWeb.LoginController do
|
|||
alias Frenzy.{Repo, User}
|
||||
alias FrenzyWeb.Endpoint
|
||||
|
||||
if Frenzy.oidc_enabled?() do
|
||||
plug Ueberauth
|
||||
end
|
||||
|
||||
def login(conn, params) do
|
||||
conn
|
||||
|> put_session(:continue_path, Map.get(params, "continue"))
|
||||
|> render("login.html", %{
|
||||
oidc_enabled?: Frenzy.oidc_enabled?()
|
||||
render(conn, "login.html", %{
|
||||
continue: Map.get(params, "continue")
|
||||
})
|
||||
end
|
||||
|
||||
def login_post(conn, %{"username" => username, "password" => password}) do
|
||||
def login_post(conn, %{"username" => username, "password" => password} = params) do
|
||||
user = Repo.get_by(User, username: username)
|
||||
|
||||
case Bcrypt.check_pass(user, password) do
|
||||
{:ok, user} ->
|
||||
put_user_and_redirect(conn, user)
|
||||
user_token = Phoenix.Token.sign(Endpoint, "user token", user.id)
|
||||
conn = put_session(conn, :user_token, user_token)
|
||||
|
||||
redirect_uri = Map.get(params, "continue") || Routes.group_path(Endpoint, :index)
|
||||
redirect(conn, to: redirect_uri)
|
||||
|
||||
{:error, _reason} ->
|
||||
conn
|
||||
|> put_flash(:error, "Invalid username or password.")
|
||||
|> redirect(to: Routes.login_path(Endpoint, :login, continue: continue_path(conn)))
|
||||
|> redirect(to: Routes.login_path(Endpoint, :login))
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -35,52 +33,4 @@ defmodule FrenzyWeb.LoginController do
|
|||
|> clear_session()
|
||||
|> redirect(to: "/")
|
||||
end
|
||||
|
||||
def ueberauth_callback(%{assigns: %{ueberauth_failure: _fails}} = conn, _params) do
|
||||
conn
|
||||
|> put_flash(:error, "Failed to authenticate.")
|
||||
|> redirect(to: Routes.login_path(Endpoint, :login, continue: continue_path(conn)))
|
||||
end
|
||||
|
||||
def ueberauth_callback(
|
||||
%{assigns: %{ueberauth_auth: %{credentials: %{other: %{user_info: %{"sub" => subject}}}}}} =
|
||||
conn,
|
||||
_params
|
||||
) do
|
||||
case Repo.get_by(User, oidc_subject: subject) do
|
||||
nil ->
|
||||
conn = FrenzyWeb.Plug.Authenticate.call(conn, nil)
|
||||
|
||||
case conn.assigns.user do
|
||||
%User{} = user ->
|
||||
changeset = User.set_oidc_subject_changeset(user, %{oidc_subject: subject})
|
||||
{:ok, _user} = Repo.update(changeset)
|
||||
|
||||
conn
|
||||
|> put_flash(:info, "Successfully linked OIDC.")
|
||||
|> redirect(to: continue_path(conn))
|
||||
|
||||
_ ->
|
||||
# TODO: register new user for subject
|
||||
conn
|
||||
|> put_flash(:error, "No matching OIDC subject.")
|
||||
|> redirect(to: Routes.login_path(Endpoint, :login, continue: continue_path(conn)))
|
||||
end
|
||||
|
||||
user ->
|
||||
put_user_and_redirect(conn, user)
|
||||
end
|
||||
end
|
||||
|
||||
defp continue_path(conn) do
|
||||
get_session(conn, :continue_path) || Routes.group_path(Endpoint, :index)
|
||||
end
|
||||
|
||||
defp put_user_and_redirect(conn, user) do
|
||||
user_token = Phoenix.Token.sign(Endpoint, "user token", user.id)
|
||||
|
||||
conn
|
||||
|> put_session(:user_token, user_token)
|
||||
|> redirect(to: continue_path(conn))
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
defmodule FrenzyWeb.Endpoint do
|
||||
if Frenzy.sentry_enabled?() do
|
||||
use Sentry.PlugCapture
|
||||
end
|
||||
|
||||
use Phoenix.Endpoint, otp_app: :frenzy
|
||||
|
||||
@session_options [
|
||||
|
@ -25,7 +21,7 @@ defmodule FrenzyWeb.Endpoint do
|
|||
at: "/",
|
||||
from: :frenzy,
|
||||
gzip: false,
|
||||
only: ~w(assets fonts images favicon.ico robots.txt)
|
||||
only: ~w(css fonts images js favicon.ico robots.txt)
|
||||
|
||||
# Code reloading can be explicitly enabled under the
|
||||
# :code_reloader configuration of your endpoint.
|
||||
|
@ -43,10 +39,6 @@ defmodule FrenzyWeb.Endpoint do
|
|||
pass: ["*/*"],
|
||||
json_decoder: Phoenix.json_library()
|
||||
|
||||
if Frenzy.sentry_enabled?() do
|
||||
plug Sentry.PlugContext
|
||||
end
|
||||
|
||||
plug Plug.MethodOverride
|
||||
plug Plug.Head
|
||||
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
<div id={@id}>
|
||||
<%= if Application.fetch_env!(:frenzy, :env) == :dev do %>
|
||||
<div id="<%= @id %>">
|
||||
<%= if Mix.env == :dev do %>
|
||||
<pre><%= Jason.encode!(@opts, pretty: true) %></pre>
|
||||
<% end %>
|
||||
|
||||
<%= form_for @opts, "#", [as: :opts, phx_change: :update_stage, phx_target: @myself], fn f -> %>
|
||||
<div class="row mb-2">
|
||||
<label class="col-sm-2 col-form-label" for={"#{@id}-stage"}>Module</label>
|
||||
<%= f = form_for @opts, "#", [as: :opts, phx_change: :update_stage, phx_target: @myself] %>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label" for="<%= @id %>-stage">Module</label>
|
||||
<div class="col-sm-10">
|
||||
<%= select f, :stage, @stages, prompt: "Select a stage...", id: "#{@id}-stage", class: "form-select" %>
|
||||
<%= select f, :stage, @stages, prompt: "Select a stage...", id: "#{@id}-stage", class: "custom-select" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</form>
|
||||
|
||||
<div class="card mb-2">
|
||||
<div class="card-header">
|
||||
|
@ -19,7 +19,7 @@
|
|||
<div class="card-body">
|
||||
<% component = component_module(@opts["stage"]) %>
|
||||
<%= unless is_nil(component) do %>
|
||||
<%= live_component(component, index: @index, id: "#{@id}-conditional", stage: @stage, keypath: @keypath ++ ["opts"]) %>
|
||||
<%= live_component(@socket, component, index: @index, id: "#{@id}-conditional", stage: @stage, keypath: @keypath ++ ["opts"]) %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -31,8 +31,8 @@
|
|||
<div class="col">
|
||||
<h4 class="m-0">Condition: Filter</h4>
|
||||
</div>
|
||||
<div class="col text-end">
|
||||
<button phx-click="convert_to_rule" phx-target={@id} class="btn btn-primary btn-sm">Convert to Rule</button>
|
||||
<div class="col text-right">
|
||||
<button phx-click="convert_to_rule" phx-target="#<%= @id %>" class="btn btn-primary btn-sm">Convert to Rule</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -40,12 +40,12 @@
|
|||
<%= if @confirm_convert_to_rule do %>
|
||||
<div class="alert alert-danger mb-2">
|
||||
<p>This will modify the conditional stage to only run when the first rule is met. Are you sure you want to proceed?</p>
|
||||
<button class="btn btn-danger btn-sm" phx-click="confirm_convert_to_rule" phx-target={@id}>Convert to Rule</button>
|
||||
<button class="btn btn-secondary btn-sm" phx-click="cancel_convert_to_rule" phx-target={@id}>Cancel</button>
|
||||
<button class="btn btn-danger btn-sm" phx-click="confirm_convert_to_rule" phx-target="#<%= @id %>">Convert to Rule</button>
|
||||
<button class="btn btn-secondary btn-sm" phx-click="cancel_convert_to_rule" phx-target="#<%= @id %>">Cancel</button>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= live_component FrenzyWeb.FilterLive, id: "##{@id}-filter", parent_id: @id, filter: @opts["condition"] %>
|
||||
<%= live_component @socket, FrenzyWeb.FilterLive, id: "##{@id}-filter", parent_id: @id, filter: @opts["condition"] %>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
|
@ -55,13 +55,13 @@
|
|||
<div class="col">
|
||||
<h4 class="m-0">Condition: Rule</h4>
|
||||
</div>
|
||||
<div class="col text-end">
|
||||
<button phx-click="convert_to_filter" phx-target={@id} class="btn btn-primary btn-sm">Convert to Filter</button>
|
||||
<div class="col text-right">
|
||||
<button phx-click="convert_to_filter" phx-target="#<%= @id %>" class="btn btn-primary btn-sm">Convert to Filter</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<%= live_component FrenzyWeb.FilterRuleLive, id: "##{@id}-rule", parent_id: @id, rule: @opts["condition"], index: :no_index %>
|
||||
<%= live_component @socket, FrenzyWeb.FilterRuleLive, id: "##{@id}-rule", parent_id: @id, rule: @opts["condition"], index: :no_index %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
|
@ -25,7 +25,7 @@ defmodule FrenzyWeb.ConfigureStage.FilterStageLive do
|
|||
def handle_event("add_rule", _params, socket) do
|
||||
new_rules =
|
||||
socket.assigns.opts["rules"] ++
|
||||
[%{"mode" => "contains_string", "param" => "", "property" => "title", "weight" => 1}]
|
||||
[%{"mode" => "text", "param" => "", "property" => "title", "weight" => 1}]
|
||||
|
||||
new_opts = Map.put(socket.assigns.opts, "rules", new_rules)
|
||||
new_stage = Frenzy.Keypath.set(socket.assigns.stage, socket.assigns.keypath, new_opts)
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
<div id={@id}>
|
||||
<%= if Application.fetch_env!(:frenzy, :env) == :dev do %>
|
||||
<pre><%= Jason.encode!(@opts, pretty: true) %></pre>
|
||||
<% end %>
|
||||
<%= live_component FrenzyWeb.FilterLive, id: "#{@id}-filter", parent_id: @id, filter: @opts %>
|
||||
</div>
|
|
@ -0,0 +1,6 @@
|
|||
<div id="<%= @id %>">
|
||||
<%= if Mix.env == :dev do %>
|
||||
<pre><%= Jason.encode!(@opts, pretty: true) %></pre>
|
||||
<% end %>
|
||||
<%= live_component @socket, FrenzyWeb.FilterLive, id: "#{@id}-filter", parent_id: @id, filter: @opts %>
|
||||
</div>
|
|
@ -3,18 +3,12 @@ defmodule FrenzyWeb.ConfigureStage.ScrapeStageLive do
|
|||
|
||||
@extractors [
|
||||
{"Builtin", "builtin"},
|
||||
{"512 Pixels", Frenzy.Pipeline.Extractor.FiveTwelvePixels},
|
||||
{"Ars Technica", Frenzy.Pipeline.Extractor.ArsTechnica},
|
||||
{"beckyhansmeyer.com", Frenzy.Pipeline.Extractor.BeckyHansmeyer},
|
||||
{"birchtree.me", Frenzy.Pipeline.Extractor.Birchtree},
|
||||
{"daringfireball.net", Frenzy.Pipeline.Extractor.DaringFireball},
|
||||
{"Election Law Blog", Frenzy.Pipeline.Extractor.ElectionLawBlog},
|
||||
{"ericasadun.com", Frenzy.Pipeline.Extractor.EricaSadun},
|
||||
{"finertech.com", Frenzy.Pipeline.Extractor.FinerTech},
|
||||
{"macstories.net", Frenzy.Pipeline.Extractor.MacStories},
|
||||
{"om.co", Frenzy.Pipeline.Extractor.OmMalik},
|
||||
{"slate.com", Frenzy.Pipeline.Extractor.Slate},
|
||||
{"The Verge", Frenzy.Pipeline.Extractor.TheVerge},
|
||||
{"whatever.scalzi.com", Frenzy.Pipeline.Extractor.WhateverScalzi}
|
||||
]
|
||||
|> Enum.map(fn {pretty_name, module} ->
|
||||
|
@ -27,6 +21,11 @@ defmodule FrenzyWeb.ConfigureStage.ScrapeStageLive do
|
|||
}
|
||||
end)
|
||||
|
||||
@schema %{
|
||||
"convert_to_data_uris" => :boolean,
|
||||
"extractor" => :string
|
||||
}
|
||||
|
||||
@impl true
|
||||
def mount(socket) do
|
||||
{:ok, assign(socket, extractors: @extractors)}
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
<div id={@id}>
|
||||
<%= if Application.fetch_env!(:frenzy, :env) == :dev do %>
|
||||
<pre><%= Jason.encode!(@opts, pretty: true) %></pre>
|
||||
<% end %>
|
||||
<%= form_for @opts, "#", [as: :opts, phx_change: :update_stage, phx_target: @myself], fn f -> %>
|
||||
<div class="form-check mb-2">
|
||||
<%= checkbox f, :convert_to_data_uris, id: "#{@id}-convert_to_data_uris", class: "form-check-input" %>
|
||||
<label class="form-check-label" for={"#{@id}-convert_to_data_uris"}>Convert Images to Embedded Data URIs</label>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<label class="col-sm-2 col-form-label" for={"#{@id}-extractor"}>Extractor</label>
|
||||
<div class="col-sm-10">
|
||||
<%= select f, :extractor, @extractors, id: "#{@id}-extractor", class: "form-select" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
|
@ -0,0 +1,17 @@
|
|||
<div id="<%= @id %>">
|
||||
<%= if Mix.env == :dev do %>
|
||||
<pre><%= Jason.encode!(@opts, pretty: true) %></pre>
|
||||
<% end %>
|
||||
<%= f = form_for @opts, "#", [as: :opts, phx_change: :update_stage, phx_target: @myself] %>
|
||||
<div class="form-group form-check">
|
||||
<%= checkbox f, :convert_to_data_uris, id: "#{@id}-convert_to_data_uris", class: "form-check-input" %>
|
||||
<label class="form-check-label" for="<%= @id %>-convert_to_data_uris">Convert Images to Embedded Data URIs</label>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label" for="<%= @id %>-extractor">Extractor</label>
|
||||
<div class="col-sm-10">
|
||||
<%= select f, :extractor, @extractors, id: "#{@id}-extractor", class: "custom-select" %>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
|
@ -3,16 +3,10 @@ defmodule FrenzyWeb.EditPipelineLive do
|
|||
use Phoenix.HTML
|
||||
alias Frenzy.{Repo, Pipeline}
|
||||
|
||||
def title(%{pipeline: %Pipeline{name: name}}) do
|
||||
"Edit #{name}"
|
||||
end
|
||||
|
||||
@stages [
|
||||
{"Filter Stage", "Frenzy.Pipeline.FilterStage"},
|
||||
{"Scrape Stage", "Frenzy.Pipeline.ScrapeStage"},
|
||||
{"Conditional Stage", "Frenzy.Pipeline.ConditionalStage"},
|
||||
{"Gemini Scrape Stage", "Frenzy.Pipeline.GeminiScrapeStage"},
|
||||
{"Render Gemini Stage", "Frenzy.Pipeline.RenderGeminiStage"}
|
||||
{"Conditional Stage", "Frenzy.Pipeline.ConditionalStage"}
|
||||
]
|
||||
|
||||
def stages, do: @stages
|
||||
|
@ -106,13 +100,13 @@ defmodule FrenzyWeb.EditPipelineLive do
|
|||
end
|
||||
end
|
||||
|
||||
def component_for(%{"module_name" => module} = stage, index) do
|
||||
def component_for(socket, %{"module_name" => module} = stage, index) do
|
||||
case component_module(module) do
|
||||
nil ->
|
||||
nil
|
||||
|
||||
component ->
|
||||
live_component(component,
|
||||
live_component(socket, component,
|
||||
index: index,
|
||||
id: "stage-#{index}",
|
||||
stage: stage,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<h1>Edit <%= @pipeline.name %></h1>
|
||||
|
||||
<a href={Routes.pipeline_path(FrenzyWeb.Endpoint, :edit, @pipeline.id, json: "")} class="btn btn-primary">Edit as JSON</a>
|
||||
<a href="<%= Routes.pipeline_path(FrenzyWeb.Endpoint, :edit, @pipeline.id, json: "") %>" class="btn btn-primary">Edit as JSON</a>
|
||||
|
||||
<%= for {stage, index} <- Enum.with_index(@pipeline.stages) do %>
|
||||
<div class="card mt-4">
|
||||
|
@ -9,27 +9,27 @@
|
|||
<div class="col">
|
||||
<h4 class="m-0"><%= stage["module_name"] %></h4>
|
||||
</div>
|
||||
<div class="col text-end">
|
||||
<div class="col text-right">
|
||||
<%= content_tag :button, "Move Up", [phx_click: :move_up, phx_value_index: index, disabled: index == 0, class: "btn btn-secondary btn-sm"] %>
|
||||
|
||||
<%= content_tag :button, "Move Down", [phx_click: :move_down, phx_value_index: index, disabled: index == length(@pipeline.stages) - 1, class: "btn btn-secondary btn-sm"] %>
|
||||
|
||||
<button phx-click="delete_stage" phx-value-index={index} class="btn btn-danger btn-sm">Delete</button>
|
||||
<button phx-click="delete_stage" phx-value-index="<%= index %>" class="btn btn-danger btn-sm">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<%= component_for(stage, index) %>
|
||||
<%= component_for(@socket, stage, index) %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= form_for :stage, "#", [class: "mt-4 mb-4", phx_submit: :add_stage], fn f -> %>
|
||||
<div class="row mb-2">
|
||||
<%= f = form_for :stage, "#", [class: "mt-4 mb-4", phx_submit: :add_stage] %>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label" for="module_name">Module</label>
|
||||
<div class="col-sm-10">
|
||||
<%= select f, :module_name, @stages, class: "form-select" %>
|
||||
<%= select f, :module_name, @stages, class: "custom-select" %>
|
||||
</div>
|
||||
</div>
|
||||
<%= submit "Add Stage", class: "btn btn-primary" %>
|
||||
<% end %>
|
||||
</form>
|
|
@ -1,37 +0,0 @@
|
|||
<div id={@id}>
|
||||
<%= form_for @filter, "#", [phx_change: :update_filter, phx_target: "##{@parent_id}"], fn f -> %>
|
||||
<div class="row mb-2">
|
||||
<label class="col-sm-2 col-form-label" for={"#{@id}-mode"}>Mode</label>
|
||||
<div class="col-sm-10">
|
||||
<%= select f, :mode, @modes, id: "#{@id}-mode", class: "form-select" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<label class="col-sm-2 col-form-label" for={"#{@id}-score"}>Score</label>
|
||||
<div class="col-sm-10">
|
||||
<%= number_input f, :score, id: "#{@id}-score", class: "form-control" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= for {rule, index} <- Enum.with_index(@filter["rules"]) do %>
|
||||
<div class="card mt-2">
|
||||
<div class="card-header container-fluid">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h4 class="m-0">Rule <%= index %></h4>
|
||||
</div>
|
||||
<div class="col text-end">
|
||||
<button phx-click="delete_rule" phx-value-index={index} phx-target={"##{@id}"} class="btn btn-danger btn-sm">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<%= live_component FrenzyWeb.FilterRuleLive, id: "#{@id}-rule-#{index}", parent_id: @parent_id, rule: rule, index: index %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= form_for :rule, "#", [class: "mt-2", phx_submit: :add_rule, phx_target: "##{@parent_id}"], fn _f -> %>
|
||||
<%= submit "Add Rule", class: "btn btn-primary" %>
|
||||
<% end %>
|
||||
</div>
|
|
@ -0,0 +1,37 @@
|
|||
<div id="<%= @id %>">
|
||||
<%= f = form_for @filter, "#", [phx_change: :update_filter, phx_target: "##{@parent_id}"] %>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label" for="<%= @id %>-mode">Mode</label>
|
||||
<div class="col-sm-10">
|
||||
<%= select f, :mode, @modes, id: "#{@id}-mode", class: "custom-select" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label" for="<%= @id %>-score">Score</label>
|
||||
<div class="col-sm-10">
|
||||
<%= number_input f, :score, id: "#{@id}-score", class: "form-control" %>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<%= for {rule, index} <- Enum.with_index(@filter["rules"]) do %>
|
||||
<div class="card mt-2">
|
||||
<div class="card-header container-fluid">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h4 class="m-0">Rule <%= index %></h4>
|
||||
</div>
|
||||
<div class="col text-right">
|
||||
<button phx-click="delete_rule" phx-value-index="<%= index %>" phx-target="#<%= @id %>" class="btn btn-danger btn-sm">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<%= live_component @socket, FrenzyWeb.FilterRuleLive, id: "#{@id}-rule-#{index}", parent_id: @parent_id, rule: rule, index: index %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= f = form_for :rule, "#", class: "mt-2", phx_submit: :add_rule, phx_target: "##{@parent_id}" %>
|
||||
<%= submit "Add Rule", class: "btn btn-primary" %>
|
||||
</form>
|
||||
</div>
|
|
@ -3,15 +3,13 @@ defmodule FrenzyWeb.FilterRuleLive do
|
|||
|
||||
@modes [
|
||||
{"Contains Substring", "contains_string"},
|
||||
{"Contains Substring (case sensitive)", "contains_string_case_sensitive"},
|
||||
{"Matches Regex", "matches_regex"}
|
||||
]
|
||||
|
||||
@properties [
|
||||
{"Title", "title"},
|
||||
{"URL", "url"},
|
||||
{"Author", "author"},
|
||||
{"Content", "content"}
|
||||
{"Author", "author"}
|
||||
]
|
||||
|
||||
@impl true
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
<div id={@id}>
|
||||
<%= form_for @rule, "#", [phx_change: :update_rule, phx_target: "##{@parent_id}"], fn f -> %>
|
||||
<%= hidden_input f, :index, value: @index %>
|
||||
<div class="row">
|
||||
<div class="col-3">
|
||||
<%= select f, :property, @properties, id: "#{@id}-property", class: "form-select" %>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<%= select f, :mode, @modes, id: "#{@id}-mode", class: "form-select" %>
|
||||
</div>
|
||||
<div class="col">
|
||||
<%= text_input f, :param, id: "#{@id}-param", placeholder: if(@rule["mode"] == "contains_string", do: "substring", else: "regex"), class: "form-control text-monospace" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-0 mt-4">
|
||||
<label class="col-sm-2 col-form-label" for={"#{@id}-weight"}>Rule Weight</label>
|
||||
<div class="col-sm-10">
|
||||
<%= number_input f, :weight, id: "#{@id}-weight", class: "form-control" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
|
@ -0,0 +1,29 @@
|
|||
<div id="<%= @id %>">
|
||||
<%= f = form_for @rule, "#", [phx_change: :update_rule, phx_target: "##{@parent_id}"] %>
|
||||
<%= hidden_input f, :index, value: @index %>
|
||||
<div class="form-group row mb-2">
|
||||
<label class="col-sm-2 col-form-label" for="<%= @id %>-property">Item Property</label>
|
||||
<div class="col-sm-10">
|
||||
<%= select f, :property, @properties, id: "#{@id}-property", class: "custom-select" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row mb-2">
|
||||
<label class="col-sm-2 col-form-label" for="<%= @id %>-mode">Mode</label>
|
||||
<div class="col-sm-10">
|
||||
<%= select f, :mode, @modes, id: "#{@id}-mode", class: "custom-select" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row mb-2">
|
||||
<label class="col-sm-2 col-form-label" for="<%= @id %>-param">Value</label>
|
||||
<div class="col-sm-10">
|
||||
<%= text_input f, :param, id: "#{@id}-param", placeholder: if(@rule["mode"] == "contains_string", do: "substring", else: "regex"), class: "form-control text-monospace" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row mb-0">
|
||||
<label class="col-sm-2 col-form-label" for="<%= @id %>-weight">Rule Weight</label>
|
||||
<div class="col-sm-10">
|
||||
<%= number_input f, :weight, id: "#{@id}-weight", class: "form-control" %>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
|
@ -25,7 +25,7 @@ defmodule FrenzyWeb.Plug.Authenticate do
|
|||
|> halt()
|
||||
|
||||
user ->
|
||||
user = Repo.preload(user, [:groups, :feeds])
|
||||
user = Repo.preload(user, groups: [:feeds])
|
||||
assign(conn, :user, user)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -34,15 +34,6 @@ defmodule FrenzyWeb.Router do
|
|||
post "/oauth/authorize", Fervor.OauthController, :authorize_post
|
||||
end
|
||||
|
||||
scope "/auth", FrenzyWeb do
|
||||
pipe_through :browser
|
||||
|
||||
if Frenzy.oidc_enabled?() do
|
||||
get "/:unused", LoginController, :ueberauth_request
|
||||
get "/:unused/callback", LoginController, :ueberauth_callback
|
||||
end
|
||||
end
|
||||
|
||||
scope "/", FrenzyWeb do
|
||||
pipe_through :browser
|
||||
pipe_through :browser_authenticate
|
||||
|
@ -56,10 +47,6 @@ defmodule FrenzyWeb.Router do
|
|||
post "/account/import", AccountController, :import
|
||||
post "/account/export", AccountController, :export
|
||||
|
||||
if Frenzy.oidc_enabled?() do
|
||||
get "/account/link_oidc", AccountController, :link_oidc
|
||||
end
|
||||
|
||||
get "/", GroupController, :index
|
||||
resources "/groups", GroupController
|
||||
get "/groups/:id/read", GroupController, :read
|
||||
|
@ -112,7 +99,6 @@ defmodule FrenzyWeb.Router do
|
|||
post "/api/v1/feeds/:id/delete", FeedsController, :delete
|
||||
|
||||
get "/api/v1/items", ItemsController, :items_list
|
||||
get "/api/v1/items/sync", ItemsController, :sync
|
||||
get "/api/v1/items/:id", ItemsController, :specific_item
|
||||
post "/api/v1/items/:id/read", ItemsController, :read_specific_item
|
||||
post "/api/v1/items/:id/unread", ItemsController, :unread_specific_item
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<h2>Change Fever Password</h2>
|
||||
|
||||
<%= form_tag Routes.account_path(@conn, :do_change_fever_password), method: :post do %>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<label for="new_password" class="col-sm-2 col-form-label">New Fever Password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" name="new_password" id="new_password" minlength="8" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<%= submit "Change Fever Password", class: "btn btn-primary" %>
|
||||
</div>
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
<h2>Change Password</h2>
|
||||
|
||||
<%= form_tag Routes.account_path(@conn, :do_change_password), method: :post do %>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<label for="old_password" class="col-sm-2 col-form-label">Old Password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" name="old_password" id="old_password" minlength="8" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<label for="new_password" class="col-sm-2 col-form-label">New Password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" name="new_password" id="new_password" minlength="8" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<label for="confirm_new_password" class="col-sm-2 col-form-label">Confirm New Password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" name="confirm_new_password" id="confirm_new_password" minlength="8" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<%= submit "Change Password", class: "btn btn-primary" %>
|
||||
</div>
|
||||
|
|
|
@ -20,47 +20,43 @@
|
|||
</section>
|
||||
|
||||
<section class="card mt-4">
|
||||
<h4 class="card-header">Security</h4>
|
||||
<h4 class="card-header">Security</h4>
|
||||
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<a href="<%= Routes.account_path(@conn, :change_password) %>" class="btn btn-secondary">Change Password</a>
|
||||
<a href="<%= Routes.account_path(@conn, :change_fever_password) %>" class="btn btn-secondary">Change Fever Password</a>
|
||||
</li>
|
||||
<%= if @can_link_oidc do %>
|
||||
<ul class="list-group list-group-flush">
|
||||
<li class="list-group-item">
|
||||
<a href="<%= Routes.account_path(@conn, :link_oidc) %>" class="btn btn-secondary">Link OIDC</a>
|
||||
<a href="<%= Routes.account_path(@conn, :change_password) %>" class="btn btn-secondary">Change Password</a>
|
||||
|
||||
<a href="<%= Routes.account_path(@conn, :change_fever_password) %>" class="btn btn-secondary">Change Fever Password</a>
|
||||
</li>
|
||||
<% end %>
|
||||
<li class="list-group-item">
|
||||
<li class="list-group-item">
|
||||
<h5 class="card-title">Approved Clients</h5>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client</th>
|
||||
<th>Revoke Access</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for {approved, fervor} <- @clients do %>
|
||||
<tr>
|
||||
<td>
|
||||
<%= if fervor.website do %>
|
||||
<a href="<%= fervor.website %>"><%= fervor.client_name %></a>
|
||||
<% else %>
|
||||
<%= fervor.client_name %>
|
||||
<% end %>
|
||||
</td>
|
||||
<td>
|
||||
<%= form_tag Routes.account_path(@conn, :remove_client), method: :post do %>
|
||||
<input type="hidden" name="client_id" value="<%= approved.client_id %>">
|
||||
<%= submit "Revoke", class: "btn btn-danger" %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</li>
|
||||
</ul>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client</th>
|
||||
<th>Revoke Access</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<%= for {approved, fervor} <- @clients do %>
|
||||
<tr>
|
||||
<td>
|
||||
<%= if fervor.website do %>
|
||||
<a href="<%= fervor.website %>"><%= fervor.client_name %></a>
|
||||
<% else %>
|
||||
<%= fervor.client_name %>
|
||||
<% end %>
|
||||
</td>
|
||||
<td>
|
||||
<%= form_tag Routes.account_path(@conn, :remove_client), method: :post do %>
|
||||
<input type="hidden" name="client_id" value="<%= approved.client_id %>">
|
||||
<%= submit "Revoke", class: "btn btn-danger" %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table> </li>
|
||||
</ul>
|
||||
|
||||
</section>
|
||||
|
|
|
@ -1,32 +1,32 @@
|
|||
<h1>Edit Feed</h1>
|
||||
|
||||
<%= form_for @changeset, Routes.feed_path(@conn, :update, @feed.id), fn f -> %>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label" for="feed_url">Feed URL</label>
|
||||
<div class="col-sm-10">
|
||||
<%= text_input f, :feed_url, class: "form-control" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label" for="refresh_frequency">Refresh Frequency</label>
|
||||
<div class="col-sm-10">
|
||||
<%= select f, :refresh_frequency, @refresh_frequencies, class: "form-select" %>
|
||||
<%= select f, :refresh_frequency, @refresh_frequencies, class: "custom-select" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label" for="pipeline_id">Pipeline ID</label>
|
||||
<div class="col-sm-10">
|
||||
<%= select f, :pipeline_id, @pipelines, class: "form-select" %>
|
||||
<%= select f, :pipeline_id, @pipelines, class: "custom-select" %>
|
||||
</div>
|
||||
</div>
|
||||
<%= if @feed.pipeline_id do %>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<a href="<%= Routes.pipeline_path(@conn, :show, @feed.pipeline_id) %>" class="col-sm-2">View Pipeline</a>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<%= submit "Update Feed", class: "btn btn-primary" %>
|
||||
</div>
|
||||
|
|
|
@ -25,32 +25,19 @@
|
|||
</p>
|
||||
<% end %>
|
||||
|
||||
<table class="table table-striped item-table">
|
||||
<table class="table table-striped">
|
||||
<tbody>
|
||||
<%= for item <- @items do %>
|
||||
<tr <%= if item.read do %>class="item-read"<% end %>>
|
||||
<td>
|
||||
<a href="<%= Routes.item_path(@conn, :show, item.id) %>"><%= item.title || "(Untitled)" %></a>
|
||||
</td>
|
||||
<td class="date">
|
||||
<td>
|
||||
<%= if item.date do %>
|
||||
<% {:ok, date} = Timex.format(item.date, "{YYYY}-{0M}-{0D} {0h12}:{m} {AM}") %>
|
||||
<%= date %>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="py-0 align-middle">
|
||||
<%= if item.read do %>
|
||||
<%= form_tag Routes.item_path(@conn, :unread, item.id), method: :post do %>
|
||||
<input type="hidden" name="redirect" value="<%= current_path(@conn) %>">
|
||||
<%= submit "Unread", class: "btn btn-sm btn-secondary" %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= form_tag Routes.item_path(@conn, :read, item.id), method: :post do %>
|
||||
<input type="hidden" name="redirect" value="<%= current_path(@conn) %>">
|
||||
<%= submit "Read", class: "btn btn-sm btn-secondary" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<%= if @state do %>
|
||||
<input type="hidden" name="state" value="<%= @state %>">
|
||||
<% end %>
|
||||
<div>
|
||||
<div class="form-group">
|
||||
<%= submit "Grant access", class: "btn btn-primary" %>
|
||||
<p>To reject the request, close this page.</p>
|
||||
</div>
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
<h1>Edit Group</h1>
|
||||
|
||||
<%= form_for @changeset, Routes.group_path(@conn, :update, @group.id), fn f -> %>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label" for="title">Title</label>
|
||||
<div class="col-sm-10">
|
||||
<%= text_input f, :title, class: "form-control" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<div class="col-sm-10">
|
||||
<%= submit "Update Group", class: "btn btn-primary" %>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<%= submit "Update Group", class: "btn btn-primary" %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<h1>New Group</h1>
|
||||
|
||||
<%= form_for @changeset, Routes.group_path(@conn, :create), fn form -> %>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<label for="title" class="col-sm-2 col-form-label">Title</label>
|
||||
<div class="col-sm-10">
|
||||
<%= text_input form, :title, placeholder: "My New Group", class: "form-control" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<%= submit "Create Group", class: "btn btn-primary" %>
|
||||
</div>
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
</p>
|
||||
<% end %>
|
||||
|
||||
<table class="table table-striped item-table">
|
||||
<table class="table table-striped">
|
||||
<tbody>
|
||||
<%= for item <- @items do %>
|
||||
<tr <%= if item.read do %>class="item-read"<% end %>>
|
||||
|
@ -15,33 +15,20 @@
|
|||
<%= item.title || "(Untitled)" %>
|
||||
</a>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<a href="<%= Routes.feed_path(@conn, :show, item.feed.id) %>" style="white-space: nowrap;">
|
||||
<td>
|
||||
<a href="<%= Routes.feed_path(@conn, :show, item.feed.id) %>">
|
||||
<%= if item.feed.favicon do %>
|
||||
<img src="<%= item.feed.favicon %>" alt="<%= item.feed.title %> favicon" class="favicon">
|
||||
<% end %>
|
||||
<%= item.feed.title || "(Untitled)" %>
|
||||
</a>
|
||||
</td>
|
||||
<td class="date align-middle">
|
||||
<td>
|
||||
<%= if item.date do %>
|
||||
<% {:ok, date} = Timex.format(item.date, "{YYYY}-{0M}-{0D} {0h12}:{m} {AM}") %>
|
||||
<%= date %>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="py-0 align-middle">
|
||||
<%= if item.read do %>
|
||||
<%= form_tag Routes.item_path(@conn, :unread, item.id), method: :post do %>
|
||||
<input type="hidden" name="redirect" value="<%= current_path(@conn) %>">
|
||||
<%= submit "Unread", class: "btn btn-sm btn-secondary" %>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<%= form_tag Routes.item_path(@conn, :read, item.id), method: :post do %>
|
||||
<input type="hidden" name="redirect" value="<%= current_path(@conn) %>">
|
||||
<%= submit "Read", class: "btn btn-sm btn-secondary" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
|
|
|
@ -16,10 +16,6 @@
|
|||
<% end %>
|
||||
</div>
|
||||
|
||||
<article class="item-content mt-4">
|
||||
<%= if @item.content_type in [nil, "text/html"] do %>
|
||||
<%= raw(@item.content) %>
|
||||
<% else %>
|
||||
<pre class="raw-content"><%= raw(@item.content) %></pre>
|
||||
<% end %>
|
||||
<article class="mt-4">
|
||||
<%= raw(@item.content) %>
|
||||
</article>
|
||||
|
|
|
@ -5,14 +5,15 @@
|
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
|
||||
|
||||
<title><%= title(assigns) %></title>
|
||||
<title>Frenzy</title>
|
||||
|
||||
<%= csrf_meta_tag() %>
|
||||
|
||||
<link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/assets/app.css") %>"/>
|
||||
<script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/assets/app.js") %>"></script>
|
||||
<link phx-track-static rel="stylesheet" href="<%= Routes.static_path(@conn, "/css/app.css") %>"/>
|
||||
<script defer phx-track-static type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<main class="main mt-4" role="main">
|
||||
<div class="container">
|
||||
<label class="sidebar-toggle" for="show-sidebar">
|
||||
|
@ -48,7 +49,7 @@
|
|||
<li class="nav-item">
|
||||
<details open="">
|
||||
<summary>
|
||||
<a href="<%= Routes.group_path(@conn, :show, group.id) %>" class="nav-link"><%= group.title %></a>
|
||||
<a href="<%= Routes.group_path(@conn, :show, group.id) %>"><%= group.title %></a>
|
||||
</summary>
|
||||
<ul class="nav flex-column">
|
||||
<%= for feed <- group.feeds do %>
|
||||
|
@ -85,5 +86,7 @@
|
|||
<div class="col sidebar-background"><label for="show-sidebar"></label></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="<%= Routes.static_path(@conn, "/js/app.js") %>"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
<h1>Login</h1>
|
||||
|
||||
<%= form_tag Routes.login_path(@conn, :login_post), method: :post do %>
|
||||
<div class="row mb-2">
|
||||
<%= if @continue do %>
|
||||
<input type="hidden" name="continue" value="<%= @continue %>">
|
||||
<% end %>
|
||||
<div class="form-group row">
|
||||
<label for="username" class="col-sm-2 col-form-label">Username</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" name="username" id="username" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<label for="password" class="col-sm-2 col-form-label">Password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" name="password" id="password" class="form-control">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<%= submit "Log In", class: "btn btn-primary" %>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%= if @oidc_enabled? do %>
|
||||
<a href="<%= Routes.login_path(@conn, :ueberauth_request, "oidc") %>">Log In with OIDC</a>
|
||||
<% end %>
|
||||
|
|
|
@ -3,16 +3,16 @@
|
|||
<a href="<%= Routes.pipeline_path(@conn, :edit, @pipeline.id) %>" class="btn btn-primary">Edit UI</a>
|
||||
|
||||
<%= form_tag Routes.pipeline_path(@conn, :update, @pipeline.id), method: :put do %>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label" for="name">Name</label>
|
||||
<div class="col-sm-10">
|
||||
<%= text_input :pipeline, :name, value: @name, placeholder: "My New Pipeline", class: "form-control" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<%= textarea :pipeline, :stages, value: @stages_json, class: "form-control", rows: 15, style: "font-family: monospace;" %>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<%= submit "Edit Pipeline", class: "btn btn-primary" %>
|
||||
</div>
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
<%= form_for @changeset, Routes.pipeline_path(@conn, :create), fn form -> %>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<label class="col-sm-2 col-form-label" for="name">Name</label>
|
||||
<div class="col-sm-10">
|
||||
<%= text_input form, :name, placeholder: "My New Pipeline", class: "form-control" %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<%= textarea form, :stages, class: "form-control", rows: 15, style: "font-family: monospace;" %>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="form-group row">
|
||||
<div class="col-sm-10">
|
||||
<%= submit "Create Pipeline", class: "btn btn-primary" %>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
defmodule FrenzyWeb.AccountView do
|
||||
use FrenzyWeb, :view
|
||||
|
||||
def title(:show, _) do
|
||||
"Account Settings"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
defmodule FrenzyWeb.FeedView do
|
||||
use FrenzyWeb, :view
|
||||
alias Frenzy.Feed
|
||||
import Phoenix.Controller, only: [current_path: 1]
|
||||
|
||||
@spec feed_site_url(feed :: Feed.t()) :: String.t()
|
||||
def feed_site_url(%Feed{site_url: site_url}) when is_binary(site_url) do
|
||||
|
@ -11,12 +10,4 @@ defmodule FrenzyWeb.FeedView do
|
|||
def feed_site_url(%Feed{feed_url: feed_url}) do
|
||||
URI.merge(feed_url, "/") |> to_string()
|
||||
end
|
||||
|
||||
def title(:show, %{feed: %Feed{title: title}}) do
|
||||
title
|
||||
end
|
||||
|
||||
def title(:edit, %{feed: %Feed{title: title}}) do
|
||||
"Edit #{title}"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,20 +1,3 @@
|
|||
defmodule FrenzyWeb.GroupView do
|
||||
use FrenzyWeb, :view
|
||||
import Phoenix.Controller, only: [current_path: 1]
|
||||
|
||||
def title(:index, _) do
|
||||
"Groups"
|
||||
end
|
||||
|
||||
def title(:show, %{group: %Frenzy.Group{title: title}}) do
|
||||
title
|
||||
end
|
||||
|
||||
def title(:edit, %{group: %Frenzy.Group{title: title}}) do
|
||||
"Edit #{title}"
|
||||
end
|
||||
|
||||
def title(:read, %{group: %Frenzy.Group{title: title}}) do
|
||||
"Read #{title}"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
defmodule FrenzyWeb.ItemView do
|
||||
use FrenzyWeb, :view
|
||||
|
||||
def title(:show, %{item: %Frenzy.Item{title: title}, feed: %Frenzy.Feed{title: feed}}) do
|
||||
"#{title} | #{feed}"
|
||||
end
|
||||
end
|
|
@ -6,30 +6,4 @@ defmodule FrenzyWeb.LayoutView do
|
|||
def user_groups(user) do
|
||||
Repo.all(from g in Group, where: g.user_id == ^user.id, preload: [:feeds])
|
||||
end
|
||||
|
||||
def title(%{live_module: module} = assigns) do
|
||||
try do
|
||||
"#{module.title(assigns)} | Frenzy"
|
||||
rescue
|
||||
_ ->
|
||||
"Frenzy"
|
||||
end
|
||||
end
|
||||
|
||||
def title(assigns) do
|
||||
vm = Phoenix.Controller.view_module(assigns[:conn])
|
||||
|
||||
if function_exported?(vm, :title, 2) do
|
||||
action = Phoenix.Controller.action_name(assigns[:conn])
|
||||
|
||||
try do
|
||||
"#{vm.title(action, assigns)} | Frenzy"
|
||||
rescue
|
||||
_ ->
|
||||
"Frenzy"
|
||||
end
|
||||
else
|
||||
"Frenzy"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
defmodule FrenzyWeb.LoginView do
|
||||
use FrenzyWeb, :view
|
||||
|
||||
def title(_, _) do
|
||||
"Login"
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,19 +1,3 @@
|
|||
defmodule FrenzyWeb.PipelineView do
|
||||
use FrenzyWeb, :view
|
||||
|
||||
def title(:index, _) do
|
||||
"Pipelines"
|
||||
end
|
||||
|
||||
def title(:new, _) do
|
||||
"Add Pipeline"
|
||||
end
|
||||
|
||||
def title(:show, %{pipeline: %Frenzy.Pipeline{name: name}}) do
|
||||
name
|
||||
end
|
||||
|
||||
def title(:edit, %{pipeline: %Frenzy.Pipeline{name: name}}) do
|
||||
"Edit #{name}"
|
||||
end
|
||||
end
|
||||
|
|
35
mix.exs
35
mix.exs
|
@ -20,7 +20,7 @@ defmodule Frenzy.MixProject do
|
|||
def application do
|
||||
[
|
||||
mod: {Frenzy.Application, []},
|
||||
extra_applications: [:logger, :runtime_tools, :readability]
|
||||
extra_applications: [:logger, :runtime_tools, :httpoison, :readability]
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -33,33 +33,28 @@ defmodule Frenzy.MixProject do
|
|||
# Type `mix help deps` for examples and options.
|
||||
defp deps do
|
||||
[
|
||||
{:phoenix, "~> 1.6.11"},
|
||||
{:phoenix, "~> 1.5.2"},
|
||||
{:phoenix_pubsub, "~> 2.0"},
|
||||
{:phoenix_ecto, "~> 4.4"},
|
||||
{:ecto_sql, "~> 3.6"},
|
||||
{:phoenix_ecto, "~> 4.1"},
|
||||
{:ecto_sql, "~> 3.4"},
|
||||
{:postgrex, ">= 0.0.0"},
|
||||
{:phoenix_html, "~> 3.0"},
|
||||
{:phoenix_html, "~> 2.11"},
|
||||
{:phoenix_live_reload, "~> 1.2", only: :dev},
|
||||
{:gettext, "~> 0.11"},
|
||||
{:jason, "~> 1.2"},
|
||||
{:plug_cowboy, "~> 2.5"},
|
||||
{:castore, "~> 0.1.12"},
|
||||
{:mint, "~> 1.4"},
|
||||
{:tesla, "~> 1.4.0"},
|
||||
{:jason, "~> 1.0"},
|
||||
{:plug_cowboy, "~> 2.1"},
|
||||
{:httpoison, "~> 1.6.2"},
|
||||
{:hackney, "~> 1.16"},
|
||||
{:feed_parser,
|
||||
git: "https://git.shadowfacts.net/shadowfacts/feed_parser.git", branch: "master"},
|
||||
{:timex, "~> 3.6"},
|
||||
{:readability,
|
||||
git: "https://git.shadowfacts.net/shadowfacts/readability.git", branch: "master"},
|
||||
{:readability, git: "https://github.com/shadowfacts/readability.git", branch: "master"},
|
||||
{:bcrypt_elixir, "~> 2.0"},
|
||||
{:dialyxir, "~> 1.0.0-rc.6", only: :dev, runtime: false},
|
||||
{:xml_builder, "~> 2.1.1"},
|
||||
{:floki, "~> 0.30"},
|
||||
{:phoenix_live_view, "~> 0.17.5"},
|
||||
{:gemini, git: "https://git.shadowfacts.net/shadowfacts/gemini-ex.git", branch: "main"},
|
||||
{:sentry, "~> 8.0"},
|
||||
{:ueberauth, "~> 0.10"},
|
||||
{:ueberauth_oidc, "~> 0.1"}
|
||||
{:floki, "~> 0.23"},
|
||||
{:phoenix_live_view,
|
||||
git: "https://github.com/phoenixframework/phoenix_live_view", branch: "master"}
|
||||
]
|
||||
end
|
||||
|
||||
|
@ -73,9 +68,7 @@ defmodule Frenzy.MixProject do
|
|||
[
|
||||
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
|
||||
"ecto.reset": ["ecto.drop", "ecto.setup"],
|
||||
test: ["ecto.create --quiet", "ecto.migrate", "test"],
|
||||
"assets.deploy": ["cmd --cd assets node build.js --deploy", "phx.digest"],
|
||||
sentry_recompile: ["deps.compile sentry --force", "compile"]
|
||||
test: ["ecto.create --quiet", "ecto.migrate", "test"]
|
||||
]
|
||||
end
|
||||
end
|
||||
|
|
78
mix.lock
78
mix.lock
|
@ -1,66 +1,52 @@
|
|||
%{
|
||||
"basic_auth": {:hex, :basic_auth, "2.2.4", "d8c748237870dd1df3bc5c0f1ab4f1fad6270c75472d7e62b19302ec59e92a79", [:mix], [{:plug, "~> 0.14 or ~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
|
||||
"bcrypt_elixir": {:hex, :bcrypt_elixir, "2.0.1", "1061e2114aaac554c12e5c1e4608bf4aadaca839f30d1b85224272facd5e6427", [:make, :mix], [{:comeonin, "~> 5.1", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.4", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "64f174c76ea5edfcc471dfb7762280a20e29fe446baa02dc75c7d14251581e93"},
|
||||
"castore": {:hex, :castore, "0.1.18", "deb5b9ab02400561b6f5708f3e7660fc35ca2d51bfc6a940d2f513f89c2975fc", [:mix], [], "hexpm", "61bbaf6452b782ef80b33cdb45701afbcf0a918a45ebe7e73f1130d661e66a06"},
|
||||
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
|
||||
"certifi": {:hex, :certifi, "2.5.2", "b7cfeae9d2ed395695dd8201c57a2d019c0c43ecaf8b8bcb9320b40d6662f340", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm", "3b3b5f36493004ac3455966991eaf6e768ce9884693d9968055aeeeb1e575040"},
|
||||
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
|
||||
"comeonin": {:hex, :comeonin, "5.1.1", "0abd6bae41acc01c369bb3eafe46399f301bf4e1bacebafdb89252bbb8a1a32d", [:mix], [], "hexpm", "b77aef9eb7ec7a4c01cc3d0683332796052ab71067d858d5dacde967427de0a3"},
|
||||
"connection": {:hex, :connection, "1.1.0", "ff2a49c4b75b6fb3e674bfc5536451607270aac754ffd1bdfe175abe4a6d7a68", [:mix], [], "hexpm", "722c1eb0a418fbe91ba7bd59a47e28008a189d47e37e0e7bb85585a016b2869c"},
|
||||
"cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"},
|
||||
"cowboy_telemetry": {:hex, :cowboy_telemetry, "0.3.1", "ebd1a1d7aff97f27c66654e78ece187abdc646992714164380d8a041eda16754", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3a6efd3366130eab84ca372cbd4a7d3c3a97bdfcfb4911233b035d117063f0af"},
|
||||
"cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"},
|
||||
"db_connection": {:hex, :db_connection, "2.5.0", "bb6d4f30d35ded97b29fe80d8bd6f928a1912ca1ff110831edcd238a1973652c", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c92d5ba26cd69ead1ff7582dbb860adeedfff39774105a4f1c92cbb654b55aa2"},
|
||||
"decimal": {:hex, :decimal, "2.1.1", "5611dca5d4b2c3dd497dec8f68751f1f1a54755e8ed2a966c2633cf885973ad6", [:mix], [], "hexpm", "53cfe5f497ed0e7771ae1a475575603d77425099ba5faef9394932b35020ffcc"},
|
||||
"connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm", "4a0850c9be22a43af9920a71ab17c051f5f7d45c209e40269a1938832510e4d9"},
|
||||
"cowboy": {:hex, :cowboy, "2.8.0", "f3dc62e35797ecd9ac1b50db74611193c29815401e53bac9a5c0577bd7bc667d", [:rebar3], [{:cowlib, "~> 2.9.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "4643e4fba74ac96d4d152c75803de6fad0b3fa5df354c71afdd6cbeeb15fac8a"},
|
||||
"cowlib": {:hex, :cowlib, "2.9.1", "61a6c7c50cf07fdd24b2f45b89500bb93b6686579b069a89f88cb211e1125c78", [:rebar3], [], "hexpm", "e4175dc240a70d996156160891e1c62238ede1729e45740bdd38064dad476170"},
|
||||
"db_connection": {:hex, :db_connection, "2.2.2", "3bbca41b199e1598245b716248964926303b5d4609ff065125ce98bcd368939e", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm", "642af240d8a8affb93b4ba5a6fcd2bbcbdc327e1a524b825d383711536f8070c"},
|
||||
"decimal": {:hex, :decimal, "1.8.1", "a4ef3f5f3428bdbc0d35374029ffcf4ede8533536fa79896dd450168d9acdf3c", [:mix], [], "hexpm", "3cb154b00225ac687f6cbd4acc4b7960027c757a5152b369923ead9ddbca7aec"},
|
||||
"dialyxir": {:hex, :dialyxir, "1.0.0-rc.6", "78e97d9c0ff1b5521dd68041193891aebebce52fc3b93463c0a6806874557d7d", [:mix], [{:erlex, "~> 0.2.1", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "49496d63267bc1a4614ffd5f67c45d9fc3ea62701a6797975bc98bc156d2763f"},
|
||||
"ecto": {:hex, :ecto, "3.10.3", "eb2ae2eecd210b4eb8bece1217b297ad4ff824b4384c0e3fdd28aaf96edd6135", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "44bec74e2364d491d70f7e42cd0d690922659d329f6465e89feb8a34e8cd3433"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.10.1", "6ea6b3036a0b0ca94c2a02613fd9f742614b5cfe494c41af2e6571bb034dd94c", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 0.17.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "f6a25bdbbd695f12c8171eaff0851fa4c8e72eec1e98c7364402dda9ce11c56b"},
|
||||
"ecto": {:hex, :ecto, "3.4.4", "a2c881e80dc756d648197ae0d936216c0308370332c5e77a2325a10293eef845", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cc4bd3ad62abc3b21fb629f0f7a3dab23a192fca837d257dd08449fba7373561"},
|
||||
"ecto_sql": {:hex, :ecto_sql, "3.4.4", "d28bac2d420f708993baed522054870086fd45016a9d09bb2cd521b9c48d32ea", [:mix], [{:db_connection, "~> 2.2", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.4.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.3.0 or ~> 0.4.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.0", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "edb49af715dd72f213b66adfd0f668a43c17ed510b5d9ac7528569b23af57fe8"},
|
||||
"elixir_make": {:hex, :elixir_make, "0.5.2", "96a28c79f5b8d34879cd95ebc04d2a0d678cfbbd3e74c43cb63a76adf0ee8054", [:mix], [], "hexpm", "382eeea8e02dfe6c468f6729b6cf20fe5b14390671d38c7363e59621c7ab4efc"},
|
||||
"erlex": {:hex, :erlex, "0.2.4", "23791959df45fe8f01f388c6f7eb733cc361668cbeedd801bf491c55a029917b", [:mix], [], "hexpm", "4a12ebc7cd8f24f2d0fce93d279fa34eb5068e0e885bb841d558c4d83c52c439"},
|
||||
"esbuild": {:hex, :esbuild, "0.5.0", "d5bb08ff049d7880ee3609ed5c4b864bd2f46445ea40b16b4acead724fb4c4a3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "f183a0b332d963c4cfaf585477695ea59eef9a6f2204fdd0efa00e099694ffe5"},
|
||||
"feed_parser": {:git, "https://git.shadowfacts.net/shadowfacts/feed_parser.git", "943f4fdea7445547a9bc7aa33893a0442be36c68", [branch: "master"]},
|
||||
"feed_parser": {:git, "https://git.shadowfacts.net/shadowfacts/feed_parser.git", "8c42d4587328698e8d29d2ad562e478abb146f75", [branch: "master"]},
|
||||
"fiet": {:git, "https://github.com/shadowfacts/fiet.git", "bf117bc30a6355a189d05a562127cfaf9e0187ae", [branch: "master"]},
|
||||
"file_system": {:hex, :file_system, "0.2.6", "fd4dc3af89b9ab1dc8ccbcc214a0e60c41f34be251d9307920748a14bf41f1d3", [:mix], [], "hexpm", "0d50da6b04c58e101a3793b1600f9a03b86e3a8057b192ac1766013d35706fa6"},
|
||||
"floki": {:hex, :floki, "0.33.1", "f20f1eb471e726342b45ccb68edb9486729e7df94da403936ea94a794f072781", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "461035fd125f13fdf30f243c85a0b1e50afbec876cbf1ceefe6fddd2e6d712c6"},
|
||||
"gemini": {:git, "https://git.shadowfacts.net/shadowfacts/gemini-ex.git", "cc6f4e04374d163438faae1b12b54809bdfb7f4d", [branch: "main"]},
|
||||
"floki": {:hex, :floki, "0.23.0", "956ab6dba828c96e732454809fb0bd8d43ce0979b75f34de6322e73d4c917829", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "e680b5ef0b61ce02faa7137db8d1714903a5552be4c89fb57293b8770e7f49c2"},
|
||||
"gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm", "e0b8598e802676c81e66b061a2148c37c03886b24a3ca86a1f98ed40693b94b3"},
|
||||
"hackney": {:hex, :hackney, "1.18.1", "f48bf88f521f2a229fc7bae88cf4f85adc9cd9bcf23b5dc8eb6a1788c662c4f6", [:rebar3], [{:certifi, "~>2.9.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~>6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~>1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~>1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a4ecdaff44297e9b5894ae499e9a070ea1888c84afdd1fd9b7b2bc384950128e"},
|
||||
"hpax": {:hex, :hpax, "0.1.2", "09a75600d9d8bbd064cdd741f21fc06fc1f4cf3d0fcc335e5aa19be1a7235c84", [:mix], [], "hexpm", "2c87843d5a23f5f16748ebe77969880e29809580efdaccd615cd3bed628a8c13"},
|
||||
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
|
||||
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
|
||||
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
|
||||
"jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"},
|
||||
"jose": {:hex, :jose, "1.11.5", "3bc2d75ffa5e2c941ca93e5696b54978323191988eb8d225c2e663ddfefd515e", [:mix, :rebar3], [], "hexpm", "dcd3b215bafe02ea7c5b23dafd3eb8062a5cd8f2d904fd9caa323d37034ab384"},
|
||||
"hackney": {:hex, :hackney, "1.16.0", "5096ac8e823e3a441477b2d187e30dd3fff1a82991a806b2003845ce72ce2d84", [:rebar3], [{:certifi, "2.5.2", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.1", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.3.0", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.6", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm", "3bf0bebbd5d3092a3543b783bf065165fa5d3ad4b899b836810e513064134e18"},
|
||||
"html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm", "3e3d7156a272950373ce5a4018b1490bea26676f8d6a7d409f6fac8568b8cb9a"},
|
||||
"httpoison": {:hex, :httpoison, "1.6.2", "ace7c8d3a361cebccbed19c283c349b3d26991eff73a1eaaa8abae2e3c8089b6", [:mix], [{:hackney, "~> 1.15 and >= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "aa2c74bd271af34239a3948779612f87df2422c2fdcfdbcec28d9c105f0773fe"},
|
||||
"idna": {:hex, :idna, "6.0.1", "1d038fb2e7668ce41fbf681d2c45902e52b3cb9e9c77b55334353b222c2ee50c", [:rebar3], [{:unicode_util_compat, "0.5.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "a02c8a1c4fd601215bb0b0324c8a6986749f807ce35f25449ec9e69758708122"},
|
||||
"jason": {:hex, :jason, "1.2.1", "12b22825e22f468c02eb3e4b9985f3d0cb8dc40b9bd704730efa11abd2708c44", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b659b8571deedf60f79c5a608e15414085fa141344e2716fbd6988a084b5f993"},
|
||||
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
|
||||
"mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
|
||||
"mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm", "6cbe761d6a0ca5a31a0931bf4c63204bceb64538e664a8ecf784a9a6f3b875f1"},
|
||||
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
|
||||
"mint": {:hex, :mint, "1.4.2", "50330223429a6e1260b2ca5415f69b0ab086141bc76dc2fbf34d7c389a6675b2", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "ce75a5bbcc59b4d7d8d70f8b2fc284b1751ffb35c7b6a6302b5192f8ab4ddd80"},
|
||||
"mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"},
|
||||
"openid_connect": {:hex, :openid_connect, "0.2.2", "c05055363330deab39ffd89e609db6b37752f255a93802006d83b45596189c0b", [:mix], [{:httpoison, "~> 1.2", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm", "735769b6d592124b58edd0582554ce638524c0214cd783d8903d33357d74cc13"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
|
||||
"phoenix": {:hex, :phoenix, "1.6.12", "f8f8ac077600f84419806dd53114b2e77aedde7a502e74181a7d886355aa0643", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2d6cf5583c9c20f7103c40e6014ef802d96553b8e5d6585ad6e627bd5ddb0d12"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"},
|
||||
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm", "17ef63abde837ad30680ea7f857dd9e7ced9476cdd7b0394432af4bfc241b960"},
|
||||
"phoenix": {:hex, :phoenix, "1.5.3", "bfe0404e48ea03dfe17f141eff34e1e058a23f15f109885bbdcf62be303b49ff", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_html, "~> 2.13", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.1.2 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8e16febeb9640d8b33895a691a56481464b82836d338bb3a23125cd7b6157c25"},
|
||||
"phoenix_ecto": {:hex, :phoenix_ecto, "4.1.0", "a044d0756d0464c5a541b4a0bf4bcaf89bffcaf92468862408290682c73ae50d", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "c5e666a341ff104d0399d8f0e4ff094559b2fde13a5985d4cb5023b2c2ac558b"},
|
||||
"phoenix_html": {:hex, :phoenix_html, "2.14.2", "b8a3899a72050f3f48a36430da507dd99caf0ac2d06c77529b1646964f3d563e", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "58061c8dfd25da5df1ea0ca47c972f161beb6c875cd293917045b92ffe1bf617"},
|
||||
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.2.0", "3bb31a9fbd40ffe8652e60c8660dffd72dd231efcdf49b744fb75b9ef7db5dd2", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "b61c87a23fabcd85ff369fb9c041d9c01787d210322749026f56a69a914b7503"},
|
||||
"phoenix_live_view": {:hex, :phoenix_live_view, "0.17.11", "205f6aa5405648c76f2abcd57716f42fc07d8f21dd8ea7b262dd12b324b50c95", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7177791944b7f90ed18f5935a6a5f07f760b36f7b3bdfb9d28c57440a3c43f99"},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"},
|
||||
"phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"},
|
||||
"plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"},
|
||||
"phoenix_live_view": {:git, "https://github.com/phoenixframework/phoenix_live_view", "a622fed5e496561eb05a7fce5423d238c2597142", [branch: "master"]},
|
||||
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
|
||||
"plug": {:hex, :plug, "1.10.2", "0079345cfdf9e17da3858b83eb46bc54beb91554c587b96438f55c1477af5a86", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7898d0eb4767efb3b925fd7f9d1870d15e66e9c33b89c58d8d2ad89aa75ab3c1"},
|
||||
"plug_cowboy": {:hex, :plug_cowboy, "2.2.2", "7a09aa5d10e79b92d332a288f21cc49406b1b994cbda0fde76160e7f4cc890ea", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e82364b29311dbad3753d588febd7e5ef05062cd6697d8c231e0e007adab3727"},
|
||||
"plug_crypto": {:hex, :plug_crypto, "1.1.2", "bdd187572cc26dbd95b87136290425f2b580a116d3fb1f564216918c9730d227", [:mix], [], "hexpm", "6b8b608f895b6ffcfad49c37c7883e8df98ae19c6a28113b02aa1e9c5b22d6b5"},
|
||||
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm", "fec8660eb7733ee4117b85f55799fd3833eb769a6df71ccf8903e8dc5447cfce"},
|
||||
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
|
||||
"postgrex": {:hex, :postgrex, "0.17.2", "a3ec9e3239d9b33f1e5841565c4eb200055c52cc0757a22b63ca2d529bbe764c", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:table, "~> 0.1.0", [hex: :table, repo: "hexpm", optional: true]}], "hexpm", "80a918a9e9531d39f7bd70621422f3ebc93c01618c645f2d91306f50041ed90c"},
|
||||
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
|
||||
"readability": {:git, "https://git.shadowfacts.net/shadowfacts/readability.git", "75404b197d67e118a6575ee9b39a9ae2ac3c2dcc", [branch: "master"]},
|
||||
"postgrex": {:hex, :postgrex, "0.15.5", "aec40306a622d459b01bff890fa42f1430dac61593b122754144ad9033a2152f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "ed90c81e1525f65a2ba2279dbcebf030d6d13328daa2f8088b9661eb9143af7f"},
|
||||
"ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm", "451d8527787df716d99dc36162fca05934915db0b6141bbdac2ea8d3c7afc7d7"},
|
||||
"readability": {:git, "https://github.com/shadowfacts/readability.git", "71fa17caaf8103ef213e2c7dde4b447a48669122", [branch: "master"]},
|
||||
"saxy": {:hex, :saxy, "0.6.0", "cdb2f2fcd8133d1f3f8b0cf6a131ee1ca348dca613de266e9a239db850c4a093", [:mix], [], "hexpm"},
|
||||
"sentry": {:hex, :sentry, "8.0.5", "5ca922b9238a50c7258b52f47364b2d545beda5e436c7a43965b34577f1ef61f", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.6", [hex: :plug, repo: "hexpm", optional: true]}, {:plug_cowboy, "~> 2.3", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm", "4972839fdbf52e886d7b3e694c8adf421f764f2fa79036b88fb4742049bd4b7c"},
|
||||
"socket": {:hex, :socket, "0.3.13", "98a2ab20ce17f95fb512c5cadddba32b57273e0d2dba2d2e5f976c5969d0c632", [:mix], [], "hexpm", "f82ea9833ef49dde272e6568ab8aac657a636acb4cf44a7de8a935acb8957c2e"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
"telemetry": {:hex, :telemetry, "0.4.3", "a06428a514bdbc63293cd9a6263aad00ddeb66f608163bdec7c8995784080818", [:rebar3], [], "hexpm", "eb72b8365ffda5bed68a620d1da88525e326cb82a75ee61354fc24b844768041"},
|
||||
"tesla": {:hex, :tesla, "1.4.4", "bb89aa0c9745190930366f6a2ac612cdf2d0e4d7fff449861baa7875afd797b2", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.3", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "d5503a49f9dec1b287567ea8712d085947e247cb11b06bc54adb05bfde466457"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.6", "cf344f5692c82d2cd7554f5ec8fd961548d4fd09e7d22f5b62482e5aeaebd4b0", [:make, :mix, :rebar3], [], "hexpm", "bdb0d2471f453c88ff3908e7686f86f9be327d065cc1ec16fa4540197ea04680"},
|
||||
"telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm", "4738382e36a0a9a2b6e25d67c960e40e1a2c95560b9f936d8e29de8cd858480f"},
|
||||
"timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "f354efb2400dd7a80fd9eb6c8419068c4f632da4ac47f3d8822d6e33f08bc852"},
|
||||
"tzdata": {:hex, :tzdata, "1.0.1", "f6027a331af7d837471248e62733c6ebee86a72e57c613aa071ebb1f750fc71a", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "cf1345dfbce6acdfd4e23cbb36e96e53d1981bc89181cd0b936f4f398f4c0b78"},
|
||||
"ueberauth": {:hex, :ueberauth, "0.10.5", "806adb703df87e55b5615cf365e809f84c20c68aa8c08ff8a416a5a6644c4b02", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "3efd1f31d490a125c7ed453b926f7c31d78b97b8a854c755f5c40064bf3ac9e1"},
|
||||
"ueberauth_oidc": {:hex, :ueberauth_oidc, "0.1.7", "d610cbe5ef09881dff52126906b130307adcf02791ce158c1847fd50949b283a", [:mix], [{:httpoison, "~> 1.8", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.11", [hex: :jose, repo: "hexpm", optional: false]}, {:openid_connect, "~> 0.2.2", [hex: :openid_connect, repo: "hexpm", optional: false]}, {:plug, "~> 1.11", [hex: :plug, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "34d612f66a5425af4142d6c9dece887c60188c31e1dc113e5ee8cecdc6c5e8a9"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
|
||||
"unicode_util_compat": {:hex, :unicode_util_compat, "0.5.0", "8516502659002cec19e244ebd90d312183064be95025a319a6c7e89f4bccd65b", [:rebar3], [], "hexpm", "d48d002e15f5cc105a696cf2f1bbb3fc72b4b770a184d8420c8db20da2674b38"},
|
||||
"xml_builder": {:hex, :xml_builder, "2.1.1", "2d6d665f09cf1319e3e1c46035755271b414d99ad8615d0bd6f337623e0c885b", [:mix], [], "hexpm", "214c16caa77e66bf0c6b74099a7059ee00de8fd07728d2a3dc32afe344a7452b"},
|
||||
}
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
defmodule Frenzy.Repo.Migrations.ItemAddContentType do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:items) do
|
||||
add :content_type, :string, default: nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,7 +0,0 @@
|
|||
defmodule Frenzy.Repo.Migrations.CreatItemUniqueIndex do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
create unique_index(:items, [:feed_id, :guid], name: :items_feed_guid_index)
|
||||
end
|
||||
end
|
|
@ -1,9 +0,0 @@
|
|||
defmodule Frenzy.Repo.Migrations.ChangeItemTitleText do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:items) do
|
||||
modify :title, :text
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,11 +0,0 @@
|
|||
defmodule Frenzy.Repo.Migrations.ChangeItemFieldsToText do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:items) do
|
||||
modify :guid, :text
|
||||
modify :url, :text
|
||||
modify :creator, :text
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,9 +0,0 @@
|
|||
defmodule Frenzy.Repo.Migrations.UserAddOidcSubject do
|
||||
use Ecto.Migration
|
||||
|
||||
def change do
|
||||
alter table(:users) do
|
||||
add :oidc_subject, :string, default: nil
|
||||
end
|
||||
end
|
||||
end
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,543 @@
|
|||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
|
||||
<!--
|
||||
2014-7-1: Created.
|
||||
-->
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<metadata>
|
||||
Created by FontForge 20120731 at Tue Jul 1 20:39:22 2014
|
||||
By P.J. Onori
|
||||
Created by P.J. Onori with FontForge 2.0 (http://fontforge.sf.net)
|
||||
</metadata>
|
||||
<defs>
|
||||
<font id="open-iconic" horiz-adv-x="800" >
|
||||
<font-face
|
||||
font-family="Icons"
|
||||
font-weight="400"
|
||||
font-stretch="normal"
|
||||
units-per-em="800"
|
||||
panose-1="2 0 5 3 0 0 0 0 0 0"
|
||||
ascent="800"
|
||||
descent="0"
|
||||
bbox="-0.5 -101 802 800.126"
|
||||
underline-thickness="50"
|
||||
underline-position="-100"
|
||||
unicode-range="U+E000-E0DE"
|
||||
/>
|
||||
<missing-glyph />
|
||||
<glyph glyph-name="" unicode=""
|
||||
d="M300 700h500v-700h-500v100h400v500h-400v100zM400 500l200 -150l-200 -150v100h-400v100h400v100z" />
|
||||
<glyph glyph-name="1" unicode=""
|
||||
d="M300 700h500v-700h-500v100h400v500h-400v100zM200 500v-100h400v-100h-400v-100l-200 150z" />
|
||||
<glyph glyph-name="2" unicode=""
|
||||
d="M350 700c193 0 350 -157 350 -350v-50h100l-200 -200l-200 200h100v50c0 138 -112 250 -250 250s-250 -112 -250 -250c0 193 157 350 350 350z" />
|
||||
<glyph glyph-name="3" unicode=""
|
||||
d="M450 700c193 0 350 -157 350 -350c0 138 -112 250 -250 250s-250 -112 -250 -250v-50h100l-200 -200l-200 200h100v50c0 193 157 350 350 350z" />
|
||||
<glyph glyph-name="4" unicode=""
|
||||
d="M0 700h800v-100h-800v100zM100 500h600v-100h-600v100zM0 300h800v-100h-800v100zM100 100h600v-100h-600v100z" />
|
||||
<glyph glyph-name="5" unicode=""
|
||||
d="M0 700h800v-100h-800v100zM0 500h600v-100h-600v100zM0 300h800v-100h-800v100zM0 100h600v-100h-600v100z" />
|
||||
<glyph glyph-name="6" unicode=""
|
||||
d="M0 700h800v-100h-800v100zM200 500h600v-100h-600v100zM0 300h800v-100h-800v100zM200 100h600v-100h-600v100z" />
|
||||
<glyph glyph-name="7" unicode=""
|
||||
d="M400 700c75 0 146 -23 206 -59l-75 -225l-322 234c57 31 122 50 191 50zM125 588l191 -138l-310 -222c-4 24 -6 47 -6 72c0 114 49 215 125 288zM688 575c69 -72 112 -168 112 -275c0 -35 -8 -68 -16 -100h-218zM216 253l112 -347c-128 23 -232 109 -287 222zM372 100
|
||||
h372c-64 -109 -177 -185 -310 -197z" />
|
||||
<glyph glyph-name="8" unicode="" horiz-adv-x="600"
|
||||
d="M200 800h100v-500h200l-247 -300l-253 300h200v500z" />
|
||||
<glyph glyph-name="9" unicode=""
|
||||
d="M400 800c221 0 400 -179 400 -400s-179 -400 -400 -400s-400 179 -400 400s179 400 400 400zM300 700v-300h-200l300 -300l300 300h-200v300h-200z" />
|
||||
<glyph glyph-name="a" unicode=""
|
||||
d="M400 800c221 0 400 -179 400 -400s-179 -400 -400 -400s-400 179 -400 400s179 400 400 400zM400 700l-300 -300l300 -300v200h300v200h-300v200z" />
|
||||
<glyph glyph-name="b" unicode=""
|
||||
d="M400 800c221 0 400 -179 400 -400s-179 -400 -400 -400s-400 179 -400 400s179 400 400 400zM400 700v-200h-300v-200h300v-200l300 300z" />
|
||||
<glyph glyph-name="c" unicode=""
|
||||
d="M400 800c221 0 400 -179 400 -400s-179 -400 -400 -400s-400 179 -400 400s179 400 400 400zM400 700l-300 -300h200v-300h200v300h200z" />
|
||||
<glyph glyph-name="d" unicode=""
|
||||
d="M300 600v-200h500v-100h-500v-200l-300 247z" />
|
||||
<glyph glyph-name="e" unicode=""
|
||||
d="M500 600l300 -247l-300 -253v200h-500v100h500v200z" />
|
||||
<glyph glyph-name="f" unicode="" horiz-adv-x="600"
|
||||
d="M200 800h200v-500h200l-297 -300l-303 300h200v500z" />
|
||||
<glyph glyph-name="10" unicode=""
|
||||
d="M300 700v-200h500v-200h-500v-200l-300 297z" />
|
||||
<glyph glyph-name="11" unicode=""
|
||||
d="M500 700l300 -297l-300 -303v200h-500v200h500v200z" />
|
||||
<glyph glyph-name="12" unicode="" horiz-adv-x="600"
|
||||
d="M297 800l303 -300h-200v-500h-200v500h-200z" />
|
||||
<glyph glyph-name="13" unicode="" horiz-adv-x="600"
|
||||
d="M247 800l253 -300h-200v-500h-100v500h-200z" />
|
||||
<glyph glyph-name="14" unicode=""
|
||||
d="M400 800h100v-800h-100v800zM200 700h100v-600h-100v600zM600 600h100v-400h-100v400zM0 500h100v-200h-100v200z" />
|
||||
<glyph glyph-name="15" unicode=""
|
||||
d="M116 600l72 -72c-54 -54 -88 -126 -88 -209s34 -159 88 -213l-72 -72c-72 72 -116 175 -116 285s44 209 116 281zM684 600c72 -72 116 -171 116 -281s-44 -213 -116 -285l-72 72c54 54 88 130 88 213s-34 155 -88 209zM259 460l69 -72c-18 -18 -28 -41 -28 -69
|
||||
s10 -54 28 -72l-69 -72c-36 36 -59 89 -59 144s23 105 59 141zM541 459c36 -36 59 -85 59 -140s-23 -108 -59 -144l-69 72c18 18 28 44 28 72s-10 51 -28 69z" />
|
||||
<glyph glyph-name="16" unicode="" horiz-adv-x="400"
|
||||
d="M200 800c110 0 200 -90 200 -200s-90 -200 -200 -200s-200 90 -200 200s90 200 200 200zM100 319c31 -11 65 -19 100 -19s68 8 100 19v-319l-100 100l-100 -100v319z" />
|
||||
<glyph glyph-name="17" unicode=""
|
||||
d="M400 800c220 0 400 -180 400 -400s-180 -400 -400 -400s-400 180 -400 400s180 400 400 400zM400 700c-166 0 -300 -134 -300 -300c0 -66 21 -126 56 -175l419 419c-49 35 -109 56 -175 56zM644 575l-419 -419c49 -35 109 -56 175 -56c166 0 300 134 300 300
|
||||
c0 66 -21 126 -56 175z" />
|
||||
<glyph glyph-name="18" unicode=""
|
||||
d="M0 700h100v-600h700v-100h-800v700zM500 700h200v-500h-200v500zM200 500h200v-300h-200v300z" />
|
||||
<glyph glyph-name="19" unicode=""
|
||||
d="M397 800c13 1 23 -4 34 -13c2 -2 214 -254 241 -287h128v-100h-100v-366c0 -18 -16 -34 -34 -34h-532c-18 0 -34 16 -34 34v366h-100v100h128l234 281c9 11 22 18 35 19zM400 672l-144 -172h288zM250 300c-28 0 -50 -22 -50 -50v-100c0 -28 22 -50 50 -50s50 22 50 50
|
||||
v100c0 28 -22 50 -50 50zM550 300c-28 0 -50 -22 -50 -50v-100c0 -28 22 -50 50 -50s50 22 50 50v100c0 28 -22 50 -50 50z" />
|
||||
<glyph glyph-name="1a" unicode=""
|
||||
d="M9 700h682c6 0 9 -4 9 -10v-190h100v-200h-100v-191c0 -6 -3 -9 -9 -9h-682c-6 0 -9 3 -9 9v582c0 6 3 9 9 9zM100 600v-400h500v400h-500z" />
|
||||
<glyph glyph-name="1b" unicode=""
|
||||
d="M9 700h682c6 0 9 -4 9 -10v-190h100v-200h-100v-191c0 -6 -3 -9 -9 -9h-682c-6 0 -9 3 -9 9v582c0 6 3 9 9 9z" />
|
||||
<glyph glyph-name="1c" unicode=""
|
||||
d="M92 650c0 23 19 50 45 50h3h5h5h500c28 0 50 -22 50 -50s-22 -50 -50 -50h-50v-141c9 -17 120 -231 166 -309c16 -26 34 -61 34 -106c0 -39 -15 -77 -41 -103h-3c-26 -25 -62 -41 -100 -41h-512c-39 0 -77 15 -103 41s-41 64 -41 103c0 46 18 80 34 106
|
||||
c46 78 157 292 166 309v141h-50c-2 0 -6 -1 -8 -1c-28 0 -50 23 -50 51zM500 600h-200v-162l-6 -10s-63 -123 -119 -228h450c-56 105 -119 228 -119 228l-6 10v162z" />
|
||||
<glyph glyph-name="1d" unicode=""
|
||||
d="M400 800c110 0 200 -90 200 -200c0 -104 52 -198 134 -266c41 -34 66 -82 66 -134h-800c0 52 25 100 66 134c82 68 134 162 134 266c0 110 90 200 200 200zM300 100h200c0 -55 -45 -100 -100 -100s-100 45 -100 100z" />
|
||||
<glyph glyph-name="1e" unicode="" horiz-adv-x="600"
|
||||
d="M150 800h50l350 -250l-225 -147l225 -153l-350 -250h-50v250l-75 -75l-75 75l150 150l-150 150l75 75l75 -75v250zM250 650v-200l150 100zM250 350v-200l150 100z" />
|
||||
<glyph glyph-name="1f" unicode=""
|
||||
d="M0 800h500c110 0 200 -90 200 -200c0 -47 -17 -91 -44 -125c85 -40 144 -125 144 -225c0 -138 -112 -250 -250 -250h-550v100c55 0 100 45 100 100v400c0 55 -45 100 -100 100v100zM300 700v-200h100c55 0 100 45 100 100s-45 100 -100 100h-100zM300 400v-300h150
|
||||
c83 0 150 67 150 150s-67 150 -150 150h-150z" />
|
||||
<glyph glyph-name="20" unicode="" horiz-adv-x="600"
|
||||
d="M300 800v-300h200l-300 -500v300h-200z" />
|
||||
<glyph glyph-name="21" unicode=""
|
||||
d="M100 800h300v-300l100 100l100 -100v300h50c28 0 50 -22 50 -50v-550h-550c-28 0 -50 -22 -50 -50s22 -50 50 -50h550v-100h-550c-83 0 -150 67 -150 150v550l3 19c8 39 39 70 78 78z" />
|
||||
<glyph glyph-name="22" unicode="" horiz-adv-x="400"
|
||||
d="M0 800h400v-800l-200 200l-200 -200v800z" />
|
||||
<glyph glyph-name="23" unicode=""
|
||||
d="M0 800h800v-100h-800v100zM0 600h300v-103h203v103h297v-591c0 -6 -3 -9 -9 -9h-782c-6 0 -9 3 -9 9v591z" />
|
||||
<glyph glyph-name="24" unicode=""
|
||||
d="M300 800h200c55 0 100 -45 100 -100v-100h191c6 0 9 -3 9 -9v-241c0 -28 -22 -50 -50 -50h-700c-28 0 -50 22 -50 50v241c0 6 3 9 9 9h191v100c0 55 45 100 100 100zM300 700v-100h200v100h-200zM0 209c16 -6 32 -9 50 -9h700c18 0 34 3 50 9v-200c0 -6 -3 -9 -9 -9h-782
|
||||
c-6 0 -9 3 -9 9v200z" />
|
||||
<glyph glyph-name="25" unicode="" horiz-adv-x="600"
|
||||
d="M300 800c58 0 110 -16 147 -53s53 -89 53 -147h-100c0 39 -11 61 -25 75s-36 25 -75 25c-35 0 -55 -10 -72 -31s-28 -55 -28 -94c0 -51 20 -107 28 -175h172v-100h-178c-14 -60 -49 -127 -113 -200h491v-100h-600v122l16 12c69 69 95 121 106 166h-122v100h125
|
||||
c-8 50 -25 106 -25 175c0 58 16 114 50 156c34 43 88 69 150 69z" />
|
||||
<glyph glyph-name="26" unicode=""
|
||||
d="M34 700h4h3h4h5h700c28 0 50 -22 50 -50v-700c0 -28 -22 -50 -50 -50h-700c-28 0 -50 22 -50 50v700v2c0 20 15 42 34 48zM150 600c-28 0 -50 -22 -50 -50s22 -50 50 -50s50 22 50 50s-22 50 -50 50zM350 600c-28 0 -50 -22 -50 -50s22 -50 50 -50h300c28 0 50 22 50 50
|
||||
s-22 50 -50 50h-300zM100 400v-400h600v400h-600z" />
|
||||
<glyph glyph-name="27" unicode=""
|
||||
d="M744 797l6 -3l44 -44c4 -4 3 -8 0 -12l-266 -375l-15 -13l-25 -12c-23 72 -78 127 -150 150l12 25l13 15l375 266zM266 400c74 0 134 -60 134 -134c0 -147 -119 -266 -266 -266c-48 0 -95 12 -134 34c80 46 134 133 134 232c0 74 58 134 132 134z" />
|
||||
<glyph glyph-name="28" unicode=""
|
||||
d="M9 451c0 23 19 50 46 50c8 0 19 -3 26 -7l131 -66l29 22c-79 81 -1 250 118 250s197 -167 119 -250l28 -22l131 66c6 4 12 7 21 7c28 0 50 -22 50 -50c0 -17 -12 -37 -27 -45l-115 -56c9 -16 19 -33 25 -50h68c28 0 50 -22 50 -50s-22 -50 -50 -50h-50
|
||||
c0 -23 -2 -45 -6 -66l78 -40c21 -5 37 -28 37 -49c0 -28 -22 -50 -50 -50c-10 0 -23 5 -31 11l-65 35c-24 -46 -62 -86 -103 -110c-35 19 -60 45 -60 72v135v4v5v6v5v5v87c0 28 -22 50 -50 50c-24 0 -45 -17 -50 -40c1 -3 1 -8 1 -11s0 -8 -1 -11v-82v-4v-5v-144
|
||||
c0 -28 -24 -53 -59 -72c-41 25 -79 64 -103 110l-66 -35c-8 -6 -21 -11 -31 -11c-28 0 -50 22 -50 50c0 21 16 44 37 49l78 40c-4 21 -6 43 -6 66h-50h-5c-28 0 -50 22 -50 50c0 26 22 50 50 50h5h69c6 17 16 34 25 50l-116 56c-16 7 -28 27 -28 45z" />
|
||||
<glyph glyph-name="29" unicode=""
|
||||
d="M600 700h91c6 0 9 -3 9 -9v-582c0 -6 -3 -9 -9 -9h-91v600zM210 503l290 147v-500l-250 125v-3c-15 0 -25 -8 -28 -22l75 -178c11 -25 0 -58 -25 -69s-58 0 -69 25l-103 272h-91c-6 0 -9 3 -9 9v182c0 6 3 9 9 9h182z" />
|
||||
<glyph glyph-name="2a" unicode=""
|
||||
d="M9 800h682c6 0 9 -3 9 -9v-782c0 -6 -3 -9 -9 -9h-682c-6 0 -9 3 -9 9v782c0 6 3 9 9 9zM100 700v-200h500v200h-500zM100 400v-100h100v100h-100zM300 400v-100h100v100h-100zM500 400v-300h100v300h-100zM100 200v-100h100v100h-100zM300 200v-100h100v100h-100z" />
|
||||
<glyph glyph-name="2b" unicode=""
|
||||
d="M0 800h700v-200h-700v200zM0 500h700v-491c0 -6 -3 -9 -9 -9h-682c-6 0 -9 3 -9 9v491zM100 400v-100h100v100h-100zM300 400v-100h100v100h-100zM500 400v-100h100v100h-100zM100 200v-100h100v100h-100zM300 200v-100h100v100h-100z" />
|
||||
<glyph glyph-name="2c" unicode=""
|
||||
d="M409 800h182c6 0 10 -4 12 -9l94 -182c2 -5 6 -9 12 -9h82c6 0 9 -3 9 -9v-582c0 -6 -3 -9 -9 -9h-782c-6 0 -9 3 -9 9v441c0 83 67 150 150 150h141c6 0 10 4 12 9l94 182c2 5 6 9 12 9zM150 500c-28 0 -50 -22 -50 -50s22 -50 50 -50s50 22 50 50s-22 50 -50 50z
|
||||
M500 500c-110 0 -200 -90 -200 -200s90 -200 200 -200s200 90 200 200s-90 200 -200 200zM500 400c55 0 100 -45 100 -100s-45 -100 -100 -100s-100 45 -100 100s45 100 100 100z" />
|
||||
<glyph glyph-name="2d" unicode=""
|
||||
d="M0 600h800l-400 -400z" />
|
||||
<glyph glyph-name="2e" unicode="" horiz-adv-x="400"
|
||||
d="M400 800v-800l-400 400z" />
|
||||
<glyph glyph-name="2f" unicode="" horiz-adv-x="400"
|
||||
d="M0 800l400 -400l-400 -400v800z" />
|
||||
<glyph glyph-name="30" unicode=""
|
||||
d="M400 600l400 -400h-800z" />
|
||||
<glyph glyph-name="31" unicode=""
|
||||
d="M0 550c0 23 20 50 46 50h3h5h4h200c17 0 37 -13 44 -28l38 -72h444c14 0 19 -12 15 -25l-81 -250c-4 -13 -21 -25 -35 -25h-350c-14 0 -30 12 -34 25c-27 83 -54 167 -81 250l-10 25h-150c-2 0 -5 -1 -7 -1c-28 0 -51 23 -51 51zM358 100c28 0 50 -22 50 -50
|
||||
s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM658 100c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50z" />
|
||||
<glyph glyph-name="32" unicode=""
|
||||
d="M0 700h500v-100h-300v-300h-100l-100 -100v500zM300 500h500v-500l-100 100h-400v400z" />
|
||||
<glyph glyph-name="33" unicode=""
|
||||
d="M641 700l143 -141l-493 -493c-71 76 -146 148 -219 222l-72 71l141 141c50 -51 101 -101 153 -150c116 117 234 231 347 350z" />
|
||||
<glyph glyph-name="34" unicode=""
|
||||
d="M150 600l250 -250l250 250l150 -150l-400 -400l-400 400z" />
|
||||
<glyph glyph-name="35" unicode="" horiz-adv-x="600"
|
||||
d="M400 800l150 -150l-250 -250l250 -250l-150 -150l-400 400z" />
|
||||
<glyph glyph-name="36" unicode="" horiz-adv-x="600"
|
||||
d="M150 800l400 -400l-400 -400l-150 150l250 250l-250 250z" />
|
||||
<glyph glyph-name="37" unicode=""
|
||||
d="M400 600l400 -400l-150 -150l-250 250l-250 -250l-150 150z" />
|
||||
<glyph glyph-name="38" unicode=""
|
||||
d="M400 800c221 0 400 -179 400 -400s-179 -400 -400 -400s-400 179 -400 400s179 400 400 400zM600 622l-250 -250l-100 100l-72 -72l172 -172l322 322z" />
|
||||
<glyph glyph-name="39" unicode=""
|
||||
d="M400 800c221 0 400 -179 400 -400s-179 -400 -400 -400s-400 179 -400 400s179 400 400 400zM250 622l-72 -72l150 -150l-150 -150l72 -72l150 150l150 -150l72 72l-150 150l150 150l-72 72l-150 -150z" />
|
||||
<glyph glyph-name="3a" unicode=""
|
||||
d="M350 800c28 0 50 -22 50 -50v-50h75c14 0 25 -11 25 -25v-75h-300v75c0 14 11 25 25 25h75v50c0 28 22 50 50 50zM25 700h75v-200h500v200h75c14 0 25 -11 25 -25v-650c0 -14 -11 -25 -25 -25h-650c-14 0 -25 11 -25 25v650c0 14 11 25 25 25z" />
|
||||
<glyph glyph-name="3b" unicode=""
|
||||
d="M400 800c220 0 400 -180 400 -400s-180 -400 -400 -400s-400 180 -400 400s180 400 400 400zM400 700c-166 0 -300 -134 -300 -300s134 -300 300 -300s300 134 300 300s-134 300 -300 300zM350 600h100v-181c23 -24 47 -47 72 -69l-72 -72c-27 30 -55 59 -84 88l-16 12
|
||||
v222z" />
|
||||
<glyph glyph-name="3c" unicode=""
|
||||
d="M450 800c138 0 250 -112 250 -250v-50c58 -21 100 -85 100 -150c0 -18 -3 -34 -9 -50h-191v50c0 83 -67 150 -150 150s-150 -67 -150 -150v-50h-272c-17 30 -28 63 -28 100c0 110 90 200 200 200c23 114 129 200 250 200zM434 400h3h4c3 0 6 1 9 1c28 0 50 -22 50 -50v-1
|
||||
v-150h150l-200 -200l-200 200h150v150v2c0 20 15 42 34 48z" />
|
||||
<glyph glyph-name="3d" unicode=""
|
||||
d="M450 800c138 0 250 -112 250 -250v-50c58 -21 100 -85 100 -150c0 -18 -3 -34 -9 -50h-141l-200 200l-200 -200h-222c-17 30 -28 63 -28 100c0 110 90 200 200 200c23 114 129 200 250 200zM450 350l250 -250h-200v-50c0 -28 -22 -50 -50 -50s-50 22 -50 50v50h-200z" />
|
||||
<glyph glyph-name="3e" unicode=""
|
||||
d="M450 700c138 0 250 -112 250 -250v-50c58 -21 100 -85 100 -150c0 -83 -67 -150 -150 -150h-450c-110 0 -200 90 -200 200s90 200 200 200c23 114 129 200 250 200z" />
|
||||
<glyph glyph-name="3f" unicode=""
|
||||
d="M250 800c82 0 154 -40 200 -100c-143 0 -270 -85 -325 -209c-36 -10 -70 -25 -100 -47c-16 33 -25 67 -25 106c0 138 112 250 250 250zM450 600c138 0 250 -112 250 -250v-50c58 -21 100 -85 100 -150c0 -83 -67 -150 -150 -150h-450c-110 0 -200 90 -200 200
|
||||
s90 200 200 200c23 114 129 200 250 200z" />
|
||||
<glyph glyph-name="40" unicode=""
|
||||
d="M500 700h100l-300 -600h-100zM100 600h100l-100 -200l100 -200h-100l-100 200zM600 600h100l100 -200l-100 -200h-100l100 200z" />
|
||||
<glyph glyph-name="41" unicode=""
|
||||
d="M350 800h100l50 -119l28 -12l119 50l72 -72l-50 -119l12 -28l119 -50v-100l-119 -50l-12 -28l50 -119l-72 -72l-119 50l-28 -12l-50 -119h-100l-50 119l-28 12l-119 -50l-72 72l50 119l-12 28l-119 50v100l119 50l12 28l-50 119l72 72l119 -50l28 12zM400 550
|
||||
c-83 0 -150 -67 -150 -150s67 -150 150 -150s150 67 150 150s-67 150 -150 150z" />
|
||||
<glyph glyph-name="42" unicode=""
|
||||
d="M0 800h800v-200h-800v200zM200 500h400l-200 -200zM0 100h800v-100h-800v100z" />
|
||||
<glyph glyph-name="43" unicode=""
|
||||
d="M0 800h100v-800h-100v800zM600 800h200v-800h-200v800zM500 600v-400l-200 200z" />
|
||||
<glyph glyph-name="44" unicode=""
|
||||
d="M0 800h200v-800h-200v800zM700 800h100v-800h-100v800zM300 600l200 -200l-200 -200v400z" />
|
||||
<glyph glyph-name="45" unicode=""
|
||||
d="M0 800h800v-100h-800v100zM400 500l200 -200h-400zM0 200h800v-200h-800v200z" />
|
||||
<glyph glyph-name="46" unicode=""
|
||||
d="M150 700c83 0 150 -67 150 -150v-50h100v50c0 83 67 150 150 150s150 -67 150 -150s-67 -150 -150 -150h-50v-100h50c83 0 150 -67 150 -150s-67 -150 -150 -150s-150 67 -150 150v50h-100v-50c0 -83 -67 -150 -150 -150s-150 67 -150 150s67 150 150 150h50v100h-50
|
||||
c-83 0 -150 67 -150 150s67 150 150 150zM150 600c-28 0 -50 -22 -50 -50s22 -50 50 -50h50v50c0 28 -22 50 -50 50zM550 600c-28 0 -50 -22 -50 -50v-50h50c28 0 50 22 50 50s-22 50 -50 50zM300 400v-100h100v100h-100zM150 200c-28 0 -50 -22 -50 -50s22 -50 50 -50
|
||||
s50 22 50 50v50h-50zM500 200v-50c0 -28 22 -50 50 -50s50 22 50 50s-22 50 -50 50h-50z" />
|
||||
<glyph glyph-name="47" unicode=""
|
||||
d="M0 791c0 5 4 9 9 9h782c6 0 9 -4 9 -10v-790l-200 200h-591c-6 0 -9 3 -9 9v582z" />
|
||||
<glyph glyph-name="48" unicode=""
|
||||
d="M400 800c220 0 400 -180 400 -400s-180 -400 -400 -400s-400 180 -400 400s180 400 400 400zM400 700c-166 0 -300 -134 -300 -300s134 -300 300 -300s300 134 300 300s-134 300 -300 300zM600 600l-100 -300l-300 -100l100 300zM400 450c-28 0 -50 -22 -50 -50
|
||||
s22 -50 50 -50s50 22 50 50s-22 50 -50 50z" />
|
||||
<glyph glyph-name="49" unicode=""
|
||||
d="M400 800c220 0 400 -180 400 -400s-180 -400 -400 -400s-400 180 -400 400s180 400 400 400zM400 700v-600c166 0 300 134 300 300s-134 300 -300 300z" />
|
||||
<glyph glyph-name="4a" unicode=""
|
||||
d="M0 800h800v-100h-800v100zM0 600h500v-100h-500v100zM0 300h800v-100h-800v100zM0 100h600v-100h-600v100zM750 100c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50z" />
|
||||
<glyph glyph-name="4b" unicode=""
|
||||
d="M25 700h750c14 0 25 -11 25 -25v-75h-800v75c0 14 11 25 25 25zM0 500h800v-375c0 -14 -11 -25 -25 -25h-750c-14 0 -25 11 -25 25v375zM100 300v-100h100v100h-100zM300 300v-100h100v100h-100z" />
|
||||
<glyph glyph-name="4c" unicode=""
|
||||
d="M100 800h100v-100h450l100 100l50 -50l-100 -100v-450h100v-100h-100v-100h-100v100h-500v500h-100v100h100v100zM200 600v-350l350 350h-350zM600 550l-350 -350h350v350z" />
|
||||
<glyph glyph-name="4d" unicode=""
|
||||
d="M400 800c220 0 400 -180 400 -400s-180 -400 -400 -400s-400 180 -400 400s180 400 400 400zM400 700c-166 0 -300 -134 -300 -300s134 -300 300 -300s300 134 300 300s-134 300 -300 300zM400 600c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50z
|
||||
M200 452c0 20 15 42 34 48h3h3h8c12 0 28 -7 36 -16l91 -90l25 6c55 0 100 -45 100 -100s-45 -100 -100 -100s-100 45 -100 100l6 25l-90 91c-9 8 -16 24 -16 36zM550 500c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50z" />
|
||||
<glyph glyph-name="4e" unicode=""
|
||||
d="M300 800h200v-300h200l-300 -300l-300 300h200v300zM0 100h800v-100h-800v100z" />
|
||||
<glyph glyph-name="4f" unicode=""
|
||||
d="M0 800h800v-100h-800v100zM400 600l300 -300h-200v-300h-200v300h-200z" />
|
||||
<glyph glyph-name="50" unicode=""
|
||||
d="M200 700h600v-600h-600l-200 300zM350 622l-72 -72l150 -150l-150 -150l72 -72l150 150l150 -150l72 72l-150 150l150 150l-72 72l-150 -150z" />
|
||||
<glyph glyph-name="51" unicode=""
|
||||
d="M400 700c220 0 400 -180 400 -400h-100c0 166 -134 300 -300 300s-300 -134 -300 -300h-100c0 220 180 400 400 400zM341 491l59 -88l59 88c81 -25 141 -101 141 -191c0 -110 -90 -200 -200 -200s-200 90 -200 200c0 90 60 166 141 191z" />
|
||||
<glyph glyph-name="52" unicode=""
|
||||
d="M0 800h300v-400h400v-400h-700v800zM400 800l300 -300h-300v300zM100 600v-100h100v100h-100zM100 400v-100h100v100h-100zM100 200v-100h400v100h-400z" />
|
||||
<glyph glyph-name="53" unicode="" horiz-adv-x="600"
|
||||
d="M200 700h100v-100h75c30 0 58 -6 81 -22s44 -44 44 -78v-100h-100v94c-4 3 -13 6 -25 6h-250c-14 0 -25 -11 -25 -25v-50c0 -15 20 -40 34 -44l257 -65c66 -16 109 -73 109 -141v-50c0 -68 -57 -125 -125 -125h-75v-100h-100v100h-75c-30 0 -58 6 -81 22s-44 44 -44 78
|
||||
v100h100v-94c4 -3 13 -6 25 -6h250c14 0 25 11 25 25v50c0 15 -20 40 -34 44l-257 65c-66 16 -109 73 -109 141v50c0 68 57 125 125 125h75v100z" />
|
||||
<glyph glyph-name="54" unicode=""
|
||||
d="M0 700h300v-300l-300 -300v600zM500 700h300v-300l-300 -300v600z" />
|
||||
<glyph glyph-name="55" unicode=""
|
||||
d="M300 700v-600h-300v300zM800 700v-600h-300v300z" />
|
||||
<glyph glyph-name="56" unicode=""
|
||||
d="M300 700v-100c-111 0 -200 -89 -200 -200h200v-300h-300v300c0 165 135 300 300 300zM800 700v-100c-111 0 -200 -89 -200 -200h200v-300h-300v300c0 165 135 300 300 300z" />
|
||||
<glyph glyph-name="57" unicode=""
|
||||
d="M0 700h300v-300c0 -165 -135 -300 -300 -300v100c111 0 200 89 200 200h-200v300zM500 700h300v-300c0 -165 -135 -300 -300 -300v100c111 0 200 89 200 200h-200v300z" />
|
||||
<glyph glyph-name="58" unicode="" horiz-adv-x="600"
|
||||
d="M300 800l34 -34c11 -11 266 -270 266 -488c0 -165 -135 -300 -300 -300s-300 135 -300 300c0 218 255 477 266 488zM150 328c-28 0 -50 -22 -50 -50c0 -110 90 -200 200 -200c28 0 50 22 50 50s-22 50 -50 50c-55 0 -100 45 -100 100c0 28 -22 50 -50 50z" />
|
||||
<glyph glyph-name="59" unicode=""
|
||||
d="M400 800l400 -500h-800zM0 200h800v-200h-800v200z" />
|
||||
<glyph glyph-name="5a" unicode="" horiz-adv-x="600"
|
||||
d="M300 800l300 -300h-600zM0 300h600l-300 -300z" />
|
||||
<glyph glyph-name="5b" unicode=""
|
||||
d="M0 500h200v-200h-200v200zM300 500h200v-200h-200v200zM600 500h200v-200h-200v200z" />
|
||||
<glyph glyph-name="5c" unicode=""
|
||||
d="M0 700h800v-100l-400 -200l-400 200v100zM0 500l400 -200l400 200v-400h-800v400z" />
|
||||
<glyph glyph-name="5d" unicode=""
|
||||
d="M400 800l400 -200v-600h-800v600zM400 688l-300 -150v-188l300 -150l300 150v188zM200 500h400v-100l-200 -100l-200 100v100z" />
|
||||
<glyph glyph-name="5e" unicode=""
|
||||
d="M600 700c69 0 134 -19 191 -50l-16 -106c-49 35 -109 56 -175 56c-131 0 -240 -84 -281 -200h331l-16 -100h-334c0 -36 8 -68 19 -100h297l-16 -100h-222c55 -61 133 -100 222 -100c78 0 147 30 200 78v-122c-59 -35 -127 -56 -200 -56c-147 0 -274 82 -344 200h-256
|
||||
l19 100h197c-8 32 -16 66 -16 100h-200l25 100h191c45 172 198 300 384 300z" />
|
||||
<glyph glyph-name="5f" unicode=""
|
||||
d="M0 700h700v-100h-700v100zM0 500h500v-100h-500v100zM0 300h800v-100h-800v100zM0 100h100v-100h-100v100zM200 100h100v-100h-100v100zM400 100h100v-100h-100v100z" />
|
||||
<glyph glyph-name="60" unicode=""
|
||||
d="M0 800h800v-100h-800v100zM200 600h400l-200 -200zM0 200h800v-200h-800v200z" />
|
||||
<glyph glyph-name="61" unicode=""
|
||||
d="M0 800h100v-800h-100v800zM600 800h200v-800h-200v800zM200 600l200 -200l-200 -200v400z" />
|
||||
<glyph glyph-name="62" unicode=""
|
||||
d="M0 800h200v-800h-200v800zM700 800h100v-800h-100v800zM600 600v-400l-200 200z" />
|
||||
<glyph glyph-name="63" unicode=""
|
||||
d="M0 800h800v-200h-800v200zM400 400l200 -200h-400zM0 100h800v-100h-800v100z" />
|
||||
<glyph glyph-name="64" unicode=""
|
||||
d="M0 800h200v-100h-100v-600h600v100h100v-200h-800v800zM400 800h400v-400l-150 150l-250 -250l-100 100l250 250z" />
|
||||
<glyph glyph-name="65" unicode=""
|
||||
d="M403 700c247 0 397 -300 397 -300s-150 -300 -397 -300c-253 0 -403 300 -403 300s150 300 403 300zM400 600c-110 0 -200 -90 -200 -200s90 -200 200 -200s200 90 200 200s-90 200 -200 200zM400 500c10 0 19 -3 28 -6c-16 -8 -28 -24 -28 -44c0 -28 22 -50 50 -50
|
||||
c20 0 36 12 44 28c3 -9 6 -18 6 -28c0 -55 -45 -100 -100 -100s-100 45 -100 100s45 100 100 100z" />
|
||||
<glyph glyph-name="66" unicode="" horiz-adv-x="900"
|
||||
d="M331 700h3h3c3 1 7 1 10 1c12 0 29 -8 37 -17l94 -93l66 65c57 57 155 57 212 0c58 -58 58 -154 0 -212l-65 -66l93 -94c10 -8 18 -25 18 -38c0 -28 -22 -50 -50 -50c-13 0 -32 9 -40 20l-62 65l-381 -381h-269v272l375 381l-63 63c-9 8 -16 24 -16 36c0 20 16 42 35 48z
|
||||
M447 481l-313 -315l128 -132l316 316z" />
|
||||
<glyph glyph-name="67" unicode=""
|
||||
d="M0 800h300v-400h400v-400h-700v800zM400 800l300 -300h-300v300z" />
|
||||
<glyph glyph-name="68" unicode=""
|
||||
d="M200 800c0 0 200 -100 200 -300s-298 -302 -200 -500c0 0 -200 100 -200 300s300 300 200 500zM500 500c0 0 200 -100 200 -300c0 -150 -60 -200 -100 -200h-300c0 200 300 300 200 500z" />
|
||||
<glyph glyph-name="69" unicode=""
|
||||
d="M0 800h100v-800h-100v800zM200 800h300v-100h300l-200 -203l200 -197h-400v100h-200v400z" />
|
||||
<glyph glyph-name="6a" unicode="" horiz-adv-x="400"
|
||||
d="M150 800h150l-100 -200h200l-150 -300h150l-300 -300l-100 300h134l66 200h-200z" />
|
||||
<glyph glyph-name="6b" unicode=""
|
||||
d="M0 800h300v-100h500v-100h-800v200zM0 500h800v-450c0 -28 -22 -50 -50 -50h-700c-28 0 -50 22 -50 50v450z" />
|
||||
<glyph glyph-name="6c" unicode=""
|
||||
d="M150 800c83 0 150 -67 150 -150c0 -66 -41 -121 -100 -141v-118c15 5 33 9 50 9h200c28 0 50 22 50 50v59c-59 20 -100 75 -100 141c0 83 67 150 150 150s150 -67 150 -150c0 -66 -41 -121 -100 -141v-59c0 -82 -68 -150 -150 -150h-200c-14 0 -25 -7 -34 -16
|
||||
c50 -24 84 -74 84 -134c0 -83 -67 -150 -150 -150s-150 67 -150 150c0 66 41 121 100 141v218c-59 20 -100 75 -100 141c0 83 67 150 150 150z" />
|
||||
<glyph glyph-name="6d" unicode=""
|
||||
d="M0 800h400l-150 -150l150 -150l-100 -100l-150 150l-150 -150v400zM500 400l150 -150l150 150v-400h-400l150 150l-150 150z" />
|
||||
<glyph glyph-name="6e" unicode=""
|
||||
d="M100 800l150 -150l150 150v-400h-400l150 150l-150 150zM400 400h400l-150 -150l150 -150l-100 -100l-150 150l-150 -150v400z" />
|
||||
<glyph glyph-name="6f" unicode=""
|
||||
d="M400 800c221 0 400 -179 400 -400s-179 -400 -400 -400s-400 179 -400 400s179 400 400 400zM400 700c-56 0 -108 -17 -153 -44l22 -19c33 -18 13 -48 -13 -59c-30 -13 -77 10 -65 -41c13 -55 -27 -3 -47 -15c-42 -26 49 -152 31 -156l-59 34c-8 0 -13 -5 -16 -10
|
||||
c1 -30 10 -57 19 -84c28 -11 77 -2 100 -25c47 -28 97 -115 75 -159c34 -13 68 -22 106 -22c101 0 193 48 247 125c3 24 -8 44 -50 44c-69 0 -156 13 -153 97c2 46 101 108 66 143c-30 30 12 39 12 66c0 37 -65 32 -69 50s20 36 41 56c-30 10 -60 19 -94 19zM631 591
|
||||
c-38 -11 -94 -35 -87 -53c6 -15 52 -1 65 -13c11 -10 16 -59 44 -31l22 22v3c-11 26 -26 50 -44 72z" />
|
||||
<glyph glyph-name="70" unicode=""
|
||||
d="M703 800l97 -100l-400 -400l-100 100l-200 -203l-100 100l300 303l100 -100zM0 100h800v-100h-800v100z" />
|
||||
<glyph glyph-name="71" unicode=""
|
||||
d="M0 700h100v-100h-100v100zM200 700h100v-100h-100v100zM400 700h100v-100h-100v100zM600 700h100v-100h-100v100zM0 500h100v-100h-100v100zM200 500h100v-100h-100v100zM400 500h100v-100h-100v100zM600 500h100v-100h-100v100zM0 300h100v-100h-100v100zM200 300h100
|
||||
v-100h-100v100zM400 300h100v-100h-100v100zM600 300h100v-100h-100v100zM0 100h100v-100h-100v100zM200 100h100v-100h-100v100zM400 100h100v-100h-100v100zM600 100h100v-100h-100v100z" />
|
||||
<glyph glyph-name="72" unicode=""
|
||||
d="M0 800h200v-200h-200v200zM300 800h200v-200h-200v200zM600 800h200v-200h-200v200zM0 500h200v-200h-200v200zM300 500h200v-200h-200v200zM600 500h200v-200h-200v200zM0 200h200v-200h-200v200zM300 200h200v-200h-200v200zM600 200h200v-200h-200v200z" />
|
||||
<glyph glyph-name="73" unicode=""
|
||||
d="M0 800h300v-300h-300v300zM500 800h300v-300h-300v300zM0 300h300v-300h-300v300zM500 300h300v-300h-300v300z" />
|
||||
<glyph glyph-name="74" unicode=""
|
||||
d="M19 800h662c11 0 19 -8 19 -19v-331c0 -28 -22 -50 -50 -50h-600c-28 0 -50 22 -50 50v331c0 11 8 19 19 19zM0 309c16 -6 32 -9 50 -9h600c18 0 34 3 50 9v-290c0 -11 -8 -19 -19 -19h-662c-11 0 -19 8 -19 19v290zM550 200c-28 0 -50 -22 -50 -50s22 -50 50 -50
|
||||
s50 22 50 50s-22 50 -50 50z" />
|
||||
<glyph glyph-name="75" unicode=""
|
||||
d="M0 700h300v-100h-50c-28 0 -50 -22 -50 -50v-150h300v150c0 28 -22 50 -50 50h-50v100h300v-100h-50c-28 0 -50 -22 -50 -50v-400c0 -28 22 -50 50 -50h50v-100h-300v100h50c28 0 50 22 50 50v150h-300v-150c0 -28 22 -50 50 -50h50v-100h-300v100h50c28 0 50 22 50 50
|
||||
v400c0 28 -22 50 -50 50h-50v100z" />
|
||||
<glyph glyph-name="76" unicode=""
|
||||
d="M400 700c165 0 300 -135 300 -300v-100h50c28 0 50 -22 50 -50v-200c0 -28 -22 -50 -50 -50h-100c-28 0 -50 22 -50 50v350c0 111 -89 200 -200 200s-200 -89 -200 -200v-350c0 -28 -22 -50 -50 -50h-100c-28 0 -50 22 -50 50v200c0 28 22 50 50 50h50v100
|
||||
c0 165 135 300 300 300z" />
|
||||
<glyph glyph-name="77" unicode=""
|
||||
d="M0 500c0 109 91 200 200 200s200 -91 200 -200c0 109 91 200 200 200s200 -91 200 -200c0 -55 -23 -105 -59 -141l-341 -340l-341 340c-36 36 -59 86 -59 141z" />
|
||||
<glyph glyph-name="78" unicode=""
|
||||
d="M400 700l400 -300l-100 3v-403h-200v200h-200v-200h-200v400h-100z" />
|
||||
<glyph glyph-name="79" unicode=""
|
||||
d="M0 800h800v-800h-800v800zM100 700v-300l100 100l400 -400h100v100l-200 200l100 100l100 -100v300h-600z" />
|
||||
<glyph glyph-name="7a" unicode=""
|
||||
d="M19 800h762c11 0 19 -8 19 -19v-762c0 -11 -8 -19 -19 -19h-762c-11 0 -19 8 -19 19v762c0 11 8 19 19 19zM100 600v-300h100l100 -100h200l100 100h100v300h-600z" />
|
||||
<glyph glyph-name="7b" unicode=""
|
||||
d="M200 600c80 0 142 -56 200 -122c58 66 119 122 200 122c131 0 200 -101 200 -200s-69 -200 -200 -200c-81 0 -142 56 -200 122c-58 -66 -121 -122 -200 -122c-131 0 -200 101 -200 200s69 200 200 200zM200 500c-74 0 -100 -54 -100 -100s26 -100 100 -100
|
||||
c42 0 88 47 134 100c-46 53 -92 100 -134 100zM600 500c-43 0 -88 -47 -134 -100c46 -53 91 -100 134 -100c74 0 100 54 100 100s-26 100 -100 100z" />
|
||||
<glyph glyph-name="7c" unicode="" horiz-adv-x="400"
|
||||
d="M300 800c55 0 100 -45 100 -100s-45 -100 -100 -100s-100 45 -100 100s45 100 100 100zM150 550c83 0 150 -69 150 -150c0 -66 -100 -214 -100 -250c0 -28 22 -50 50 -50s50 22 50 50h100c0 -83 -67 -150 -150 -150s-150 64 -150 150s100 222 100 250s-22 50 -50 50
|
||||
s-50 -22 -50 -50h-100c0 83 67 150 150 150z" />
|
||||
<glyph glyph-name="7d" unicode=""
|
||||
d="M200 800h500v-100h-122c-77 -197 -156 -392 -234 -588l-6 -12h162v-100h-500v100h122c77 197 156 392 234 588l7 12h-163v100z" />
|
||||
<glyph glyph-name="7e" unicode=""
|
||||
d="M0 700h800v-100h-800v100zM0 500h800v-100h-800v100zM0 300h800v-100h-800v100zM100 100h600v-100h-600v100z" />
|
||||
<glyph glyph-name="7f" unicode=""
|
||||
d="M0 700h800v-100h-800v100zM0 500h800v-100h-800v100zM0 300h800v-100h-800v100zM0 100h600v-100h-600v100z" />
|
||||
<glyph glyph-name="80" unicode=""
|
||||
d="M0 700h800v-100h-800v100zM0 500h800v-100h-800v100zM0 300h800v-100h-800v100zM200 100h600v-100h-600v100z" />
|
||||
<glyph glyph-name="81" unicode=""
|
||||
d="M550 800c138 0 250 -112 250 -250s-112 -250 -250 -250c-16 0 -32 0 -47 3l-3 -3v-100h-200v-200h-300v200l303 303c-3 15 -3 31 -3 47c0 138 112 250 250 250zM600 700c-55 0 -100 -45 -100 -100s45 -100 100 -100s100 45 100 100s-45 100 -100 100z" />
|
||||
<glyph glyph-name="82" unicode=""
|
||||
d="M134 600h3h4h4h5h500c28 0 50 -22 50 -50v-350h100v-150c0 -28 -22 -50 -50 -50h-700c-28 0 -50 22 -50 50v150h100v350v2c0 20 15 42 34 48zM200 500v-300h100v-100h200v100h100v300h-400z" />
|
||||
<glyph glyph-name="83" unicode=""
|
||||
d="M0 800h400v-400h-400v400zM500 600h100v-400h-400v100h300v300zM700 400h100v-400h-400v100h300v300z" />
|
||||
<glyph glyph-name="84" unicode="" horiz-adv-x="600"
|
||||
d="M337 694c6 4 12 7 21 7c28 0 50 -22 50 -50c0 -17 -12 -37 -27 -45l-300 -150c-8 -6 -21 -11 -31 -11c-28 0 -50 22 -50 50c0 21 16 44 37 49zM437 544c6 4 12 7 21 7c28 0 50 -22 50 -50c0 -17 -12 -37 -27 -45l-400 -200c-8 -6 -21 -11 -31 -11c-28 0 -50 22 -50 50
|
||||
c0 21 16 44 37 49zM437 344c6 4 12 7 21 7c28 0 50 -22 50 -50c0 -17 -12 -37 -27 -45l-106 -56c24 -4 43 -26 43 -50c0 -28 -23 -51 -51 -51c-2 0 -6 1 -8 1h-200c-26 1 -48 24 -48 50c0 16 12 36 26 44zM151 -50c0 23 20 50 46 50h3h4h5h100c28 0 50 -22 50 -50
|
||||
s-22 -50 -50 -50h-100c-2 0 -6 -1 -8 -1c-28 0 -50 23 -50 51z" />
|
||||
<glyph glyph-name="85" unicode=""
|
||||
d="M199 800h100v-200h-200v100h100v100zM586 797h1c18 1 38 1 56 -3c36 -8 69 -26 97 -54c78 -78 78 -203 0 -281l-150 -150c-8 -13 -28 -24 -43 -24c-28 0 -50 22 -50 50c0 15 11 35 24 43l150 150c40 40 39 105 0 144c-41 41 -110 34 -144 0l-44 -44
|
||||
c-8 -13 -27 -24 -42 -24c-28 0 -50 22 -50 50c0 15 11 35 24 43l43 44c32 33 72 53 128 56zM208 490c4 5 14 16 22 16h3c2 0 6 1 8 1c28 0 50 -22 50 -50c0 -11 -6 -27 -14 -35l-150 -150c-40 -40 -39 -105 0 -144c41 -41 110 -34 144 0l44 44c8 13 27 24 42 24
|
||||
c28 0 50 -22 50 -50c0 -15 -11 -35 -24 -43l-43 -44c-22 -22 -48 -37 -75 -47c-70 -25 -151 -9 -207 47c-78 78 -78 203 0 281zM499 200h200v-100h-100v-100h-100v200z" />
|
||||
<glyph glyph-name="86" unicode=""
|
||||
d="M586 797c18 1 39 1 57 -3c36 -8 69 -26 97 -54c78 -78 78 -203 0 -281l-150 -150c-62 -62 -132 -81 -182 -78s-69 17 -84 25s-26 27 -26 44c0 28 22 51 50 51c8 0 19 -3 26 -7c0 0 15 -11 41 -13s62 3 106 47l150 150c40 40 39 105 0 144c-41 41 -110 34 -144 0
|
||||
c-8 -13 -28 -24 -43 -24c-28 0 -50 22 -50 50c0 15 11 35 24 43c32 33 72 53 128 56zM386 566c50 -2 64 -17 85 -22s37 -28 37 -49c0 -28 -22 -50 -50 -50c-10 0 -23 5 -31 11c0 0 -19 9 -47 10s-63 -4 -103 -44l-150 -150c-40 -40 -39 -105 0 -144c41 -41 110 -34 144 0
|
||||
c8 13 27 24 42 24c28 0 50 -22 50 -50c0 -15 -10 -35 -23 -43c-22 -22 -48 -37 -75 -47c-70 -25 -151 -9 -207 47c-78 78 -78 203 0 281l150 150c60 60 128 78 178 76z" />
|
||||
<glyph glyph-name="87" unicode=""
|
||||
d="M0 700h300v-300h-300v300zM400 700h400v-100h-400v100zM400 500h300v-100h-300v100zM0 300h300v-300h-300v300zM400 300h400v-100h-400v100zM400 100h300v-100h-300v100z" />
|
||||
<glyph glyph-name="88" unicode=""
|
||||
d="M50 700c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM200 700h600v-100h-600v100zM50 500c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM200 500h600v-100h-600v100zM50 300c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50
|
||||
s22 50 50 50zM200 300h600v-100h-600v100zM50 100c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM200 100h600v-100h-600v100z" />
|
||||
<glyph glyph-name="89" unicode=""
|
||||
d="M800 800l-400 -800l-100 300l-300 100z" />
|
||||
<glyph glyph-name="8a" unicode="" horiz-adv-x="600"
|
||||
d="M300 700c110 0 200 -90 200 -200v-100h100v-400h-600v400h100v100c0 110 90 200 200 200zM300 600c-56 0 -100 -44 -100 -100v-100h200v100c0 56 -44 100 -100 100z" />
|
||||
<glyph glyph-name="8b" unicode="" horiz-adv-x="600"
|
||||
d="M300 800c110 0 200 -90 200 -200v-200h100v-400h-600v400h400v200c0 56 -44 100 -100 100s-100 -44 -100 -100h-100c0 110 90 200 200 200z" />
|
||||
<glyph glyph-name="8c" unicode=""
|
||||
d="M400 700v-100c-111 0 -200 -89 -200 -200h100l-150 -200l-150 200h100c0 165 135 300 300 300zM650 600l150 -200h-100c0 -165 -135 -300 -300 -300v100c111 0 200 89 200 200h-100z" />
|
||||
<glyph glyph-name="8d" unicode=""
|
||||
d="M100 800h600v-300h100l-150 -250l-150 250h100v200h-400v-100h-100v200zM150 550l150 -250h-100v-200h400v100h100v-200h-600v300h-100z" />
|
||||
<glyph glyph-name="8e" unicode=""
|
||||
d="M600 700l200 -150l-200 -150v100h-500v-100h-100v100c0 55 45 100 100 100h500v100zM200 300v-100h500v100h100v-100c0 -55 -45 -100 -100 -100h-500v-100l-200 150z" />
|
||||
<glyph glyph-name="8f" unicode="" horiz-adv-x="900"
|
||||
d="M350 800c193 0 350 -157 350 -350c0 -60 -17 -117 -44 -166c5 -3 12 -8 16 -12l100 -100c16 -16 30 -49 30 -72c0 -56 -46 -102 -102 -102c-23 0 -56 14 -72 30l-100 100c-4 3 -9 9 -12 13c-49 -26 -107 -41 -166 -41c-193 0 -350 157 -350 350s157 350 350 350zM350 200
|
||||
c142 0 250 108 250 250c0 139 -111 250 -250 250s-250 -111 -250 -250s111 -250 250 -250z" />
|
||||
<glyph glyph-name="90" unicode="" horiz-adv-x="600"
|
||||
d="M300 800c166 0 300 -134 300 -300c0 -200 -300 -500 -300 -500s-300 300 -300 500c0 166 134 300 300 300zM300 700c-110 0 -200 -90 -200 -200s90 -200 200 -200s200 90 200 200s-90 200 -200 200z" />
|
||||
<glyph glyph-name="91" unicode="" horiz-adv-x="900"
|
||||
d="M0 800h800v-541c1 -3 1 -8 1 -11s0 -7 -1 -10v-238h-800v800zM495 250c0 26 22 50 50 50h5h150v400h-600v-600h600v100h-150h-5c-28 0 -50 22 -50 50zM350 600c83 0 150 -67 150 -150c0 -100 -150 -250 -150 -250s-150 150 -150 250c0 83 67 150 150 150zM350 500
|
||||
c-28 0 -50 -22 -50 -50s22 -50 50 -50s50 22 50 50s-22 50 -50 50z" />
|
||||
<glyph glyph-name="92" unicode="" horiz-adv-x="600"
|
||||
d="M0 700h200v-600h-200v600zM400 700h200v-600h-200v600z" />
|
||||
<glyph glyph-name="93" unicode="" horiz-adv-x="600"
|
||||
d="M0 700l600 -300l-600 -300v600z" />
|
||||
<glyph glyph-name="94" unicode="" horiz-adv-x="600"
|
||||
d="M300 700c166 0 300 -134 300 -300s-134 -300 -300 -300s-300 134 -300 300s134 300 300 300z" />
|
||||
<glyph glyph-name="95" unicode=""
|
||||
d="M400 700v-600l-400 300zM400 400l400 300v-600z" />
|
||||
<glyph glyph-name="96" unicode=""
|
||||
d="M0 700l400 -300l-400 -300v600zM400 100v600l400 -300z" />
|
||||
<glyph glyph-name="97" unicode=""
|
||||
d="M0 700h200v-600h-200v600zM200 400l500 300v-600z" />
|
||||
<glyph glyph-name="98" unicode=""
|
||||
d="M0 700l500 -300l-500 -300v600zM500 100v600h200v-600h-200z" />
|
||||
<glyph glyph-name="99" unicode="" horiz-adv-x="600"
|
||||
d="M0 700h600v-600h-600v600z" />
|
||||
<glyph glyph-name="9a" unicode=""
|
||||
d="M200 800h400v-200h200v-400h-200v-200h-400v200h-200v400h200v200z" />
|
||||
<glyph glyph-name="9b" unicode=""
|
||||
d="M0 700h800v-100h-800v100zM0 403h800v-100h-800v100zM0 103h800v-100h-800v100z" />
|
||||
<glyph glyph-name="9c" unicode="" horiz-adv-x="600"
|
||||
d="M278 700c7 2 13 4 22 4c55 0 100 -45 100 -100v-4v-200c0 -55 -45 -100 -100 -100s-100 45 -100 100v200v2c0 44 35 88 78 98zM34 500h4h3c3 0 6 1 9 1c28 0 50 -22 50 -50v-1v-50c0 -111 89 -200 200 -200s200 89 200 200v50c0 28 22 50 50 50s50 -22 50 -50v-50
|
||||
c0 -148 -109 -270 -250 -294v-106h50c55 0 100 -45 100 -100h-400c0 55 45 100 100 100h50v106c-141 24 -250 146 -250 294v50v2c0 20 15 42 34 48z" />
|
||||
<glyph glyph-name="9d" unicode=""
|
||||
d="M0 500h800v-200h-800v200z" />
|
||||
<glyph glyph-name="9e" unicode=""
|
||||
d="M34 700h4h3h4h5h700c28 0 50 -22 50 -50v-500c0 -28 -22 -50 -50 -50h-250v-100h100c55 0 100 -45 100 -100h-600c0 55 45 100 100 100h100v100h-250c-28 0 -50 22 -50 50v500v2c0 20 15 42 34 48zM100 600v-400h600v400h-600z" />
|
||||
<glyph glyph-name="9f" unicode=""
|
||||
d="M272 700c-14 -40 -22 -83 -22 -128c0 -221 179 -400 400 -400c45 0 88 8 128 22c-53 -158 -202 -272 -378 -272c-221 0 -400 179 -400 400c0 176 114 325 272 378z" />
|
||||
<glyph glyph-name="a0" unicode=""
|
||||
d="M350 700l150 -150h-100v-150h150v100l150 -150l-150 -150v100h-150v-150h100l-150 -150l-150 150h100v150h-150v-100l-150 150l150 150v-100h150v150h-100z" />
|
||||
<glyph glyph-name="a1" unicode=""
|
||||
d="M800 800v-550c0 -83 -67 -150 -150 -150s-150 67 -150 150s67 150 150 150c17 0 35 -4 50 -9v206c-201 -6 -327 -27 -400 -50v-397c0 -83 -67 -150 -150 -150s-150 67 -150 150s67 150 150 150c17 0 35 -4 50 -9v409s100 100 600 100z" />
|
||||
<glyph glyph-name="a2" unicode="" horiz-adv-x="700"
|
||||
d="M499 700c51 0 102 -20 141 -59c78 -78 78 -203 0 -281l-250 -244c-48 -48 -127 -48 -175 0s-48 127 0 175l96 97l69 -69l-90 -94l-7 -3c-10 -10 -10 -28 0 -38s28 -10 38 0l250 247c37 40 39 102 0 141s-104 40 -144 0l-278 -275c-66 -69 -68 -179 0 -247
|
||||
c69 -69 181 -69 250 0l9 12l116 113l69 -69l-125 -125c-107 -107 -281 -107 -388 0s-107 281 0 388l278 272c39 39 90 59 141 59z" />
|
||||
<glyph glyph-name="a3" unicode=""
|
||||
d="M600 800l200 -200l-100 -100l-200 200zM400 600l200 -200l-400 -400h-200v200z" />
|
||||
<glyph glyph-name="a4" unicode=""
|
||||
d="M550 800c83 0 150 -90 150 -200s-67 -200 -150 -200c-22 0 -40 8 -59 19c6 26 9 52 9 81c0 84 -27 158 -72 212c27 52 71 88 122 88zM250 700c83 0 150 -90 150 -200s-67 -200 -150 -200s-150 90 -150 200s67 200 150 200zM725 384c44 -22 75 -66 75 -118v-166h-200v66
|
||||
c0 50 -17 96 -44 134c66 2 126 33 169 84zM75 284c45 -53 106 -84 175 -84s130 31 175 84c44 -22 75 -66 75 -118v-166h-500v166c0 52 31 96 75 118z" />
|
||||
<glyph glyph-name="a5" unicode=""
|
||||
d="M400 800c110 0 200 -112 200 -250s-90 -250 -200 -250s-200 112 -200 250s90 250 200 250zM191 300c54 -61 128 -100 209 -100s155 39 209 100c106 -5 191 -92 191 -200v-100h-800v100c0 108 85 195 191 200z" />
|
||||
<glyph glyph-name="a6" unicode="" horiz-adv-x="600"
|
||||
d="M19 800h462c11 0 19 -8 19 -19v-762c0 -11 -8 -19 -19 -19h-462c-11 0 -19 8 -19 19v762c0 11 8 19 19 19zM100 700v-500h300v500h-300zM250 150c-28 0 -50 -22 -50 -50s22 -50 50 -50s50 22 50 50s-22 50 -50 50z" />
|
||||
<glyph glyph-name="a7" unicode=""
|
||||
d="M350 800c17 0 34 -1 50 -3v-397l-297 297c63 64 150 103 247 103zM500 694c169 -25 300 -168 300 -344c0 -193 -157 -350 -350 -350c-85 0 -161 31 -222 81l272 272v341zM91 562l237 -234l-212 -212c-70 55 -116 138 -116 234c0 84 35 158 91 212z" />
|
||||
<glyph glyph-name="a8" unicode=""
|
||||
d="M92 650c0 23 20 50 46 50h3h4h5h400c28 0 50 -22 50 -50s-22 -50 -50 -50h-50v-200h100c55 0 100 -45 100 -100h-300v-300l-56 -100l-44 100v300h-300c0 55 45 100 100 100h100v200h-50c-2 0 -6 -1 -8 -1c-28 0 -50 23 -50 51z" />
|
||||
<glyph glyph-name="a9" unicode=""
|
||||
d="M400 800c221 0 400 -179 400 -400s-179 -400 -400 -400s-400 179 -400 400s179 400 400 400zM300 600v-400l300 200z" />
|
||||
<glyph glyph-name="aa" unicode=""
|
||||
d="M300 800h200v-300h300v-200h-300v-300h-200v300h-300v200h300v300z" />
|
||||
<glyph glyph-name="ab" unicode=""
|
||||
d="M300 800h100v-400h-100v400zM172 656l62 -78l-40 -31c-58 -46 -94 -117 -94 -197c0 -139 111 -250 250 -250s250 111 250 250c0 80 -39 151 -97 197l-37 31l62 78l38 -31c82 -64 134 -164 134 -275c0 -193 -157 -350 -350 -350s-350 157 -350 350c0 111 53 211 134 275z
|
||||
" />
|
||||
<glyph glyph-name="ac" unicode=""
|
||||
d="M200 800h400v-200h-400v200zM9 500h782c6 0 9 -3 9 -9v-282c0 -6 -3 -9 -9 -9h-91v200h-600v-200h-91c-6 0 -9 3 -9 9v282c0 6 3 9 9 9zM200 300h400v-300h-400v300z" />
|
||||
<glyph glyph-name="ad" unicode=""
|
||||
d="M0 700h100v-700h-100v700zM700 700h100v-700h-100v700zM200 600h200v-100h-200v100zM300 400h200v-100h-200v100zM400 200h200v-100h-200v100z" />
|
||||
<glyph glyph-name="ae" unicode=""
|
||||
d="M325 700c42 -141 87 -280 131 -419c29 74 59 148 88 222c30 -57 58 -114 87 -172h169v-100h-231l-13 28c-37 -92 -74 -184 -112 -275c-38 129 -79 257 -119 385c-42 -133 -83 -267 -125 -400c-28 88 -56 175 -84 262h-116v100h188l9 -34l3 -6c42 137 83 273 125 409z" />
|
||||
<glyph glyph-name="af" unicode=""
|
||||
d="M200 600c0 57 43 100 100 100s100 -43 100 -100c0 -28 -18 -48 -28 -72c-3 -6 -3 -16 -3 -28h231v-231c12 0 22 0 28 3c24 10 44 28 72 28c57 0 100 -43 100 -100s-43 -100 -100 -100c-28 0 -48 18 -72 28c-6 3 -16 3 -28 3v-231h-231c0 12 0 22 3 28c10 24 28 44 28 72
|
||||
c0 57 -43 100 -100 100s-100 -43 -100 -100c0 -28 18 -48 28 -72c3 -6 3 -16 3 -28h-231v600h231c0 12 0 22 -3 28c-10 24 -28 44 -28 72z" />
|
||||
<glyph glyph-name="b0" unicode="" horiz-adv-x="500"
|
||||
d="M247 700c84 0 148 -20 191 -59s59 -93 59 -141c0 -117 -69 -181 -119 -225s-81 -67 -81 -150v-25h-100v25c0 117 65 181 115 225s85 67 85 150c0 25 -8 48 -28 66s-56 34 -122 34s-97 -18 -116 -37s-27 -43 -31 -69l-100 12c5 38 19 88 59 128s103 66 188 66zM197 0h100
|
||||
v-100h-100v100z" />
|
||||
<glyph glyph-name="b1" unicode=""
|
||||
d="M450 800c138 0 250 -112 250 -250v-50c58 -21 100 -85 100 -150c0 -69 -48 -127 -112 -144c-22 55 -75 94 -138 94c-20 0 -39 -5 -56 -12c-17 64 -75 112 -144 112s-127 -48 -144 -112c-17 7 -36 12 -56 12c-37 0 -71 -12 -97 -34c-33 36 -53 82 -53 134
|
||||
c0 110 90 200 200 200c23 114 129 200 250 200zM334 300h4h3c3 0 6 1 9 1c28 0 50 -22 50 -50v-1v-200c0 -28 -22 -50 -50 -50s-50 22 -50 50v200v2c0 20 15 42 34 48zM134 200h4h3c3 0 6 1 9 1c28 0 50 -22 50 -50v-1v-100c0 -28 -22 -50 -50 -50s-50 22 -50 50v100v2
|
||||
c0 20 15 42 34 48zM534 200h3h4c3 0 6 1 9 1c28 0 50 -22 50 -50v-1v-100c0 -28 -22 -50 -50 -50s-50 22 -50 50v100v2c0 20 15 42 34 48z" />
|
||||
<glyph glyph-name="b2" unicode=""
|
||||
d="M600 800l200 -150l-200 -150v100h-50l-153 -191l175 -206l6 -3h22v100l200 -150l-200 -150v100h-25c-35 0 -56 12 -78 38l-166 190l-153 -190c-22 -27 -43 -38 -78 -38h-100v100h100l166 206l-163 191l-3 3h-100v100h100c34 0 56 -12 78 -38l153 -178l141 178
|
||||
c22 27 43 38 78 38h50v100z" />
|
||||
<glyph glyph-name="b3" unicode=""
|
||||
d="M400 800c110 0 209 -47 281 -119l119 119v-300h-300l109 109c-54 55 -126 91 -209 91c-166 0 -300 -134 -300 -300s134 -300 300 -300c83 0 158 34 212 88l72 -72c-72 -72 -174 -116 -284 -116c-220 0 -400 180 -400 400s180 400 400 400z" />
|
||||
<glyph glyph-name="b4" unicode=""
|
||||
d="M400 800h400v-400l-166 166l-400 -400l166 -166h-400v400l166 -166l400 400z" />
|
||||
<glyph glyph-name="b5" unicode="" horiz-adv-x="600"
|
||||
d="M250 800l250 -300h-200v-200h200l-250 -300l-250 300h200v200h-200z" />
|
||||
<glyph glyph-name="b6" unicode=""
|
||||
d="M300 600v-200h200v200l300 -250l-300 -250v200h-200v-200l-300 250z" />
|
||||
<glyph glyph-name="b7" unicode=""
|
||||
d="M0 800c441 0 800 -359 800 -800h-200c0 333 -267 600 -600 600v200zM0 500c275 0 500 -225 500 -500h-200c0 167 -133 300 -300 300v200zM0 200c110 0 200 -90 200 -200h-200v200z" />
|
||||
<glyph glyph-name="b8" unicode=""
|
||||
d="M100 800c386 0 700 -314 700 -700h-100c0 332 -268 600 -600 600v100zM100 600c276 0 500 -224 500 -500h-100c0 222 -178 400 -400 400v100zM100 400c165 0 300 -135 300 -300h-100c0 111 -89 200 -200 200v100zM100 200c55 0 100 -45 100 -100s-45 -100 -100 -100
|
||||
s-100 45 -100 100s45 100 100 100z" />
|
||||
<glyph glyph-name="b9" unicode=""
|
||||
d="M300 800h400c55 0 100 -45 100 -100v-200h-400v150c0 28 -22 50 -50 50s-50 -22 -50 -50v-250h400v-300c0 -55 -45 -100 -100 -100h-500c-55 0 -100 45 -100 100v200h100v-150c0 -28 22 -50 50 -50s50 22 50 50v550c0 55 45 100 100 100z" />
|
||||
<glyph glyph-name="ba" unicode=""
|
||||
d="M75 700h225v-100h-200v-500h400v100h100v-125c0 -41 -34 -75 -75 -75h-450c-41 0 -75 34 -75 75v550c0 41 34 75 75 75zM600 700l200 -200l-200 -200v100h-200c-94 0 -173 -65 -194 -153c23 199 189 353 394 353v100z" />
|
||||
<glyph glyph-name="bb" unicode=""
|
||||
d="M500 700l300 -284l-300 -316v200h-100c-200 0 -348 -102 -400 -300c0 295 100 500 500 500v200z" />
|
||||
<glyph glyph-name="bc" unicode=""
|
||||
d="M381 791l19 9l19 -9c127 -53 253 -108 381 -160v-31c0 -166 -67 -313 -147 -419c-40 -53 -83 -97 -125 -128s-82 -53 -128 -53s-86 22 -128 53s-85 75 -125 128c-80 107 -147 253 -147 419v31c128 52 254 107 381 160zM400 100v591l-294 -122c8 -126 58 -243 122 -328
|
||||
c35 -46 73 -86 106 -110s62 -31 66 -31z" />
|
||||
<glyph glyph-name="bd" unicode=""
|
||||
d="M600 800h100v-800h-100v800zM400 700h100v-700h-100v700zM200 500h100v-500h-100v500zM0 300h100v-300h-100v300z" />
|
||||
<glyph glyph-name="be" unicode=""
|
||||
d="M300 800h100v-200h200l100 -100l-100 -100h-200v-400h-100v500h-200l-100 100l100 100h200v100z" />
|
||||
<glyph glyph-name="bf" unicode=""
|
||||
d="M200 800h100v-600h200l-250 -200l-250 200h200v600zM400 800h200v-100h-200v100zM400 600h300v-100h-300v100zM400 400h400v-100h-400v100z" />
|
||||
<glyph glyph-name="c0" unicode=""
|
||||
d="M200 800h100v-600h200l-250 -200l-250 200h200v600zM400 800h400v-100h-400v100zM400 600h300v-100h-300v100zM400 400h200v-100h-200v100z" />
|
||||
<glyph glyph-name="c1" unicode=""
|
||||
d="M75 700h650c41 0 75 -34 75 -75v-550c0 -41 -34 -75 -75 -75h-650c-41 0 -75 34 -75 75v550c0 41 34 75 75 75zM100 600v-100h100v100h-100zM300 600v-100h400v100h-400zM100 400v-100h100v100h-100zM300 400v-100h400v100h-400zM100 200v-100h100v100h-100zM300 200
|
||||
v-100h400v100h-400z" />
|
||||
<glyph glyph-name="c2" unicode=""
|
||||
d="M400 800l100 -300h300l-250 -200l100 -300l-250 200l-250 -200l100 300l-250 200h300z" />
|
||||
<glyph glyph-name="c3" unicode=""
|
||||
d="M400 800c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM150 700c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM650 700c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM400 600c110 0 200 -90 200 -200
|
||||
s-90 -200 -200 -200s-200 90 -200 200s90 200 200 200zM50 450c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM750 450c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM150 200c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50
|
||||
s22 50 50 50zM650 200c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50zM400 100c28 0 50 -22 50 -50s-22 -50 -50 -50s-50 22 -50 50s22 50 50 50z" />
|
||||
<glyph glyph-name="c4" unicode=""
|
||||
d="M34 800h632c18 0 34 -16 34 -34v-732c0 -18 -16 -34 -34 -34h-632c-18 0 -34 16 -34 34v732c0 18 16 34 34 34zM100 700v-500h500v500h-500zM350 150c-38 0 -63 -42 -44 -75s69 -33 88 0s-6 75 -44 75z" />
|
||||
<glyph glyph-name="c5" unicode=""
|
||||
d="M0 800h300l500 -500l-300 -300l-500 500v300zM200 700c-55 0 -100 -45 -100 -100s45 -100 100 -100s100 45 100 100s-45 100 -100 100z" />
|
||||
<glyph glyph-name="c6" unicode=""
|
||||
d="M0 600h200l300 -300l-200 -200l-300 300v200zM340 600h160l300 -300l-200 -200l-78 78l119 122zM150 500c-28 0 -50 -22 -50 -50s22 -50 50 -50s50 22 50 50s-22 50 -50 50z" />
|
||||
<glyph glyph-name="c7" unicode=""
|
||||
d="M400 800c220 0 400 -180 400 -400s-180 -400 -400 -400s-400 180 -400 400s180 400 400 400zM400 700c-166 0 -300 -134 -300 -300s134 -300 300 -300s300 134 300 300s-134 300 -300 300zM400 600c110 0 200 -90 200 -200s-90 -200 -200 -200s-200 90 -200 200
|
||||
s90 200 200 200zM400 500c-56 0 -100 -44 -100 -100s44 -100 100 -100s100 44 100 100s-44 100 -100 100z" />
|
||||
<glyph glyph-name="c8" unicode=""
|
||||
d="M0 700h559l-100 -100h-359v-500h500v159l100 100v-359h-700v700zM700 700l100 -100l-400 -400l-200 200l100 100l100 -100z" />
|
||||
<glyph glyph-name="c9" unicode=""
|
||||
d="M9 800h782c6 0 9 -3 9 -9v-782c0 -6 -3 -9 -9 -9h-782c-6 0 -9 3 -9 9v782c0 6 3 9 9 9zM150 722l-72 -72l100 -100l-100 -100l72 -72l172 172zM400 500v-100h300v100h-300z" />
|
||||
<glyph glyph-name="ca" unicode=""
|
||||
d="M0 800h800v-200h-50c0 55 -45 100 -100 100h-150v-550c0 -28 22 -50 50 -50h50v-100h-400v100h50c28 0 50 22 50 50v550h-150c-55 0 -100 -45 -100 -100h-50v200z" />
|
||||
<glyph glyph-name="cb" unicode=""
|
||||
d="M0 700h100v-400h-100v400zM200 700h350c21 0 39 -13 47 -31c0 0 103 -291 103 -319s-22 -50 -50 -50h-150c-28 0 -50 -25 -50 -50s39 -158 47 -184s-5 -55 -31 -63s-52 5 -66 31s-109 219 -128 238s-44 28 -72 28v400z" />
|
||||
<glyph glyph-name="cc" unicode=""
|
||||
d="M400 666c10 19 28 32 47 34l19 -3c26 -8 39 -37 31 -63s-47 -159 -47 -184s22 -50 50 -50h150c28 0 50 -22 50 -50s-103 -319 -103 -319c-8 -18 -26 -31 -47 -31h-350v400c28 0 53 9 72 28s114 212 128 238zM0 400h100v-400h-100v400z" />
|
||||
<glyph glyph-name="cd" unicode=""
|
||||
d="M200 700h300v-100h-100v-6c25 -4 50 -8 72 -16l-34 -94c-28 11 -58 16 -88 16c-139 0 -250 -111 -250 -250s111 -250 250 -250s250 111 250 250c0 31 -5 60 -16 88l91 37c14 -38 25 -81 25 -125c0 -193 -157 -350 -350 -350s-350 157 -350 350c0 176 130 323 300 347v3
|
||||
h-100v100zM700 584c0 0 -296 -348 -316 -368s-48 -20 -68 0s-20 48 0 68s384 300 384 300z" />
|
||||
<glyph glyph-name="ce" unicode=""
|
||||
d="M600 700l200 -150l-200 -150v100h-600v100h600v100zM200 300v-100h600v-100h-600v-100l-200 150z" />
|
||||
<glyph glyph-name="cf" unicode=""
|
||||
d="M300 800h100c55 0 100 -45 100 -100h100c55 0 100 -45 100 -100h-700c0 55 45 100 100 100h100c0 55 45 100 100 100zM100 500h100v-350c0 -28 22 -50 50 -50s50 22 50 50v350h100v-350c0 -28 22 -50 50 -50s50 22 50 50v350h100v-481c0 -11 -8 -19 -19 -19h-462
|
||||
c-11 0 -19 8 -19 19v481z" />
|
||||
<glyph glyph-name="d0" unicode=""
|
||||
d="M100 800h200v-400c0 -55 45 -100 100 -100s100 45 100 100v400h100v-400c0 -110 -90 -200 -200 -200h-50c-138 0 -250 90 -250 200v400zM0 100h700v-100h-700v100z" />
|
||||
<glyph glyph-name="d1" unicode=""
|
||||
d="M9 700h182c6 0 9 -3 9 -9v-482c0 -6 -3 -9 -9 -9h-182c-6 0 -9 3 -9 9v482c0 6 3 9 9 9zM609 700h182c6 0 9 -3 9 -9v-482c0 -6 -3 -9 -9 -9h-182c-6 0 -9 3 -9 9v482c0 6 3 9 9 9zM309 500h182c6 0 9 -3 9 -9v-282c0 -6 -3 -9 -9 -9h-182c-6 0 -9 3 -9 9v282
|
||||
c0 6 3 9 9 9zM0 100h800v-100h-800v100z" />
|
||||
<glyph glyph-name="d2" unicode=""
|
||||
d="M10 700h181c6 0 9 -3 9 -9v-191h-200v191c0 6 4 9 10 9zM610 700h181c6 0 9 -3 9 -9v-191h-200v191c0 6 5 9 10 9zM310 600h181c6 0 9 -3 9 -9v-91h-200v91c0 6 4 9 10 9zM0 400h800v-100h-800v100zM0 200h200v-191c0 -6 -3 -9 -9 -9h-182c-6 0 -9 3 -9 9v191zM300 200
|
||||
h200v-91c0 -6 -3 -9 -9 -9h-181c-6 0 -10 3 -10 9v91zM600 200h200v-191c0 -6 -3 -9 -9 -9h-181c-6 0 -10 3 -10 9v191z" />
|
||||
<glyph glyph-name="d3" unicode=""
|
||||
d="M0 700h800v-100h-800v100zM9 500h182c6 0 9 -3 9 -9v-482c0 -6 -3 -9 -9 -9h-182c-6 0 -9 3 -9 9v482c0 6 3 9 9 9zM309 500h182c6 0 9 -3 9 -9v-282c0 -6 -3 -9 -9 -9h-182c-6 0 -9 3 -9 9v282c0 6 3 9 9 9zM609 500h182c6 0 9 -3 9 -9v-482c0 -6 -3 -9 -9 -9h-182
|
||||
c-6 0 -9 3 -9 9v482c0 6 3 9 9 9z" />
|
||||
<glyph glyph-name="d4" unicode=""
|
||||
d="M50 600h500c28 0 50 -22 50 -50v-150l100 100h100v-300h-100l-100 100v-150c0 -28 -22 -50 -50 -50h-500c-28 0 -50 22 -50 50v400c0 28 22 50 50 50z" />
|
||||
<glyph glyph-name="d5" unicode=""
|
||||
d="M334 800h66v-800h-66l-134 200h-200v400h200zM500 600v100c26 0 52 -4 75 -10c130 -33 225 -150 225 -290s-95 -258 -225 -291h-3c-23 -6 -47 -9 -72 -9v100c17 0 34 2 50 6c86 22 150 100 150 194s-64 172 -150 194c-16 4 -33 6 -50 6zM500 500l25 -3
|
||||
c44 -11 75 -51 75 -97s-32 -86 -75 -97l-25 -3v200z" />
|
||||
<glyph glyph-name="d6" unicode="" horiz-adv-x="600"
|
||||
d="M334 800h66v-800h-66l-134 200h-200v400h200zM500 500l25 -3c44 -11 75 -51 75 -97s-32 -86 -75 -97l-25 -3v200z" />
|
||||
<glyph glyph-name="d7" unicode="" horiz-adv-x="400"
|
||||
d="M334 800h66v-800h-66l-134 200h-200v400h200z" />
|
||||
<glyph glyph-name="d8" unicode=""
|
||||
d="M309 800h82c6 0 10 -4 12 -9l294 -682l3 -19v-81c0 -6 -3 -9 -9 -9h-682c-6 0 -9 3 -9 9v81l3 19l294 682c2 5 6 9 12 9zM300 500v-200h100v200h-100zM300 200v-100h100v100h-100z" />
|
||||
<glyph glyph-name="d9" unicode=""
|
||||
d="M375 800c138 0 269 -39 378 -109l-53 -82c-93 60 -205 91 -325 91c-119 0 -229 -32 -322 -91l-53 82c109 70 237 109 375 109zM375 500c78 0 154 -23 216 -62l-53 -85c-46 30 -104 47 -163 47c-60 0 -112 -17 -159 -47l-54 85c62 40 134 62 213 62zM375 200
|
||||
c55 0 100 -45 100 -100s-45 -100 -100 -100s-100 45 -100 100s45 100 100 100z" />
|
||||
<glyph glyph-name="da" unicode="" horiz-adv-x="900"
|
||||
d="M551 800c16 0 32 0 47 -3l-97 -97v-200h200l97 97c3 -15 3 -31 3 -47c0 -138 -112 -250 -250 -250c-32 0 -62 8 -90 19l-288 -291c-20 -20 -46 -28 -72 -28s-52 8 -72 28c-39 39 -39 105 0 144l291 287c-11 28 -19 59 -19 91c0 138 112 250 250 250zM101 150
|
||||
c-28 0 -50 -22 -50 -50s22 -50 50 -50s50 22 50 50s-22 50 -50 50z" />
|
||||
<glyph glyph-name="db" unicode=""
|
||||
d="M141 700c84 -84 169 -167 253 -250c82 83 167 165 247 250l143 -141l-253 -253c84 -82 167 -166 253 -247l-143 -143c-81 86 -165 169 -247 253l-253 -253l-141 143c85 80 167 164 250 247c-83 84 -166 169 -250 253z" />
|
||||
<glyph glyph-name="dc" unicode=""
|
||||
d="M0 800h100l231 -300h38l231 300h100l-225 -300h225v-100h-300v-100h300v-100h-300v-200h-100v200h-300v100h300v100h-300v100h225z" />
|
||||
<glyph glyph-name="dd" unicode="" horiz-adv-x="900"
|
||||
d="M350 800c193 0 350 -157 350 -350c0 -61 -17 -119 -44 -169c4 -2 10 -6 13 -9l103 -100c16 -16 30 -49 30 -72c0 -56 -46 -102 -102 -102c-23 0 -56 14 -72 30l-100 103c-3 3 -7 9 -9 13c-50 -28 -108 -44 -169 -44c-193 0 -350 157 -350 350s157 350 350 350zM350 700
|
||||
c-139 0 -250 -111 -250 -250s111 -250 250 -250c62 0 119 23 163 60c7 11 19 25 31 31l3 3c34 43 53 97 53 156c0 139 -111 250 -250 250zM300 600h100v-100h100v-100h-100v-100h-100v100h-100v100h100v100z" />
|
||||
<glyph glyph-name="de" unicode="" horiz-adv-x="900"
|
||||
d="M350 800c193 0 350 -157 350 -350c0 -61 -17 -119 -44 -169c4 -2 10 -6 13 -9l103 -100c16 -16 30 -49 30 -72c0 -56 -46 -102 -102 -102c-23 0 -56 14 -72 30l-100 103c-3 3 -7 9 -9 13c-50 -28 -108 -44 -169 -44c-193 0 -350 157 -350 350s157 350 350 350zM350 700
|
||||
c-139 0 -250 -111 -250 -250s111 -250 250 -250c62 0 119 23 163 60c7 11 19 25 31 31l3 3c34 43 53 97 53 156c0 139 -111 250 -250 250zM200 500h300v-100h-300v100z" />
|
||||
</font>
|
||||
</defs></svg>
|
After Width: | Height: | Size: 54 KiB |
Binary file not shown.
Binary file not shown.
|
@ -1,50 +0,0 @@
|
|||
defmodule Frenzy.OPML.ExporterTest do
|
||||
use ExUnit.Case
|
||||
alias Frenzy.OPML.Exporter
|
||||
alias Frenzy.{Feed, Group}
|
||||
doctest Exporter
|
||||
|
||||
test "export groups" do
|
||||
res =
|
||||
Exporter.export([
|
||||
%Group{
|
||||
title: "Group 1",
|
||||
feeds: [
|
||||
%Feed{
|
||||
feed_url: "https://shadowfacts.net/feed.xml",
|
||||
site_url: "https://shadowfacts.net/",
|
||||
title: "Shadowfacts"
|
||||
}
|
||||
]
|
||||
},
|
||||
%Group{
|
||||
title: "Group 2",
|
||||
feeds: [
|
||||
%Feed{
|
||||
feed_url: "some other url",
|
||||
site_url: "my site",
|
||||
title: "the title"
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
|
||||
assert res ==
|
||||
"""
|
||||
<opml version="1.0">
|
||||
<head>
|
||||
<title>Frenzy export</title>
|
||||
</head>
|
||||
<body>
|
||||
<outline text="Group 1" title="Group 1">
|
||||
<outline htmlUrl="https://shadowfacts.net/" text="Shadowfacts" title="Shadowfacts" type="rss" xmlUrl="https://shadowfacts.net/feed.xml"/>
|
||||
</outline>
|
||||
<outline text="Group 2" title="Group 2">
|
||||
<outline htmlUrl="my site" text="the title" title="the title" type="rss" xmlUrl="some other url"/>
|
||||
</outline>
|
||||
</body>
|
||||
</opml>
|
||||
"""
|
||||
|> String.trim()
|
||||
end
|
||||
end
|
|
@ -1,30 +0,0 @@
|
|||
defmodule Frenzy.OPML.ImporterTests do
|
||||
use ExUnit.Case
|
||||
alias Frenzy.OPML.Importer
|
||||
doctest Importer
|
||||
|
||||
@opml """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- OPML generated by NetNewsWire -->
|
||||
<opml version="1.1">
|
||||
<head>
|
||||
<title>Subscriptions-OnMyMac.opml</title>
|
||||
</head>
|
||||
<body>
|
||||
<outline text="Julia Evans" title="Julia Evans" description="" type="rss" version="RSS" htmlUrl="" xmlUrl="https://jvns.ca/atom.xml"/>
|
||||
<outline text="my folder" title="my folder">
|
||||
<outline text="The Shape of Everything" title="The Shape of Everything" description="" type="rss" version="RSS" htmlUrl="https://shapeof.com/" xmlUrl="https://shapeof.com/feed.json"/>
|
||||
</outline>
|
||||
</body>
|
||||
</opml>
|
||||
"""
|
||||
|
||||
test "parse simple OPML" do
|
||||
res = Importer.parse_opml(@opml)
|
||||
|
||||
assert res == %{
|
||||
:default => ["https://jvns.ca/atom.xml"],
|
||||
"my folder" => ["https://shapeof.com/feed.json"]
|
||||
}
|
||||
end
|
||||
end
|
|
@ -18,8 +18,7 @@ defmodule FrenzyWeb.ConnCase do
|
|||
using do
|
||||
quote do
|
||||
# Import conveniences for testing with connections
|
||||
import Plug.Conn
|
||||
import Phoenix.ConnTest
|
||||
use Phoenix.ConnTest
|
||||
alias FrenzyWeb.Router.Helpers, as: Routes
|
||||
|
||||
# The default endpoint for testing
|
||||
|
|
Loading…
Reference in New Issue